Elixir is a dynamic, functional programming language for building scalable and maintainable applications.
It’s runs on the BEAM virtual machine used to implement the Erlang programming language. Elixir builds on top of Erlang and shares the same abstractions for building distributed, fault-tolerant applications. It’s means you can use all the packages that is built for Erlang in your Elixir applications.
Let’s understand Elixir language constructs using the JavaScript language constructs.
Credits
https://elixirforajs.dev/ (I am adding more examples for my reference as a post here. All credits to this original website)
Code Conventions
Elixir uses snake_cased naming whereas JavaScript uses camelCasing.
E.g.
JavaScript
// camelCasing
const welcomeMessage = "Welcome to Elixir for JavaScript Devs";
const welcomeStatement = () => {
return "Welcome to Elixir for JS Devs";
}
Elixir
# snake_cased
welcome_message = "Welcome to Elixir for JS Devs"
def welcome_statement do
"Welcome to Elixir for JS Devs"
end
Code comments
JavaScript uses the standard ‘C’ style comments. /* Comments goes here */
For single line comments two forward slash can be used as well // Like this
Variable Declarations (or binding)
If you know how to declare variables in JavaScript, well you almost know how it works in Elixir as well.
JavaScript
const foo = 1;
let bar = "bar";
var baz = "baz";
// Export constants for reuse and to avoid duplication
export const URL = "https://teachyourselfcoding.com";
// Symbols
let s = Symbol('foo');
Elixir
foo = 1
# atoms are elixir variables that are used to represent a contant whose
# value is it's own name\
# Example
:ok
:error
# For exporting the variables or functions to be used by another module declare a module attribute
defmodule MySharedModule do
@foo 1
end
# Note that module attribute values are computed at compile time
# so assigning the return value of a function call to a module attribute
# will remain that value once compiled.
defmodule MySharedModule do
@foo DateTime.utc_now()
end
Comparison Operator
Things get a bit different here. See the pin ^ operator in the example below.
JavaScript
"hi" = "hi" // Uncaught SyntaxError
"hi" == "hi" // true
"hi" === "hi" // true
Elixir
"hi" = "hi" # "hi"
"hello" = "hi" # (MatchError)
"hi" == "hi" #true
"hello" == "hi" false
# More example
a = 10 # 10
10 = a # 10 as both matches
20 = a # ** (MatchError) no match of right hand side value: 10
# (note if right side is a const and left hand side is a variable then the right hand side is assigned to lhs)
a = 20 # 20
# Use pin operator (^) to pin the variable for comparison
a = 20
^a = 20 # 20
^a = 30 # ** (MatchError) no match of right hand side value: 20
In Elixir “=” is the match operator. If the value on the left hand side is a variable and right hand side a const then the right hand side value is assigned to the left hand side variable.
Imports
JavaScript/ES6
import React, { useState } from 'react';
import React as MyReact from 'react';
Elixir
# In elixir, you can use a fully qualified name without importing,
# or you can import like this:
import MyProject.HelperModule
# or alias HelperModule to avoid typing the fully qualified name when used
alias MyProject.HelperModule
# or if you only want to mixin certain functions into your module from
# HelperModule:
import MyProject.HelperModule, only: [my_function, 1]
Functions
JavaScript
// Normal function
function add(n1, n2) {
return n1 + n2;
}
// Arrow function
const add = (n1, n2) => n1 + n2;
Elixir
# Normal function
def add(n1, n2) do
n1 + n2
end
# Arrow function
add = fn(a, b) -> a + b end
# Using capture syntax
add = &(&1 + &2)
# You can invoke the above function as shown below
add.(1,2) # 3
# functions in elixir can be private
defp private_add(n1, n2) do
n1 + n2
end
Method Chaining
JavaScript
const people = [{name: 'Alice', age: 18}, {name: 'Bob', age: 30}];
const filteredAndMapped = people
.filter({age} => age > 21)
.map({name} => name); // Bob
Elixir (|> pipe operator)
people = [%{name: "Bob", age: 30}, %{name: "Alice", age: 18}]
old_names = people
|> Enum.filter(fn %{age: age} -> age > 21 end)
|> Enum.map(fn n -> n.name end)
# The above code is same as
old_names = Enum.filter(people, fn %{age: age} -> age > 21 end)
names = Enum.map(old_names, fn n-> n.name end) # ["Bob"]
NOTE: The pipe operator will be available in the upcoming ECMA script edition. The proposal is here and can be currently used with Babel as transpiler as well.
https://github.com/tc39/proposal-pipeline-operator
The pipe operator enables passing implicit parameter from one function to another.
Destructuring
JavaScript
const o = { nested: { prop: 'Hi!' } };
const { nested: { prop } = {} } = o;
console.log(prop);
// Hi!
Elixir
o = %{nested: %{prop: "Hi!"}}
%{nested: %{prop: prop}} = o
IO.inspect(prop) # Hi!
Pattern Matching
JavaScript
const list = ( user ) => {
if (user.isAdmin) {
return doSomething();
}
return doSomethingElse();
}
Elixir
# if the user passed in is an admin, this
# function will be called
defp list(%{is_admin: true}) do
doSomething()
end
# regular users have this function called
defp list(user) do
doSomethingElse()
end
Control Flow
JavaScript
const response = fetchApi("https://someurl.com/posts");
switch (response.status) {
case 200:
return "Success";
case 401:
return "Not Allowed";
default:
return "There was an error";
}
Elixir
# very common in elixir to use case statements
# to match on the results of a function. In this
# example get_a_response returns a tuple where the
# first element is the status
case fetchApi("https://someurl.com/posts")do
{:ok, _} ->
"Success"
{:error, %{reason = "Unauthorized"}} ->
"Not Allowed"
{:error, _} ->
"There was an error"
end
cond do
foo == "foo" ->
"Success"
bar < 1 ->
"Not Allowed"
_ -> # This is default clause
"There was an error"
end
If/Else Statements
JavaScript
if (n > 100) {
return 100;
} else {
return n;
}
n > 100 ? 100 : n;
Elixir
if n > 100 do
100
else
n
end
# unless keyword is a sort of reverse if
unless n > 100 do
n
else
100
end
# ternary isn't really directly supported, but this version of
# an if statement is one line and very easy to read
if n > 100, do: 100, else: n
Common List Operations
JavaScript
const a = [1, 2, 3];
const b = a.map(n => n * 2);
// b is [2, 4, 6]
Elixir
a = [1, 2, 3]
b = Enum.map(a, fn n -> n * 2 end)
# b is [2, 4, 6]
Filter
JavaScript
const people = [{name: 'Bob', age: 30}, {name: 'Bill', age: 18}];
const oldEnough = people.filter({age} => age > 21);
Elixir
people = [%{name: "Bob", age: 30}, %{name: "Bill", age: 18}]
old_enough = Enum.filter(people, fn %{age: age} -> age > 21 end)
Reduce
JavaScript
const sum = [1, 2, 3].reduce((acc, n) => n + acc, 0)
// sum is 6
Elixir
sum = Enum.reduce([1, 2, 3], 0, fn n, acc -> n + acc end)
# sum is 6
Object
JavaScript
const Square = {
unit: "sq",
area: (s) => `${s * s} ${Square.unit} unit `,
};
console.log(Square.area(4)); // 16 sq unit
Elixir
defmodule Square do
def unit, do: "sq"
def area(s), do:
"#{s * s} #{unit} unit"
end
Square.area(4) # 16 sq unit
Closures
A closure refers to the value of variable or a function in an outer scope being available in the context of the inner scope, even if that inner scope is invoked at a much later and outer scope is no longer available.
JavaScript – Example 1
function fnFactory() {
let number = 7;
return () => console.log(number);
}
let someFunction = fnFactory();
someFunction(); // Log 7
Here in the above example even thought the ‘number’ variable has gone out of scope after the invocation of fnFactory(), it’s value was preserved because it was referenced in the inner scope of the function that was returned. That’s one example of closure for you.
JavaScript – Example 2
What happens if you change the value of a variable that was captured in a closure?
let firstName = "Pinku";
let printName = () => console.log(firstName );
printName(); // Pinku
firstName = "Chinku";
printName(); // Chinku
From the above example it is clear that closure in JavaScript will refer to the captured variable by reference. So, if the value of the captured variable is changed between the time it is captured and the time that it is used, the updated value will be reflected when it is used again.
Elixir
defmodule Example do
def fnFactory() do
number = 7
fn -> console.log(number) end
end
end
someFunction = Example.fnFactory();
someFunction.(); // Log 7 (Note the '.' after someFunction)
Elixir- Example 2
What happens if you change the value of a variable that was captured in a closure?
first_name = "Pinku"
print_name = fn -> IO.puts(first_name) end
print_name.() # Pinku
first_name = "Chinku"
print_name.() # Pinku (Observe the change in output here)
The ‘first_name’ variable printed by the closure always has the same value, even though the value of the ‘first_name’ has been changed.
So, we can concluded that in Elixir the ‘value’ of the variable is captured by the closure and not the reference to the variable. Since data in Elixir is immutable, the inner scope continues to refer to the original value and not the modified value, which is located somewhere else in memory.
This if kind of good in the sense that in a functional language side effects are not a good practice unless explicitly needed.
More examples of closures in anonymous function in Elixir.
animal_talk = fn (something) -> (fn -> IO.puts(something) end) end
say_dog = animal_talk .("dog")
say_cat = animal_talk .("cat")
say_dog.() # dog
say_cat.() # cat