Elixir cheatsheets
1. How to organize code
- A module is a collection of functions, like namespace. Functions must be defined inside a module.
- A file can contain multiple module.
- Rules
- Module name starts with uppercase letter and is usually formated as CamelCase style.
- Use
.
in module to represent herachichy. We could also define child module inside module. - Function name are like abcdef.
- Module name starts with uppercase letter and is usually formated as CamelCase style.
2. How to call functions from other module
- We could import other module into current module, to allow us to call functions without module prefix.
We could use change the module’s name
alias IO, as: MyIO
For example, you have a Geometry.Rectangle module. You can alias it in your client module and use a shorter name
defmodule MyModule do alias Geometry.Rectangle, as: Rectange def my_fun do Rectange.area(...) end end
3. How to compile the code
- In Emacs,
M-x alchemist-iex-send-last-sexp
. - In terminal,
iex geometry.ex
.
The code will be compiled and resulting module is loaded into the runtime.
4. How to do type specification
defmodule Circle do @pi 3.14 @spec area(number) :: number def area(r) do r * r * @pi end end
5. How to get the integer from div
- The division operator
/
always return float. - To get integer use
div
to get the integer part.rem
to get the remainder
6. How to define constant
- Atom, like
:this_is_an_atom
7. How to use Tuples DataStructure
person = {"bob", 25} age = elem(persion, 1) # => 25 put_elem(person, 1, 100) # => {"bob", 100}
- Untyped structure, or record, group a fixed number of element together.
8. How to use List DataStructure
iex(11)>prime_numbers = [2, 3, 5, 7] [2, 3, 5, 7] iex(12)> length(prime_numbers) length(prime_numbers) 4 iex(13)> Enum.at(prime_numbers, 1) Enum.at(prime_numbers, 1) 3 iex(14)> 100 in prime_numbers 100 in prime_numbers false iex(15)> List.replace_at(prime_numbers, 0 , 11) List.replace_at(prime_numbers, 0 , 11) [11, 3, 5, 7] iex(16)> prime_numbers prime_numbers [2, 3, 5, 7] iex(17)> new_primes = List.replace_at(prime_numbers, 0 , 11) new_primes = List.replace_at(prime_numbers, 0 , 11) [11, 3, 5, 7] iex(18)> new_primes = List.insert_at(new_primes, 3, 13) new_primes = List.insert_at(new_primes, 3, 13) [11, 3, 5, 13, 7] iex(19)> new_primes new_primes [11, 3, 5, 13, 7] iex(20)> new_primes = List.insert_at(new_primes, -1, 103) new_primes = List.insert_at(new_primes, -1, 103) [11, 3, 5, 13, 7, 103] iex(21)> [1, 2, 3] ++ [4, 5] [1, 2, 3] ++ [4, 5] [1, 2, 3, 4, 5] iex(22)> hd([1, 2, 3, 4]) hd([1, 2, 3, 4]) 1 iex(23)> tl([1, 2, 3, 4]) tl([1, 2, 3, 4]) [2, 3, 4] iex(24)> a_list = [5, :value, true] a_list = [5, :value, true] [5, :value, true] iex(25)> new_list = [:new_element | a_list] new_list = [:new_element | a_list] [:new_element, 5, :value, true]
9. Tuples, keywork lists, map and struct DataStructure
Two key pieces missing from tuples and lists:
- Tuples are relatively annonymous structures. Relying on specific order and number of components in tuples can create maintainance headaches.
- Lists have similar problems: the usual appraoches to list processing assume that lists are just sequences of similar parts.
Sometimes we want to call things out by name instead of number, or pattern matching to a specific location.
9.1. Mixing lists and tuples
How to convert two lists into a single list of tuples or vice versa?
list1 = ["Hydrogen", "Helium", "Lithium"] list2 = ["H", "He", "Li"] list3 = [1, 2 ,3] element_list = Enum.zip(list1, list2) # [{"Hydrogen", "H"}, {"Helium", "He"}, {"Lithium", "Li"}] seperate_lists = Enum.unzip(element_list) # {["Hydrogen", "Helium", "Lithium"], ["H", "He", "Li"]}
9.2. Keyword lists
- It is used to process lists of tuples containing two elements that can be considered as “key and value” pair, where the key is an atom.
- Elixir display them in keywork list format.
9.3. Map
From lists to map: Keyword lists are a convenient way to address content stored in lists by key, but underneath, Elixir is still walking through the list. That might be OK if you have other plans for that list requiring walking through all of it, but it can be unnecessary overhead if you’re planning to use keys as your only approach to the data.
As key/value store
# Dynamically sized map empty_map = %{} # map with value squares = %{1 => 1, 2 => 4, 3 => 9} # or squares = Map.new([{1, 1}, {2, 4}, {3, 9}]) # fetch value for a given key squares[2] # where 1 is the key, => 4 squares[7] # => nil # or Map.get(squares, 2) # => 4 Map.get(squares, 8, :not_found) #=> :not_found, use this to specify default value. Map.fetch(squares, 5) #=> :error This will distinguishly produce # or raise exception Map.fetch!(squares, 5)
Power dynamically sized key/value structures, overlap Tuples’ feature but let you to access fields by name
bob = %{:name => "Bob", :age => 25, :works_at => "Initech"} # if keys are atom, we could short as bob = %{name: "Bob", age: 25, works_at: "Initech"} # fetch bob[:works_at] # => Initech bod[:non_exist_key] #=> nil # or use short syntax if key is atom bob.age #=> 25 # though it will get KeyError if the key is not exist next_bob = %{bob | age: 26} # => %{age: 26, name: "Bob", works_at: "Initech"} next_bob = %{bob | age: 26, works_at: "no_where"} # change multiple key/value
- Can only update keys exist in map!
- Use map to hold structured data is very common.
- Can only update keys exist in map!
9.4. Struct (tagged map)
From maps to structs: Structs are extensions built on top of maps that provide compile-time checks and default values.
defmodule Microsoft.Azure.Storage do @derive {Inspect, except: [:account_key]} defstruct [ :account_name, :account_key, :aad_token_provider, :cloud_environment_suffix, :is_development_factory ] ... end
- It is only possible to define a struct per module, as the struct it tied to the module itself
- Its fields:
- could be a keyword list
- or, a list of atoms as in this example: in this case, the atoms in the list will be used as the struct’s field names and they will all default to
nil
.
- could be a keyword list
- About
@derive
- Here, we define a custom strcut and make it could be inspected, except
:account_key
field.
10. Protocols
- What is a protocol
- It is a module in which you declare functions without implementing them.
- It is a module in which you declare functions without implementing them.
- Why we need protocol if we already could achieve polymorphism using patter matching?
(Remember: polymorphism means you want behavior to vary depending on the data type.)
Consider this example, we have a simple Utility module to tell use the types of input variable:
defmodule Utility do def type(value) when is_binary(value), do: "string" def type(value) when is_integer(value), do: "integer" # ... other implementations ... end
- This only works well if we implement this code and this code is not shared by multiple apps. Because there would be no easy way to extend its features.
- This only works well if we implement this code and this code is not shared by multiple apps. Because there would be no easy way to extend its features.
- Protocol can help us:
- The protocol implementation doesn’t need to be part of any module. It means: you can implement a protocol for a type even if you can’t modify the type’s source code.
- Dispatching on a protocol is available to any data type that has implemented the protocol and a protocol can be implemented by anyone, at any time.
So, rewrite those features as a protocol
defprotocol Utility do @spec type(t) :: String.t() def type(value) end # spread them over multiple files as needed defimpl Utility, for: BitString do def type(_value), do: "string" end defimpl Utility, for: Integer do def type(_value), do: "integer" end
- Functions defined in a protocol may have more than one input, but the dispatching will always be based on the data type of the first input.
- The protocol implementation doesn’t need to be part of any module. It means: you can implement a protocol for a type even if you can’t modify the type’s source code.
- The power of Elixir’s extensibility comes when protocols and structs are used together.
- Deriving
11. How to process binary
- A binary is a chunk of byte
Create binary by enclosing the byte sequence
<<1, 2, 3>>
- Each number represent the value of the corresponding byte.
If the value is bigger than 255, it is truncated to the byte size
<<257>> #=> <<1>>
- Each number represent the value of the corresponding byte.
Specify the size of each value and tell the compiler how many bits to use for that particular value
<<234::16>> # => <<0, 234>>, used 2 bytes, the first has value 0, the second is 234 <<1234::32>> # => <<0, 0, 4, 210>>
The size specifier is in bits and not needed to be a multiple of 8!!
<<1::4, 15::4>> # => <<31>>
If the total size of all values is not a multiple of 8, it is called a bitstring – a sequence of bits
<<1::1, 0::1, 1::1>> # => <<5::size(3)>>
Concatenate two binaries with
<>
<<1, 2>> <> <<3, 4>> # => <<1, 2, 3, 4>>
11.1. How to view a string’s binary representation
# A common trick in Elixir when you want to see the inner binary representation of a string is to concatenate the null byte <<0>> to it: iex> "hełło" <> <<0>> <<104, 101, 197, 130, 197, 130, 111, 0>> # Alternatively, you can view a string’s binary representation by using IO.inspect/2: iex> IO.inspect("hełło", binaries: :as_binaries) <<104, 101, 197, 130, 197, 130, 111>>
11.2. How to match on a binary of unknown size
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>> <<0, 1, 2, 3>> iex> x <<2, 3>>
- Matching on arbitrary length can only be done at end of the pattern and not anywhere else.
If you have the data which can be arbitrary bit length then you can add
bitstring
instead, so the pattern now looks like.
<<header :: size(8), data :: bitstring>>
11.3. How to match n bytes in a binary
iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>> <<0, 1, 2, 3>> iex> head <<0, 1>> iex> rest <<2, 3>>
11.4. How to pattern match on string with multibyte characters
iex> <<x::utf8, rest::binary>> = "über" "über" iex> x == ?ü true iex> rest "ber"
- Therefore, when pattern matching on strings, it is important to use the utf8 modifier.
11.5. Example: chunk from PNG
Chunk format
+--------------+----------------+-------------------+ | Length (32) | Chunk type (32)| Data (Length size)| +--------------+----------------+-------------------+ | CRC (32) | +--------------+
Pattern matching the chunk format
<<length :: size(32), chunk_type :: size(32), chunk_data :: binary - size(length), crc :: size(32), chunks :: binary>>
- Another way of defining n byte length is
binary - size(n)
. Note
: we matchedlength
in pattern and used in the pattern as well. In Elixir pattern matching you can use the assigned variable in the pattern following it, thats why we are able to extract thechunk_data
based on thelength
.
- Another way of defining n byte length is
12. How to represent string
- String in elixir is either a binary or a list type.
String inter – evaluate values in string template
"embedded expression: #{1 + 3}" #=>"embedded expression: 4"
How to include quote inside string
~s("embedded expression": #{1 + 3}) #=> "\"embedded expression\": 4" """ embedded expression: "#{1 + 3}" """ # => "embedded expression: \"4\"\n"
Aother way to represent string is use single-quote
'ABC' [65, 66, 67] # => they both produce 'ABC'
- The runtime doesn’t distinguish between a list of integers and a character list.
- The runtime doesn’t distinguish between a list of integers and a character list.
13. How to convert between binary string to character list
- binary string is represent using
""
while character list is represent as''
. - Use binary string as much as possbile
Convert
String.to_charlist("ABC")
14. How to define Lambda function and use it
basic lambda
square = fn x -> x * x end iex(2)> square.(24) square.(24) 576
- The dot operator is to make the code explicit such that you know an anonymous function is being called.
square(5)
will be a named function defined somewhere in the module.
- The dot operator is to make the code explicit such that you know an anonymous function is being called.
Capture makes us to make full function qualifier as lambda
Enum.each([1, 2, 3, 4], &IO.puts/1) iex(4)> Enum.each([1, 2, 3, 4], &IO.puts/1) 1 2 3 4 :ok
The closure capture doesn’t affect the previous defined lambda that references the same symbolic name
outside_var = 5 lambda = fn -> IO.puts(outside_var) end outside_var = 6 lambda.() #=> 5
15. How to use other types
- range
- keyword list
- A list of pair, where the first one is atom.
- Often used for small key-value structures.
- Often used as the last optional argument when define a function.
- A list of pair, where the first one is atom.
- MapSet, a set implementation
Time and date
date = ~D[2008-09-30] time = ~T[11:59:12] naive_datetime = ~N[2018-01-31 11:59:12.000007]
- IO lists
- Special for incrementally building output that will be forwarded to an I/O service.
Appending to an IO list is O(1), very useful to incrementally build a stream of bytes
iolist = [] iolist = [iolist, "This"] iolist = [iolist, "is"] iolist = [iolist, "Amazing"] iex(20)> iolist = [] iex(21)> [[], "This"] iex(22)> [[[], "This"], "is"] iex(23)> [[[[], "This"], "is"], "Amazing"] iex(24)> IO.puts(iolist) IO.puts(iolist) ThisisAmazing :ok
- Special for incrementally building output that will be forwarded to an I/O service.
16. How to use lib from mix
- In mix.exs, add the lib into
deps
. - In iex, run
recompile()
or disconnect from iex and re-run alchemist-iex-project-run: “C-c a i p”. - Test the example of lib in iex shell.
- If we want to shortcut the name, we could use
alias
to create short name.
17. How to check the and load additional code paths
load additional code path from command-line when started erlang runtime
$ iex -pa my/code/path -pa another/code/path # from command-line to load additional code path
once start runtime, check current loaded path
:code.get_path # check path
18. How to dynamically call a function
apply(IO, :puts, ["Dynamic function call."])
19. How to run a single script
Create
.exs
file
defmodule MyModule do def run do IO.puts("Called Mymodule.run") end end # Code outside of a module is executed immediately MyModule.run
On terminal
elixir script.exs
- With
--no-halt
, it will make the BEAM instance keep running. Useful when your script start other concurrent tasks.
- With
20. How to get current time
iex(28)> {_, time} = :calendar.local_time() {{2022, 2, 11}, {13, 32, 10}} iex(29)> time time {13, 32, 10}
21. How to handle exception error in guard
- If an error is raised from inside the guard, it won’t be propagated. And the guard expression will return false. The corresponding clause won’t match.
22. How to match the content of variable
iex(30)> expected_name = "bob" expected_name = "bob" "bob" iex(31)> {^expected_name, age} = {"bob", 25} {^expected_name, age} = {"bob", 25} {"bob", 25} iex(32)> age age 25
23. How to check the type of a variable
From REPL
iex(10)> i x i x Term 1 Data type Integer Reference modules Integer Implemented protocols IEx.Info, Inspect, List.Chars, String.Chars
From code
defmodule Util do def typeof(a) do cond do is_float(a) -> "float" is_number(a) -> "number" is_atom(a) -> "atom" is_boolean(a) -> "boolean" is_binary(a) -> "binary" is_function(a) -> "function" is_list(a) -> "list" is_tuple(a) -> "tuple" true -> "idunno" end end end cases = [ 1.337, 1337, :'1337', true, <<1, 3, 3, 7>>, (fn(x) -> x end), {1, 3, 3, 7} ] Enum.each cases, fn(case) -> IO.puts (inspect case) <> " is a " <> (Util.typeof case) end
24. How to chain multiple pattern matching
defmodule ChainPattern do # define some helper function def extract_login(%{"login" => login}) do {:ok, login} end def extract_login(_) do {:error, "login missed"} end def extract_email(%{"email" => email}) do {:ok, email} end def extract_email(_) do {:error, "email missed"} end def extract_password(%{"password" => password}) do {:ok, password} end def extract_password(_) do {:error, "password missed"} end def extract_info(submitted) do with {:ok, login} <-extract_login(submitted), {:ok, email} <-extract_email(submitted), {:ok, password} <-extract_password(submitted) do {:ok, %{login: login, email: email, password: password}} end end end submitted = %{ "login" => "alice", "email" => "some_email", "password" => "password", "other_field" => "some_value", "yet_another_not_wanted_field" => "..." } # iex(20)> ChainPattern.extract_info(submitted) # ChainPattern.extract_info(submitted) # {:ok, %{email: "some_email", login: "alice", password: "password"}}
25. How to build abstraction
- Princple
- Module is used as the abstraction over the data type.
- Modifier functions should return data of the same type.
- Query functions expect an instance of the data abstraction as the first argument and return another type of information.
- Module is used as the abstraction over the data type.
26. How to update hierachical data
- In general
- We can’t directly modify part of it that resides deep in its tree.
- We have to walk down the tree to particular part that needs to be modified, and then transform it and all of its ancestors.
- The result is a copy of the entire model.
- We can’t directly modify part of it that resides deep in its tree.
- Useful macros from Kernel:
put_in/2
put_in/3
get_in/2
update_in/2
get_and_update_in/2
- Those macros rely on the Access module. So, if we want our custom data to work with Access, we need to implement a couple of function required by Access contract. See: Access behaviour
27. How to register a process
- If you know there will always be only one instance of some type of server, you can give the process a local name and use that name to send messages to the process. The name is called local because it has meaning only in the currently running BEAM instance.
- Using the registered server is much simpler becaue we don’t need to pass server pid around through interface.
Example
Process.register(self(), :some_name) send(:some_name, :msg) receive do msg -> IO.puts("received #{msg}") end
28. How to handle unlimited process mailbox problem
- If a message is not match, it will be stored in mailbox with unlimited number. If we don’t process them, they will slow down the system and even crash the system when all memory is consumed.
- For each server process, we should introduce a match-all receive clause that deals with unexpected kind of messages.
29. How to implement a general server process
- In general, there are 5 things to do
- spawn a seperate process
- loop to infinite in that process
- receive message
- send message back to the caller
- maintain state
- spawn a seperate process
30. How to debug
Check the representation of a struct
Fraction.new(1,4) |> IO.inspect() |> Fraction.add(Fraction.new(1,4)) |> IO.inspect() |> Fraction.value() # %Fraction{a: 1, b: 4} # iex(70)> %Fraction{a: 1, b: 4} # %Fraction{a: 1, b: 4} # iex(71)> %Fraction{a: 8, b: 16} # iex(72)> %Fraction{a: 8, b: 16} # %Fraction{a: 8, b: 16} # iex(73)> 0.5
31. How to get the number of currently running process
:erlang.system_info(:process_count)
32. How state is maintained in server process
- In plain server process implementation
- State is passed as argument in loop clause. State is modified (new state) as the result of callback module’s message handling.
- This means the callback module’s
handle_call/2
andhandle_cast/2
need to pass state as argument
- State is passed as argument in loop clause. State is modified (new state) as the result of callback module’s message handling.
- In GenServer
- state is passed in from callback module’s interface as argument
- state is passed in in
handle_cast/2
as argument
- state is passed in from callback module’s interface as argument
33. How to create a singleton of a module
Implement
GenServer
in your module
def start do # locally register the process, make sure only one instance of the database process. GenServer.start(__MODULE__, nil, name: __MODULE__) end
34. How to use elixir to request access token
defmodule Script do @secret "84G7Q~JiELHPu3XuNKqckEB1eavVnMpHmnoZh" @client_id "2470ca86-3843-4aa2-95b8-97d3a912ff69" @tenant "72f988bf-86f1-41af-91ab-2d7cd011db47" @scope "https://microsoft.onmicrosoft.com/3b4ae08b-9919-4749-bb5b-7ed4ef15964d/.default" @moduledoc """ A HTTP client for doing RESTful action for DeploymentService. """ def request_access_token() do url = "https://login.microsoftonline.com/#{@tenant}/oauth2/v2.0/token" case HTTPoison.post(url, urlencoded_body(), header()) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body |> Poison.decode |> fetch_access_token # |> IO.puts {:ok, %HTTPoison.Response{status_code: 404}} -> IO.puts "Not found :(" {:error, %HTTPoison.Error{reason: reason}} -> IO.inspect reason end end def trigger_workflow(token) do definition_name = "AuroraK8sDynamicCsi" url = "https://xscndeploymentservice.westus2.cloudapp.azure.com/api/Workflow?definitionName=#{definition_name}" HTTPoison.post( url, json_body(), [ {"Content-type", "application/json"}, {"Authorization", "Bearer #{token}"}, {"accept", "text/plain"}]) end def test() do request_access_token() |> trigger_workflow end def fetch_access_token({:ok, %{"access_token" => access_token}}) do access_token end def header() do [{"Content-type", "application/x-www-form-urlencoded"}] end def urlencoded_body() do %{"client_id" => @client_id, "client_secret" => @secret, "scope" => @scope, "grant_type" => "client_credentials"} |> URI.encode_query end def json_body() do %{ SubscriptionId: "33922553-c28a-4d50-ac93-a5c682692168", DeploymentLocation: "East US 2 EUAP", Counter: "1", AzureDiskStorageClassAsk: "Random", AzureDiskPvcSize: "13" } |> Poison.encode! end end
35. How to do OAuth
36. How to check a module’s available functions
- <ModuleName>._info_(:functions)
Example
KeyValueStore.__info__(:functions) [get: 2, handle_call: 2, handle_cast: 2, init: 0, put: 3, start: 0]
37. How to represent a grid
- ref: Multidimensional Arrays in Elixir
- ref: https://elixirforum.com/t/how-to-make-proper-two-dimensional-data-structures-in-elixir/872/16
- My solution01
- Represent grid as a map
- key is the {x, y} coordinate
- value is the stored information
- key is the {x, y} coordinate
Init grid from list of list numbers, here each {x, y} stores {value, visited?}
defmodule BingoGrid do # each grid_inputs is a row of the grid, so grid_inputs is a list of list number def init(grid_inputs) do # how to represent the grid grid = grid_inputs |> Enum.with_index |> Enum.reduce(%{}, fn x, acc -> add_row_elements_to_map(x, acc) end) {:ok, grid} end defp add_row_elements_to_map({row_nums, r}, grid) do row_nums |> Enum.with_index |> Enum.reduce(grid, fn {num, c}, acc -> Map.put_new(acc, {r, c}, {num, false}) end) end end BingoGrid.init([[1,2,3], [4,5,6]])
- Notice, we need to use Enum.reduce to change some value. Assign variable inside Enum.each will not work.
{:ok, %{ {0, 0} => {1, false}, {0, 1} => {2, false}, {0, 2} => {3, false}, {1, 0} => {4, false}, {1, 1} => {5, false}, {1, 2} => {6, false} }}
- Notice, we need to use Enum.reduce to change some value. Assign variable inside Enum.each will not work.
- Represent grid as a map
38. How to produce permutation and combination from list
- ref: Comb
39. Difference between alias, use, require and import in Elixir
alias
is used to give shortcut names for a model.import
: Aliases are great for shortening module names but what if we use functions from given module extensively and want to skip using module name part?
import
imports all public functions and macros from given module.require
is like import + alias while different from eitherimport
oralias
.
- It is used like
alias
, but different from it that it will compile module first. - So, if a module contains a macro, and we want to use as SomeModule.SomeMacro,
require
will work but notalias
.
- It is used like
use
allow us to inject any code in the current module.
40. Elixir with Phoenix notes
40.1. 02-24
create a project and start
mix phx.new hello cd hello/ cd assets/ npm install cd .. mix ecto.create # create db configuration mix phx.server # or iex -S mix phx.server
- visit http://localhost:4000
- visit http://localhost:4000
- A new feature: print a string when you load a specific URL
All actions is done inlib/hello_web
- Map requests coming in to a specific URL
- Edit router, specify the controller, and a action name.
- Edit router, specify the controller, and a action name.
- tie a URL to a function on a controller
- Define a function in controller
- The name of the function should match the action name specified on router
- Define a function in controller
- Tie that function to a view
- But how how bind controller with view?
- We defined a module
HelloWeb.HelloView
- That file doesn閳ユ獩 actually do any work beyond tying the view for world with some code to render a template. We閳ユ獟l rely on the defaults to render a template.
- We defined a module
- Inside the action function from controller, we specify the render with a template
- But how how bind controller with view?
- About request parameters
- Defined in router
- Extract out in controller with patter matching
- Notice the convention from string to atom
- Notice the convention from string to atom
- Use it template(<actionname>.html.eex) with “@<parametername>”.
- Defined in router
- Map requests coming in to a specific URL
About pattern matching, string and atom
# from top to bottom, be shorthanded [{:name, name}] [:name => name] [name: name] name: name # if it is used as the last argument of a function
- Problems about starts project as
iex -S mix phx.server
from Emacs
- Make sure the Emacs is running as Administrator.
- If it has postgres issue, go to the project root, and re-run
mix ecto.create
. - When start Phoenix project from Emacs using alchemist, the default command is
iex -S mix
, we need to edit toiex -S mix phx.server
- Make sure the Emacs is running as Administrator.
How to check Phoenix version
cd into mix project root folder, run
mix phx.new --version
40.2. 02-27
- A view in Phoenix is just a module, and templates are just functions.
- How to persistent out model data with database?
- Modify models from lib: for example,
lib/rumbl/accounts/user.ex
.
- This is for schema definition.
- This is for schema definition.
run
ecto.gen.migration
mix ecto.gen.migration create_users
- This will create migration
<timesteamp>_create_users.exs
file in pathpriv/migrations/
. - Edit the generated
.exs
file to define. This is for use code to operator database to create corresponding tables. - In general, we write elixir code to create/update table schema.
- This will create migration
- run
mix ecto.migrate
- In this step, the actual table is created.
- In this step, the actual table is created.
- Modify models from lib: for example,
In general, 3 steps
- In lib modify our model
- Define schema using
Ecto.Schema
- Define corresponding changeset.
- Define schema using
- Generate migration file, in which we define database operations.
- Execute migration, by
mix ecto.migrate
.
40.3. 02-28
- After
- Use
mix phx.routes
to check all available routes in our web application.
40.4. 03-01
After migration (create user table, we could test it from iex shell)
alias Rumbl.Repo alias Rumbl.Accounts.User Repo.insert(%User{name: "Jose", username: "josevalim"}) Repo.insert(%User{name: "Bruce", username: "redrapids"}) Repo.insert(%User{name: "Chris", username: "mccord"})
- Check
Phoenix.HTML.FormData
contract to understand how the changes in the changeset available to the form. - How to refer to specific routes in the application
- Use
YourApplication.Router.Helpers
. In fact, phx help use withalias RumblWeb.Router.Helpers, as: Routes
. - So, we can get any route through
Routes.some_path
.
- Use
- Install application as dependencies
- edit
mix.exs
mix deps.get
- edit
- Check point01
- Install password feature dependencies
- In
user.ex
- define schema for password and passwordhash
- create our
registration_changeset
- define schema for password and passwordhash
Test in iex shell
alias Rumbl.Accounts.User alias Rumbl.RumblWeb.Router.Helpers, as: Routes # this one is not valid changeset = User.registration_changeset(%User{}, %{username: "max", name: "Max", password: "123"}) # this one is valid changeset = User.registration_changeset(%User{}, %{username: "max", name: "Max", password: "asecret"})
- Install password feature dependencies
- Check point02
To fix already existing user to make them have valid password
recompile() alias Rumbl.Repo for u <- Repo.all(User) do Repo.update!(User.registration_changeset(u, %{password: "gghh3344"})) end
- Check point03
- At this point, all users shall meet the requirement: new user registration need to have passworld.
- In Account module, use User module’s registration changeset. (model)
- In usercontroller, use exposed function from Account module.
- Modify new user html to provide slot for pasword. (view)
- At this point, all users shall meet the requirement: new user registration need to have passworld.
- Check point04
Check if there is a new user in the session and store it inconn.assigns
for every incoming request. In other words, we need to prevent user to access certain action when there is no session record.
- loading data from session
- use it to restric user access
- loading data from session
- Check point05
Add a mechanism to log the users in.
- create login function in auth.ex
- assigns current user
- put session and configurationsession to reuse.
- assigns current user
- use login function in user controller
- create login function in auth.ex
Problem01
lib/rumbl/accounts.ex:48: Users.__struct__/1 is undefined, cannot expand struct Users. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code
- When I add a new user, it shows this.
- Following error message, I could solve this: there is a typo in accounts.ex which I use
%Users{}
instead of should use%User{}
.
- When I add a new user, it shows this.
40.5. 03-02
- Check Point 01
- Expose a function to validate username and password.
- Use RESTful session API for
- GET for new session login form
- POST for login
- DELETE for logout
- GET for new session login form
- Expose a function to validate username and password.
- Check Point 02
- Change the layout of the application to handle the new user features
- Change the layout of the application to handle the new user features
- Problem01
When I go home to execute steps from check point02. I got error:
iex(3)> for u <- Repo.all(User) do Repo.update!(User.registration_changeset(u, %{password: "tmppass"})) end ...(3)> ...(3)> ** (Protocol.UndefinedError) protocol Ecto.Queryable not implemented for User of type Atom, the given module does not exist. This protocol is implemented for the following type(s): Atom, BitString, Ecto.Query, Ecto.SubQuery, Tuple (ecto 3.7.1) lib/ecto/queryable.ex:40: Ecto.Queryable.Atom.to_query/1 (ecto 3.7.1) lib/ecto/repo/queryable.ex:16: Ecto.Repo.Queryable.all/3
- Here, I forgot to refer User as
alias Rumbl.Accounts.User
. After executealias Rumbl.Accounts.User
. The problem solved.
- Here, I forgot to refer User as
- What is the differences between pipeline and plugs?
40.6. 03-04
- What is user registration
- Apply changeset to Repo user.
- User has username and password
- Apply changeset to Repo user.
- What happended when user do a registration
- We create a
new(conn, _params_)
in our controller to handle the get request to our url/users/new
.
- In which we use
Accounts.change_registration
which is an wrapper foruser.registration_changeset
.
- In which the user’s params like username, passoword are validate by changeset and applyied with
put_change
.
- In which the user’s params like username, passoword are validate by changeset and applyied with
- In which we use
- We create a
- What is the differences between
new
andcreate
from usercontroller.ex
new
is used in controller to handle request to/users/new
, it is used for rendering the form.create
is used in formRoutes.user_path(@conn, :create)
, it is used for submiting the form.
- What is login for a user
- A user is login when the session contains the user’s username.
- A user is login when the session contains the user’s username.
- How to implement authentication feature (login and logout)
- We implement authentication as a plug. So, we can add it to a plug pipeline for our router.
- There are two kinds of plugs, one is function plugs and another is module plugs. When to prefer module plug over function plug?
- When we want to share a plug across more than one module.
- When we want to share a plug across more than one module.
In module plug, there are two methods matters:
- init
- call
- its second argument is the result of
init
. - its first argument is the
conn
which isPlug.Conn
struct.
- its second argument is the result of
So, we need to import
import Plug.Conn
.
- init
- The plug for authentication implementation:
- Store the user ID in the session every time a user registers or a user login.
- Check if there is a new user in the session and store it in the conn.assign for every incoming request.
- Do this in our plug
call
.
- Do this in our plug
- Store the user ID in the session every time a user registers or a user login.
- We implement authentication as a plug. So, we can add it to a plug pipeline for our router.
40.7. 03-05
- What is a context?
- A context in Phoenix is just a module that groups functions with a shared purpose.
- A context encapsulates all business logic for a common purpose.
- This way, we can interact with our business logic from controllers, channels, or remote APIs, without having to duplicate code.
- In other words, a controller exists to work with context functions.
- A controller parses end user requests, calls context functions, and translates those results into something the end user can understand. In other words, the controller’s job is to translate whatever our business logic returns into something meaningfull for the user.
- The context doesn’t know about the controller, and the controller doesn’t know about the business rules.
- When build a context, think about the way of how the context is available to the controller.
- A context in Phoenix is just a module that groups functions with a shared purpose.
- How to make a function plug available across controller and views?
In
rumbl_web.ex
import the plug function in both controller and router
def router do quote do use Phoenix.Router ... import RumblWeb.Auth, only: [authenticate_user: 2] end end def controller do quote do use Phoenix.Controller, namespace: RumblWeb ... import RumblWeb.Auth, only: [authenticate_user: 2] end end
- Where the
2
is the number of arguments expected by theauthenticate_user
.
- Where the
- What is the relationship between view and templates
- A view pick all its corresponding templates and transform them into functions.
- If a view is
rumbl_web/view/video_view.ex
, then the templates are located atrumbl_web/templates/video/
.
- A view pick all its corresponding templates and transform them into functions.
How to find a video’s associated user without creating the bundling data:
query = Ecto.assoc(video, :user) Repo.one(query)
- Notice, here we avoid including a complete user info into video to find out this answer.
- Notice, here we avoid including a complete user info into video to find out this answer.
- When building relationship between module, we generally to avoid having cyclic dependencies. That is, prefer the one-way relationship. Here, the Video schema depends on User.
- How to use query to restrict CRUD operation of Video are limited to current user?
- Problems
`Rumbl.Multimedia.Video` that was not loaded when try to associate a video with a user
{:ok, video} = Rumbl.Multimedia.create_video(%{title: "new video", url: "http://example.com", alias Ecto.Changeset alias Rumbl.Repo user = Rumbl.Accounts.get_user_by(username: "zhaowei") changeset = video |> Changeset.change() |> Changeset.put_assoc(:user, user)
Error message
** (RuntimeError) attempting to cast or change association `user` from `Rumbl.Multimedia.Video` that was not loaded. Please preload your associations before manipulating them through changesets
- The error message says the Video.user is not loaded.
- The error message says the Video.user is not loaded.
Solution, we need to preload it:
video = Rumbl.Repo.preload(video, :user)
- The
preload
accepts one name or a collection of association names. After Ecto tries to fetch the association, we can reference the video.user. It is great for boundling data (we include a complete user info into the video). - Now, we could do the associate now.
- The
At last, don’t forget to make changeset take effect
video = Repo.update!(changeset) # check user binded to that video video.user
The above shows the steps to create an association between video and user. We could also do the following without the
put_assoc
.
video = video |> Changeset.change() |> Changeset.put_change(:user_id, user.id) |> Repo.update!() video = Repo.preload(video, :user)
40.8. 03-06
What is context generator
Currently, we have met the following kind of generators
- mix.ecto.gen.migration, generate only migration files
- mix phx.gen.html, generate migrations, schemas, context, as well as controllers, views, and templates.
- mix phx.gen.context,useful for generating a resource with all of its context function.
- mix phx.gen.schema, useful for creating a resource when we want to define the context functions by ourselves.
For more information, type
mix help GENERATOR_NAME
in the terminal.
- mix.ecto.gen.migration, generate only migration files
How to add category into existing video?
We choose to use
mix phx.gen.schema
to generate schema.
mix phx.gen.schema Multimedia.Category categories name:string
- We choose this because we probably don’t need most of the generated context function.
- It produces two related files
category.ex
xxx_create_categories.exs
. This file contains the migration which will create tables in db.
- We choose this because we probably don’t need most of the generated context function.
- Edit the generated migration file to fit our need.
- Edit the “name” field as NOT NULL and create a unique index for it.
- At this stage, we also edit the corresponding video schema from Video.ex to create a belongs-to relationship.
- Edit the “name” field as NOT NULL and create a unique index for it.
Use
mix ecto.gen.migration
to generate a migration to add the categoryid to our video table.
mix ecto.gen.migration add_category_id_to_video
- This command generate a migration with empty content left for us to fill.
Define the database contraint between videos and categories.
defmodule Rumbl.Repo.Migrations.AddCategoryIdToVideo do use Ecto.Migration def change do alter table(:videos) do add :category_id, references(:categories) end end end
- This command generate a migration with empty content left for us to fill.
Finally, migrate our database with our new migrations.
mix ecto.migrate
In general, we defines two migrations, one is to create categories table, another is to add the constrains on the existing video table.
- How to regret a just did migration?
- We could use
mix ecto.rollback
to migration down. - For example, we just did some migration. But we found we need to add an extra field for our just created table.
- We could use
mix ecto.rollback
to revert the migration. Edit the change, then domix ecto.migrate
to apply the changes.
- We could use
- Seeding and associating categories
- How to use script to populate our data while maintain database constrains
- How to associate videos and categories
- Fetch all categories name and IDs from db.
- Sort them by name
- Pass them into view as “select” input.
- Fetch all categories name and IDs from db.
- How to use script to populate our data while maintain database constrains
- Problem: my categories currently don’t have unique value even when I already specify the unique contraint from schema.
The way I created table categories using migration
defmodule Rumbl.Repo.Migrations.RecreateCategory do use Ecto.Migration def change do create table(:categories) do add :name, :string, null: false timestamps() end create unique_index(:categories, [:name]) end end
- The way how I add Category using changeset
In multimedia.ex
alias Rumbl.Multimedia.Category def create_category!(name) do %Category{} |> Category.changeset(%{name: name}) Repo.insert!(%Category{name: name}, on_conflict: :nothing) end
In category.ex
defmodule Rumbl.Multimedia.Category do use Ecto.Schema import Ecto.Changeset import Ecto.Query schema "categories" do field :name, :string timestamps() end def changeset(category, attrs) do category |> cast(attrs, [:name]) |> validate_required([:name]) |> unique_constraint(:name) end end
40.9. 03-07
- How to delete all created categories
- Currently, there are multiple duplicated values. I plan to delete all of them and create some values.
Delete existing categories
alias Rumbl.Repo alias Rumbl.Multimedia.Category import Ecto.Query, only: [from: 2] query = Category Rumbl.Repo.delete_all(Category)
Populate category
alias Rumbl.Multimedia for category <- ~w(Action Drama Romance Comedy Sci-fi) do Multimedia.create_category!(category) end
Why my changeset doesn’t impose constraint?
import Ecto.Changeset alias Rumbl.Repo alias Rumbl.Multimedia.Video alias Rumbl.Multimedia.Category def create_category!(name) do %Category{} |> create_category_changeset(%{name: name}) Repo.insert!(%Category{name: name}, on_conflict: :nothing) end def create_category_changeset(category, attrs) do category |> cast(attrs, [:name]) |> validate_required([:name]) |> unique_constraint(:name) end
Test it with
alias Rumbl.Multimedia Multimedia.create_category!("Action") alias Rumbl.Multimedia.Category Multimedia.create_category_changeset(%Category{}, %{name: "Action"})
- It should shows false, but the valid is true…
- It should shows false, but the valid is true…
- Currently, there are multiple duplicated values. I plan to delete all of them and create some values.
- How to delete the category table and create it again?
- ref: How to delete/drop table?
- Steps
Generate a migration
mix ecto.gen.migration drop_category
- This command will contain a
change
function. By adding our custom logic into thatchange
function, we could apply custom changes.
- This command will contain a
Add change function to the migration
defmodule Rumbl.Repo.Migrations.DropCategory do use Ecto.Migration def change do drop table("categories"), mode: :cascade end end
Apply those changes
mix ecto.migrate
- ref: How to delete/drop table?
40.10. 03-10
- After we define our model using schema, how migration understand that model?
- It doesn’t. We have to define the migration content by ourselves.
- For example:
In our account/user.ex, we defined the following models
defmodule Rumbl.Accounts.User do use Ecto.Schema import Ecto.Changeset schema "users" do field(:name, :string) field(:username, :string) timestamps() end end
- mix ecto.gen.migration createusers
- This will generate migration file named like: creating priv/repo/migrations/20180315023132createusers.exs
- This will generate migration file named like: creating priv/repo/migrations/20180315023132createusers.exs
- In that createusers.exs, the change is empty. We have to define how to operation the database.
So, we have to define how to create table, create attributes, and create indexes. - At last, we use
mix ecto.migrate
to migrate up the database.
- It doesn’t. We have to define the migration content by ourselves.
40.11. 03-12
- How the test cases could know our helper functions?
- We define our helper functions in Rumbl.TestHelpers module.
- We then import them globally in Rumbl.DataCase module in
test/support/data_case.ex
.
- We define our helper functions in Rumbl.TestHelpers module.
40.12. 03-18
- How to drop a entire dev database and recreate all associated tables?
- mix ecto.drop rumbldev –force-drop
- mix ecto.create
- mix ecto.migrations (Use this command to check the current migrations available.)
- mix ecto.migrate
- mix ecto.drop rumbldev –force-drop
40.13. 03-19
- How the js code in
assets/js
folder are available to the pages(template) - See, chapter10
40.14. 03-21
How to quickly generate model (including schema and database change migration)
mix phx.gen.schema Multimedia.Annotation annotations \ body:text at:integer \ user_id:references:users \ video_id:references:videos
- This command is create model annotation which including 4 fields
- body
- at
- userid
- videoid
- body
- The result of running this command are two files one is migrationchange. Another is annotation.ex file which changeset and schema.
- Notice, the schema here doesn’t wire annotation with user or video.
- To bind relationship between annotation with user and video. We need to do it with manually with careful decision.
- Don’t forgot to modify the corresponding video or user’s schema to make space for annotation.
- This command is create model annotation which including 4 fields
41. Some notes
- Always keep in mind that a Boolean is just an atom that has a value of true or false.
- short-circuit operators:
||
,&&
,!
.
||
returns the first expression that isn’t falsy.
Use for like
read_cache || read_from_disk || read_from_database