Skip to content

Instantly share code, notes, and snippets.

@claritee
Last active December 22, 2019 09:01
Show Gist options
  • Save claritee/eabbb10b14549e1e4f2507f93f456f59 to your computer and use it in GitHub Desktop.
Save claritee/eabbb10b14549e1e4f2507f93f456f59 to your computer and use it in GitHub Desktop.
Elixir Notes

Installation

brew install elixir

REPL

Interactive Elixir

iex

Note: Hit ctrl-c twice to exit

List of libs

http://getawesomeness.herokuapp.com/get/elixir

Key Concepts

  • Variables are immutable
  • Not OO, data is passed in arguments
  • Actor model for concurrent tasks

Run a file

elixirc code.ex 

File extensions (see below in Mix command)

  • .ex files - generate production artifacts (.beam files). e.g. lib files
  • .exs files - dont generate production artifacts. Used for scripting, e.g. config or testing

Debugging

Run in iex shell

iex -S mix

In code:

require IEx;
IEx.pry

IO

IO.puts "Hello world"
IO.inspect(x)

Calling Erlang from Elixir

Prefix :

Syntax

:module.function_name

Example:

:random.uniform()
# 0.7230402056221108
:erlang.float_to_binary(7.12, [decimals: 2])
# "7.12"

Types

Decimals

0xA
0b1

Atom

:hello

Multi-line string

"""
Hello
World
"""

Check the type

is_map(arg)
is_list(arg)

etc

In the REPL

message = "hello"
is_bitstring(message) 	# true

byte_size(message) 		# 5

Lists

Stored as linked lists Immutable: Lists cannot be mutated, new values are always created. Good for concurrency

[1, "Hello", :an_atom, true]
IO.puts([104, 101, 108, 108, 111])  # 'hello' charset
is_list('Hello')                    # true
length([1, 2, :true, "str"])        # 4

Concatenation

[1, 2, 3] ++ [4, 5, 6]  # [1, 2, 3, 4, 5, 6]

Subtraction

[1, true, 2, false, 3, true] -- [true, false] 	# [1, 2, 3, true]

^ subtracts the first encountered value

Head & tail of list

list = [1, 2, 3]
hd(list)        # 1
tl(list)        # [2, 3]

Other functions

# list = [1, 2, 3]
List.delete(list, 2)                # [1, 3]
List.delete_at(list, 2)             # [1, 2]
List.first(list)                    # 1
List.flatten([1, 2, 3, [4, 5]])     # [1, 2, 3, 4, 5]
List.last(list)                     # 3

Iteration

def create_deck do
  values = ["Ace", "Two", "Three", "Four", "Five", ...]
  suits = ["Spades", "Clubs", "Hearts", "Diamonds"]
  for value <- values, suit <- suits do
    "#{value} of #{suit}"
  end
end

Lists grow at the head

x = [1, 2, 3]	# [1, 2, 3]
[0 | x ]		# [0, 1, 2, 3]

^ Prepend to the head of the list and then reverse

Enum.reverse([0 | x])	# [3, 2, 1, 0]

Storage in memory:

  • Stored as cons cells [left | right]
  • Stored in a tree structure - so it's fast to prepend, but slow to append elements e.g. [4 | list ]

Tuples

What: Ordered collection of elements

Can hold different types Stored in contiguous memory block Accessing values are quick Updating / inserting elements is slow

tuple = { 1, "Hello", :an_atom, true }
elem(tuple, 0)              # 1
tuple_size({:ok, "hello"})  # 2

Most of the time, use a Tuple using 2 values Where the first value is an atom

e.g.

{:ok, "some content"}

usage:
{status, content} = {:ok, "content"}
{error, message} = {:error, "some error"}

Appending

tuple = {:ok, "Hello"}      # {:ok, "Hello"}
Tuple.append(tuple, :world) # {:ok, "Hello", :world}

Inserting

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)   # {:foo, :bar, :baz}
new_tuple_2 = put_elem(tuple, 1, :foobar)       # {:bar, :foobar}

Variables

Convention: Snake case

hello = "hello world"

Can lead with _ but usually for variables not used again

Operators

div: div(10, 2) #5
rem: rem(20, 3) #Remainder: 2

Comparisons

=== type equality
!== type inequality

Logical Operators

and

true and "hello" # "hello"
true and false   # false
true and 0       # 0

and, or, && and || are short circuit operators

Pattern Matching

languages = ["Elixir", "Javascript", "Ruby"]
[first, second, third] = languages

OR

[head | tail] = languages # head = "Elixir", tail = ["Javascript", "Ruby"]

OR

[head | _ ] = languages   # the rest of the list is ignored
[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
# ["Random string", [:an_atom, {24}]]

The other values will be ignored if there is a _

Matching left and right values

a = 25
b = 25
^a = b

Example using concatenation <> (and pattern matching)

"John " <> last_name = "John Smith"
IO.puts last_name 	#Smith

Function Arguments

Example on function arguments (avoid if statements) In this case - avoid passing "type" argument to the function

defmodule Account do
	def run_transaction(balance, amount, :deposit) do
		balance + amount
	end
	def run_transaction(balance, amount, :withdrawal) do
		balance - amount
	end
end

Usage

Account.run_transaction(1000, 50, :deposit)

Function Pattern Matching

This uses the cons operator | to split the list

defmodule Language do
	def print_list([head | tail]) do
		IO.puts "Head: #{head}"
		IO.puts "Tail: #{tail}"
	end
end

Language.print_list(["Elixir", "Javascript", "Ruby"])

Recursion

No for loops, use recursion

e.g.

defmodule Language do
	def print_list([head | tail]) do
		IO.puts head
		print_list(tail) # remaining elements are passed
	end
	
	def print_list([]) do
	end
end

Another example with pattern matching:

  defp find_tax([%{ "state" => "FL", "rate" => rate } | _ ]) do
    rate
  end

  defp find_tax([_ | tail]) do
    find_tax(tail)
  end

  defp find_tax([]) do
    raise "FL rate not found"
  end

Conditionals

if boolean-statement do
   #Code to be executed if condition is satisfied
else
   #Code to be executed if condition is not satisfied
end
unless boolean-statement do
   #Code to be executed if condition is false
end

Cond

This works like if-else multiple conditionals When: checking multiple conditions

guess = 46
cond do
   guess == 10 -> IO.puts "You guessed 10!"
   guess == 46 -> IO.puts "You guessed 46!"
   guess == 42 -> IO.puts "You guessed 42!"
   true        -> IO.puts "I give up."
end

Another example

def valid_transfer?(amount, hourOfDay) do
	cond do
		hourOfDay < 12 -> amount <= 5000
		hourOfDay < 18 -> amount <= 1000
		true -> amount <= 300
	end
end

^ The true statement is last, as cond will raise an error if no conditions are met

Case

What: tests a value against a set of patterns

When: matching on multiple patterns (e.g. match on return values)

Like switch statement If pattern matches, then exit. Otherwise throw CaseClauseError

case 3 do
   1 -> IO.puts("Hi, I'm one")
   2 -> IO.puts("Hi, I'm two")
   3 -> IO.puts("Hi, I'm three")
   _ -> IO.puts("Oops, you dont match!")
end

Guard Clauses

Uses when

defmodule Account do
	def list_transactions(filename) do
		case File.read(filename) do
			{ :ok, content }
				when byte_size(content) > 10 -> "Content: #{content}"
			{ :error, type } -> "Error: #{type}"
		end	
	end
end

Code smell

Nested-if statements should be refactored to use case

e.g. Refactor this

defmodule Account do
	def list_transactions(filename) do
		if result == :ok do
			"Content: #{content}"
		else
			if result == :error do
				"Error: #{content}"
			end
		end
	end
end

To:

defmodule Account do
	def list_transactions(filename) do
		case File.read(filename) do
			{ :ok, content } -> "Content: #{content}"
			{ :error, type } -> "Error: #{type}"
		end	
	end
end

WIP

Strings

String length String.length("Hello")

Interpolation/Substitution "X-men #{x}"

String Concatenation

"Hello" <> " " <> "World"

Reversal

String.reverse("Elixir")

String comparison

== or the === operators

String Matching

Output: true / false

IO.puts(String.match?("foo", ~r/foo/))
IO.puts("foo" =~ ~r/foo/)

Conversion

Integer.parse("1")		# {1, ""}
String.to_integer("1")	# 1

Common methods

String.at("Hello", 1)               # "e"
String.capitalize("hi")             # "Hi"
String.contains?("Hello", "H")      # true
String.downcase("HI")               # "hi"
String.ends_with?("hello", "lo")    # true
String.first("hello")               # "h"
String.last("hello")                # "o"
String.replace("hello", "l", "p")   # "heppo"
String.slice("hello", 0, 2)         # "he"
String.slice("hello", -2, 2)        # "lo"
String.split("hello", "")           # ["h", "e", "l", "l", "o", ""]
String.upcase("hello")              # "HELLO"

UTF-8 and Encodings

http://elixir-lang.github.io/getting-started/binaries-strings-and-char-lists.html

UTF-8 is used as an encoding Special chars like ł need a certain amount of bytes to represent the char (code point is 322)

byte_size("hełło")      # 7
String.length("hełło")  # 11
is_binary("hełło")      # true

To get the codepoint:

?a  # 97
?ł  #322
String.codepoints("hełło") # ["h", "e", "ł", "ł", "o"]

Binaries and Bitstrings

A binary is a sequence of bytes enclosed in <<>>

<<0, 1, 2, 3>>              # <<0, 1, 2, 3>>
byte_size(<<0, 1, 2, 3>>)   # 4

To test if the binary makes a valid String:

String.valid?(<<239, 191, 19>>) # false
String.valid?(<<322, 97>>)      # true. The value is ał

String Concatenation

<<0, 1>> <> <<2, 3>>            # <<0, 1, 2, 3>> 
"hełło" <> <<0>>                # <<104, 101, 197, 130, 197, 130, 111, 0>>

Each binary only goes up to 255

<<255>> # <<255>>
<<256>> # truncated <<0>>

Specify number of bits to store the number

<<256 :: size(16)>> # use 16 bits (2 bytes) to store the number <<1, 0>>
<<256 :: utf8>>     # the number is a code point "Ā"
<<256 :: utf8, 0>>  # Store the overflow when binary is > 255: <<196, 128, 0>>

Passing in 1 bit

is_binary(<< 1 :: size(16)>>) # true is_binary(<< 1 :: size(32)>>) # true is_binary(<< 1 :: size(64)>>) # true is_binary(<< 1 :: size(128)>>) # true is_binary(<<1 :: size(15)>>) # false Pattern match binaries/bitstrings

<<0, 1, x>> = <<0, 1, 2>> # <<0, 1, 2>>

"he" <> rest = "hello" # "hello"
rest # "llo"

Charlists

Charlists are used mostly when interfacing with Erlang, in particular old libraries that do not accept binaries as arguments

'hełło'             # [104, 101, 322, 322, 111]
is_list 'hełło'     # true
is_list 'hi'        # false
List.first('hello') # 104

Converting between charlist and string

  • These functions are polymorphic (convert different types to strings)
to_charlist "hełło" # [104, 101, 322, 322, 111]
to_string 'hełło'   # "hełło"
to_string :hello    # "hello"
to_string 1         # "1"

Module Attributes

3 uses:

- They serve to annotate the module, often with information to be used by the user or the VM.
- They work as constants.
- They work as a temporary module storage to be used during compilation.

As constants:

defmodule MyServer do
  @initial_state %{host: "127.0.0.1", port: 3456}
  IO.inspect @initial_state
end

Ranges

range = 1..5	#1..5

Keyword Lists

What: List of two-value tuples

Usually used as options argument in a function

Features:

  • Keys must be atoms.
  • Keys are ordered, as specified by the developer.
  • Keys can be given more than once.

Syntax

[key: value]

E.g.

list = [{:a, 1}, {:b, 2}]
list == [a: 1, b: 2]

In a function:

defmodule Account do
	def balance(transactions, options) do
		currency = options[:currency]
		symbol = options[:symbol]
		balance = calculate_balance(transactions)
		"Balance in #{currency}: #{symbol}#{balance}"
	end
end

Account.balance(transactions, currency: "pounds", symbol: "£")

Other functions:

list ++ [c: 3]						# [a: 1, b: 2, c: 3]
new_list = [a: 0] ++ list			# [a: 0, a: 1, b: 2]
new_list[:a]						# 0 front values are fetched on lookup

Keyword.get(new_list, :b)						# 2
Keyword.get_values(new_list, :a)				# [0, 1]
Keyword.values(new_list)						# [0, 1, 2]
new_list = Keyword.put_new(new_list, :c, 5)	# [c: 5, a: 0, a: 1, b: 2]

Keyword.delete_first(new_list, :a)				# [c: 5, a: 1, b: 2]
Keyword.delete(new_list, :a)					# [c: 5, b: 2] - this would delete all instances of keys with :a
Keyword.get(new_list, :a)						# nil

if/2 macro

if false, do: :this, else: :that			# :that
if(false, [do: :this, else: :that])		# :that
if(false, [{:do, :this}, {:else, :that}])	# :that

Pattern Matching on keyword lists

[a: x, b: y] = [a: 1, b: 2]		# [a: 1, b: 2]
x	# 1
y	# 2

Not usually done as this requires matching items and order e.g.

[a: a] = [a: 1, b: 2]			# MatchError
[b: b, a: a] = [a: 1, b: 2] 	# MatchError

Equality

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
list_1 == list_2		# true

Maps

When: Want a collection of key-value pairs, simple efficient storage

Denoted by: %{}

Features

  • Maps allow any value as a key.
  • Maps' keys do not follow any ordering.
  • Pattern matching works well
  • All keys in a map are atoms

Docs: https://hexdocs.pm/elixir/Map.html

Example:

person = %{ "name" => "Brooke", "age" => 42 }

Map.fetch(person, "name")

Another example:

map = %{:a => 1, 2 => :b}	# %{2 => :b, :a => 1}

Retrieval

map[:a]				# 1
map[:c]				# nil
map.a				# 1
map[:b]				# nil
map[2]				# :b

Using Fetch

Map.fetch(map, 2) 	# {:ok, :b}
Map.fetch(map, :d)	# :error
Map.keys(map)		# [2, :a]
Map.values(map)		# [:b, 1]
Map.fetch(:c)		# nil
Map.fetch!(map, :a)	# 1, fetches only the value
Map.fetch!(map, :c)	# KeyError

Shorthand Syntax

%{"hello" => "world", a: 1, b: 2}	# %{:a => 1, :b => 2, "hello" => "world"}

Nested Structures

users = [
  john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
  mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]

Pattern matching:

  • Matched on the first subset of a value
person = %{ "name" => "Brooke", "age" => 42 }
%{ "name" => name, "age" => age } = person

Equivalent to:
%{ "name" => name, "age" => age } = %{ "name" => "Brooke", "age" => 42 }

Can Pattern match on portions

NOTE: Only Maps support partial match

person = %{ "name" => "Brooke", "age" => 42 }
${ "name" => name } = person

Nested example:

person = %{ "name" => "Sam", "age" => 31,
  "bank_info" => %{ "routing" => "001002", "account" => "123123" }
}

%{"bank_info" => %{"account" => account}} = person
IO.puts "Bank Account is #{account}"

Other uses:

%{} = %{:a => 1, 2 => :b} 						# %{2 => :b, :a => 1}
%{:a => a} = %{:a => 1, 2 => :b}				# %{2 => :b, :a => 1} => a is assigned
%{:a => 1, 2 => :b } = %{:a => 1, 2 => :b}		# %{2 => :b, :a => 1}
%{2 => :b} = %{:a => 1, 2 => :b}				# %{2 => :b, :a => 1}
%{:c => c} = %{:a => 1, 2 => :b}				# match error

Adding

Use: Map.put or:

mm = %{a: 1, b:2}
iex> %{mm | a: 0 }  	# %{a: 0, b: 2}
iex> mm			# %{a: 1, b: 2}

Equality

Map.equal?(%{:a =>  1}, %{:a => 1})	# true

Modules

defmodule - macro to create modules

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Math.sum(1, 2)	# 3

Module Nesting

defmodule Foo do
   
   defmodule Bar do
      
   end
end

Foo.Bar
Foo.Bar.Baz # still valid

You can define arbitrarily nested modules without defining any module in the chain

Scripting Modules

Extension: .exs

Execute: elixir math.exs

During execution - the file is compiled and load modules into memory

Functions

Functions can be

  • assigned to variables
  • passed as arguments to other functions

Defined with arity e.g. more than one function with same name but can have different number of params

Anonymous Functions

Anonymous functions

  • no name
  • no module

Creation:

fn ->

Invoking:

function_name.(args)

e.g.

max_balance = fn(amount) -> "Max: #{amount}" end  # Function<6.99386804/1 in :erl_eval.expr/5>
max_balance.(500)								   # "Max: 500"

Shorthand:

max_balance = &("Max: #{&1}")
max_balance.(500)

Passing function as argument

the function transaction is passed

defmodule Account do
	def run_transaction(balance, amount, transaction) do
		if balance <= 0 do
			"Cannot perform any transaction"
		else
			transaction.(balance, amount)
		end
	end
end

Pattern Matching

account_transaction = fn
  (balance, amount, :deposit) -> balance + amount
  (balance, amount, :withdrawal) -> balance - amount
end
account_transaction.(100, 40, :deposit)
account_transaction.(100, 40, :withdrawal)

Shorthand

deposit = fn(balance, amount) -> balance + amound end

Same as

deposit = &(&1 + &2) # &1 is balance, &2 is amount

Inline:

Account.run_transaction(1000, 20, &(&1 + & 2))

Default Arguments

Use \\ for optional args Use || to set default value

E.g.

defmodule Account do
	def balance(transactions, options \\ []) do
		currency = options[:currency] || "dollar"
		symbol = options[:symbol] || "$"
	end
end

Account.balance(transactions)

Private Functions

defmodule ModuleName do
	defp function_name do
		...
	end
end

^ Never called outside of it's module

Structs

Definition:

defmodule User do
   defstruct name: "John", age: 27
end

Creation:

new_john = %User{}						# %User{age: 27, name: "John"}
ayush = %User{name: "Ayush", age: 20}	# %User{age: 20, name: "Ayush"}
megan = %User{name: "Megan"}			# %User{age: 27, name: "Megan"}

Access:

john.name	# John
john.age	# 27

Update:

meg = %{john | name: "Meg"}		# %User{age: 27, name: "Meg"}

Note: Similar to Records

Records

When: you want more structured data

Must be within a Module

defmodule MyModule do
  require Record
  Record.defrecord :user, name: "john", age: 25
end

Importing a record

import User

Creation

record = user()        #=> {:user, "meg", 25}
record = user(age: 26) #=> {:user, "meg", 26}

Get a field

user(record, :name) #=> "meg"

Update record

user(record, age: 26) #=> {:user, "meg", 26}

To get the zero-based index of the field in record tuple (index 0 is occupied by the record "tag")

user(:name) #=> 1

Convert a record to a keyword list

user(record) #=> [name: "meg", age: 26]

TODO

Structs vs Records

TODO

Streams

Ranges are streams

range = 1..5				# 1..5
Enum.map range, &(&1 * 2)	# [2, 4, 6, 8, 10]
Enum.map range, &(&1 + 2)	# [3, 4, 5, 6, 7]

Streams map to a range

range = 1..3							# 1..3
stream = Stream.map(range, &(&1 * 2))  # Stream<[enum: 1..3, funs: [#Function<46.40091930/1 in Stream.map/2>]]>

Enum.map(stream, &(&1 + 1)) 			#[3, 5, 7]
[3, 5, 7]								#[3, 5, 7]

Stream to List

Enum.to_list(stream)

Other uses

Enum.map(["a", "b", "c"], &(&1))			# ["a", "b", "c"]
Enum.map(["a", "b", "c"], &(&1 <> "x" ))	# ["ax", "bx", "cx"]

Enum.map(["1", "2", "3"], &(String.to_integer(&1))) # [1, 2, 3]

stream = Stream.chunk_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1))
Enum.to_list(stream)	# [[1], [2, 2], [3], [4, 4, 6], [7, 7]]

Functions that return Streams

E.g.

IO.stream/2 - streams input lines, one by one
URI.query_decoder/1 - decodes a query string, pair by pair

Enums

Provides functions to iterate over Enumerables

Enum.at([1,2,3], 1)								# 2
Enum.each([1,2,3,4,5], fn(x) -> IO.puts x end)	# 1 2 3 4 5
Enum.empty?([])									# true
Enum.fetch([2, 4, 6], 0)						# {:ok, 2}
Enum.count([1,213,1,34,567,1,9], &(&1 == 1))    # 3

TODO

Macros

Used for meta-programming Expanded at compile time

Aliases

https://elixir-examples.github.io/examples/alias-use-import-require

alias, required, import - directives (lexically scoped)

Usage:

alias Foo.Bar, as: Bar	# Foo.Bar
Bar						# Foo.Bar

same as 

alias Foo.Bar	  		# Foo.Bar
require Foo

import Foo

use Foo					# extension point

Alias

Examples

String == is Elixir.String == :"Elixir.String"

defmodule Stats do
  alias Math.List, as: List  # Note: normal List needs to be accessed by Elixir.List
end

Shorthand:

alias Math.List 
alias Math.List, as: List

List # Math.List

Lexical Scope:

  • The alias can be used inside functions
defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end
end

Calling Erlang Modules

:lists.flatten([1, [2], 3])		# [1, 2, 3]

Require

If you need to use a macro, then need to require the module

e.g.

require Integer
Integer.is_odd(3)

Import

When: Accessing functions/macros from other modules without using full qualitifed name

E.g.

import List, only: [duplicate: 2]	# only is optional
duplicate :ok, 3					# [:ok, :ok, :ok]

import List, except: [first: 1]		# exclude first function/macro

import Integer, only: :macros
import Integer, only: :functions

Lexical Scope

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    duplicate(:ok, 10)
  end
end

Use

When: Bringing external functionality into current lexical scope e.g. modules

e.g.

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    assert true
  end
end

Behind the scenes:

defmodule Example do
  use Feature, option: :value
end

same as

defmodule Example do
  require Feature
  Feature.__using__(option: :value)
end

Multi alias/import/require/use

alias MyApp.{Foo, Bar, Baz}

Same as

alias MyApp.Foo
alias MyApp.Bar
alias MyApp.Baz

Files

Reading

{:ok, contents} = File.read("test.txt")
IO.puts(contents) # "hello\n"

Contents only (or error)

contents = File.read!("test.txt")	# "hello\n"

Don't know if success/failure, a way to handle this

def Account do
	def parse_file({:ok, content}) do
		IO.puts "#{content}"
	end
	
	def parse_file({:error, error}) do
		IO.puts "Error parsing file"
	end
end

File.read("transactions.csv") |> Account.parse_file()

Can also read files in chunks e.g. large files

File.open! file, fn(pid) ->
  IO.read(pid, 25) == "some text to compare to" #reads first 25 chars
end

Projects

mix new project-name

Kind of like rake

e.g.

> mix new expenses

* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/expenses.ex
* creating test
* creating test/test_helper.exs
* creating test/expenses_test.exs

Code Smells

Complicated, nested functions => use Pipe Operator

E.g.

defmodule Account do
	def balance(initial, spending) do
		interest(discount(initial, 10), 0.1)
	end
	def discount(total, amount) do
	end
	def interest(total, rate) do
	end
end

Pipe Operator

This helps with readability

Example:

defmodule Account do
	def balance(initial, spending) do
		discount(initial, 10) |> interest(0.1)
	end
...
end

The result of discount(initial, 10) is piped to the interest function i.e. interest(result, 0.1)

Note: each function call should go to a newline if the line is too long

Documentation

@doc """
documentation goes here
"""

Mix Tool

https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html

Check the version

mix --version

New project

mix new project-name

Directory:

├── README.md
├── config
│   └── config.exs
├── lib
│   └── budget.ex
├── mix.exs
└── test
    ├── budget_test.exs
    └── test_helper.exs

Compile the project

mix compile

Module

e.g. project called budget

Mix will create a module called budget under lib/budget.ex

defmodule Budget do
	# self defined functions
end

By default:

defmodule Budget do
  @moduledoc """
  Documentation for Budget.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Budget.hello
      :world

  """
  def hello do
    :world
  end
end

Run programs

mix run -e "code"

or

mix run -e "Module.function_name(params)"

^ Evaluate code in the context of the application (Budget is the module with a function function_name)

e.g.

mix run -e "Budget.current_balance(100, 20) |> IO.puts"

What happens:

  1. compile application
  2. load bytecode into Erlang VM
  3. detect -e option and evaluate the arg as code

Start an iex session

Start the session that loads the project

iex -S mix

Call a function on a module

> Budget.hello	# :world

File extensions

budget
|
|-- README.MD
|
|-- mix.exs
|
|-- config
|	  |
|	  |-- config.exs
|
|-- lib
|	  |
|	  |-- budget.ex
|
|-- test
	  |
	  |-- budget_test.exs
	  |
	  |-- test_helper.exs

.exs - scripts e.g. tests and config .ex - compiled code and production artifacts e.g. lib/

Submodules

Budget.Conversion

Directory structure:

budget
|
|-- README.MD
|
|-- mix.exs
|
|-- config
|	  |
|	  |-- config.exs
|
|-- lib
|	  |
|	  |-- budget.ex
|	  		|
|	 		|-- conversion.ex
|
|-- test

Other commands

mix help

Working with External Third Party Dependencies

Dependencies are declared in mix.exs file in the project

Definition

Definition in:

  • defp deps block
  • list of tuples

E.g.

defmodule Budget.Mixfile do
	defp deps do
		[{:httpoison, "~> 0.10.0"}, {:poison, "~> 3.0"}]
	end
end

Install deps

mix deps.get

Get installed to deps dir

budget
|
|-- README.MD
|
|-- mix.exs
|
|-- config
|
|-- lib
|
|-- deps
	  |
	  |-- httpoison
	  |
	  |-- exjsx

Also updates the mix.lock file

Creating Tasks

lib/mix/tasks/

e.g. lib/mix/tasks/list_transactions.ex

defmodule Mix.Tasks.ListTransactions do
  use Mix.Task

  @shortdoc "List transactions from CSV file"
  def run(_) do
    Budget.list_transactions |> IO.inspect
  end
end

Default Task

Add: default_task to the /mix.exs project file

defmodule Budget.Mixfile do
  def project do
    [
      app: :budget,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(), default_task: "list_transactions"
    ]
  end
  ...
end

List tasks

$mix compile
$mix

Compiling 2 files (.ex)
Generated budget app
"listing transaction"

To list everything

$mix help

mix                   # Runs the default task (current: "mix list_transactions")
mix app.start         # Starts all registered apps
mix app.tree          # Prints the application tree
mix archive           # Lists installed archives
mix archive.build     # Archives this project into a .ez file
mix archive.install   # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean             # Deletes generated application file
...

Run a test

mix test

Tests

defmodule StreamersTest do
  use ExUnit.Case, async: true

  doctest Streamers
  
  @index_file "test/fixtures/emberjs/9af0270acb795f9dcafb5c51b1907628.m3u8"

  test "finds index file in a directory" do
    assert Streamers.find_index("test/fixtures/emberjs") == @index_file
  end
end

Notes:

  • async: true - run tests in parallel
  • doctest Streamers - documentation
  • @index_file - attribute, like a constant

Distributed mode

iex --name [email protected]
iex --name [email protected]

iex([email protected])> Node.self
:"[email protected]"

iex([email protected])> Node.connect(:"[email protected]")
iex([email protected])> Node.list
[:"[email protected]"]

Resources

Official

Others

Pragmatic

List of libraries

CheatSheet

Blogs

Articles

Videos & Courses

Plural Sight

CodeSchool

Udemy

Umbrella Apps

Feature Flags

Debugging

Example Apps

Web Frameworks

Phoenix

Sugar

Dynamo (note: currently under mainteance mode)

Other Frameworks

Documentation

Consistency

Database

Ecto

Issues

Testing

ExUnit

Wallaby (concurrent browser testing)

Mocks

Legacy Migrations

Deployments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment