UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

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.

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

  1. 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.
  2. 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.

  1. 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)
    
  2. 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.

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.
  • About @derive
  • Here, we define a custom strcut and make it could be inspected, except :account_key field.

10. Protocols

  1. What is a protocol
    • It is a module in which you declare functions without implementing them.
  2. 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.)
    1. 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.
    2. 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.
  3. The power of Elixir’s extensibility comes when protocols and structs are used together.
  4. 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>>
      
  • 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 matched length 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 the chunk_data based on the length.

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.

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.
  • 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

  1. range
  2. 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.
  3. MapSet, a set implementation
  4. Time and date

    date = ~D[2008-09-30]
    time = ~T[11:59:12]
    naive_datetime = ~N[2018-01-31 11:59:12.000007]
    
  5. 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
      

16. How to use lib from mix

  1. In mix.exs, add the lib into deps.
  2. In iex, run recompile() or disconnect from iex and re-run alchemist-iex-project-run: “C-c a i p”.
  3. Test the example of lib in iex shell.
  4. 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.

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.

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.
  • 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

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 and handle_cast/2 need to pass state as argument
  • In GenServer
    • state is passed in from callback module’s interface as argument
    • state is passed in in handle_cast/2 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
    • 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}
       }}
      

38. How to produce permutation and combination from list

39. Difference between alias, use, require and import in Elixir

  1. alias is used to give shortcut names for a model.
  2. 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.
  3. require is like import + alias while different from either import or alias.
    • 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 not alias.
  4. 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
    
  • A new feature: print a string when you load a specific URL
    All actions is done in lib/hello_web
    1. Map requests coming in to a specific URL
      • Edit router, specify the controller, and a action name.
    2. 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
    3. 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.
      • Inside the action function from controller, we specify the render with a template
    4. About request parameters
      • Defined in router
      • Extract out in controller with patter matching
        • Notice the convention from string to atom
      • Use it template(<actionname>.html.eex) with “@<parametername>”.
  • 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 to iex -S mix phx.server
  • 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?
    1. Modify models from lib: for example, lib/rumbl/accounts/user.ex.
      • This is for schema definition.
    2. run ecto.gen.migration

      mix ecto.gen.migration create_users
      
      • This will create migration <timesteamp>_create_users.exs file in path priv/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.
    3. run mix ecto.migrate
      • In this step, the actual table is created.

In general, 3 steps

  • In lib modify our model
    • Define schema using Ecto.Schema
    • Define corresponding changeset.
  • 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 with alias RumblWeb.Router.Helpers, as: Routes.
    • So, we can get any route through Routes.some_path.
  • Install application as dependencies
    • edit mix.exs
    • mix deps.get
  • Check point01
    1. Install password feature dependencies
    2. In user.ex
      • define schema for password and passwordhash
      • create our registration_changeset
    3. 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"})
      
  • 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
    1. At this point, all users shall meet the requirement: new user registration need to have passworld.
    2. In Account module, use User module’s registration changeset. (model)
    3. In usercontroller, use exposed function from Account module.
    4. Modify new user html to provide slot for pasword. (view)
  • Check point04
    Check if there is a new user in the session and store it in conn.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
  • 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.
    • use login function in user controller
  • 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{}.

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
  • Check Point 02
    • 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 execute alias Rumbl.Accounts.User. The problem solved.
  • What is the differences between pipeline and plugs?

40.6. 03-04

  1. What is user registration
    • Apply changeset to Repo user.
    • User has username and password
  2. 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 for user.registration_changeset.
        • In which the user’s params like username, passoword are validate by changeset and applyied with put_change.
  3. What is the differences between new and create 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 form Routes.user_path(@conn, :create), it is used for submiting the form.
  4. What is login for a user
    • A user is login when the session contains the user’s username.
  5. 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.
    • 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 is Plug.Conn struct.

      So, we need to import import Plug.Conn.

    • 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.

40.7. 03-05

  1. 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.
  2. 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 the authenticate_user.
  3. 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 at rumbl_web/templates/video/.
  4. 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.
  5. 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.
  6. How to use query to restrict CRUD operation of Video are limited to current user?
  7. 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.
      • 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.
      • 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

  1. What is context generator
    Currently, we have met the following kind of generators

    1. mix.ecto.gen.migration, generate only migration files
    2. mix phx.gen.html, generate migrations, schemas, context, as well as controllers, views, and templates.
    3. mix phx.gen.context,useful for generating a resource with all of its context function.
    4. 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.

  2. How to add category into existing video?

    1. 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.
    2. 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.
    3. 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
        
    4. 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.

  3. 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 do mix ecto.migrate to apply the changes.
  4. 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.
  5. 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…
  • How to delete the category table and create it again?
    • ref: How to delete/drop table?
    • Steps
      1. Generate a migration

        mix ecto.gen.migration drop_category
        
        • This command will contain a change function. By adding our custom logic into that change function, we could apply custom changes.
      2. 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
        
      3. Apply those changes

        mix ecto.migrate
        

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
      • 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.

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.

40.12. 03-18

  • How to drop a entire dev database and recreate all associated tables?
    1. mix ecto.drop rumbldev –force-drop
    2. mix ecto.create
    3. mix ecto.migrations (Use this command to check the current migrations available.)
    4. mix ecto.migrate

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
    • 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.

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