- To generate a new project
mix new <project-name> mix helpwill list all the tasks- For generated projects:
- code goest into
lib mix.exsis similar to package.json
- code goest into
- Files with
.exextension are meant to be compiled - Files with
.exsextension can be ran as scripts - Elixir uses modules, one module per file
- Convention is to put modules in a directory under lib (
lib/app_name/) - Then de module will be call
defmodule APPNAME.MODULENAME - An :atom is a name whose value is the same as the key (think constants in redux actions)
To run an elixir file you can do:
- elixir <file-path>
The file will be compiled in memory and run in the Erlang Virtual Machine
- Modules can have attributtes
- Module attributes are defined at the top level
- You can use attributes to store metadata about the module
- i.e.
@moduledocto document the module documentation @docdocuments the function underneath- We can use module attributes as constants
- It's best to declare the attributes first thing in the file, since they are bound at compile time
- A single elixir file can define multiple modules
- All modules are defined at top level, using dots in the name is just a convenient way to namespace
App.Module - To call other module functions in a module you can:
- Call them including the module name
App.Module.function - Import the module
import App.Moduleandfunctionwill be available (it imports the entire module) - Import only the functions we need by using only:
import App.Module, only: [function]
- Call them including the module name
- Elixir atoms are prefixed by a colon. I.E.
%{ :method => "GET", :path => "/wildthings" } - However, it's so common to use atoms as keys that Elixir gives us a shortcut.
%{ method: "GET", path: "/wildthings" } - If the keys are anything but atoms, you must use the general => form. For example, here's a map with strings as keys:
%{ "method" => "GET", "path" => "/wildthings" } - If the keys are atoms, so to get the values associated with the keys we can use the square-bracket syntax:
conv[:method]or the dot notationconv.method - If a key does not exist and we use the square brackets notation we will get
nilif we use the dot notation we will get an error (so prefer the dot notation) - If the keys are strings, the dot notation does not work only the square brackets (and we need to include the quotes)
conv["method"] - All data structures in elixir are Immutable, but we can rebind variables, let's change a key in the conv object:
Map.put(conv, :resp_body, "Hello"). This won't change conv, it will return a new object with the changed value. But we can rebindconv->conv = Map.put(conv, :resp_body, "Hello") - That is a common operation so there's this shortcut for it:
conv = %{ conv | resp_body: "Hello"}(But it will only modify keys that already exist in the map, not create new ones)
=is know as the match operator elixir always tries to make the left part the same as the right side- If there's a variable on the left and a value on the right, then the variable will be assigned the value to make the expression match
- If you don't want to bind the left side you can use the pin operator
^a = 2 - Remember that elixir will only try to bind the variables on the left side
- We can use pattern matching to check values or extract them
- If you are not interested in a value you can use the underscore
_to match anything - To concatenate strings we use the
<>operator, so we can also pattern match strings by doing"/bear/" <> id = "/bear/1"1will be binded toid - The cons operator can pattern match the first element of a list and the rest of them like this
[head | tail] = [1, 2, 3, 4, 5]thenheadwill have the value1and tails will have[2, 3, 4, 5] - We can continue calling it until there's only one value left, then the head will be the value, and tail will be an empty list
- We can add guards to a function so it will only run when cerain conditions match. i.e.
def get_storm(id) when is_integer(id) do ... endwill only run if the id is an integer and will throw an error if not
- Many methods in elixir return an tuple with two o rmore elements, the first one being
:okor:errwe can then pattern match on those to handle errors:
def route(%{ method: "GET", path: "/about" <> id } = conv) do
case File.read("../../pages/about.html") do
{:ok, content} ->
%{ conv | status: 200, resp_body: content}
{:error, reason} ->
%{ conv | status: 500, resp_body: "File error: #{reason}"}
end
%{ conv | status: 200, resp_body: "File"}
end- Maybe it's better to design that conditional using clause functions:
def route(%{ method: "GET", path: "/about" } = conv) do
file =
Path.expand("../../pages", __DIR__)
|> Path.join("about.html")
|> File.read
|> handle_file(conv)
end
def handle_file({:ok, content}, conv) do
%{ conv | status: 200, resp_body: content}
end
def handle_file({:error, :enoent}, conv) do
%{ conv | status: 404, resp_body: "File not found"}
end
def handle_file({:error, reason}, conv) do
%{ conv | status: 500, resp_body: "File error: #{reason}"}
end- You can also use
iexas a REPL - Once inside iex to compile (and run) a module use
c "<module-path>"the module will be run and made available - You can also do
iex <module-path>to start iex with the module available iex -S mixwill compile and make available all the modules in the project- To recompile inside iex you can run
recompile() - To execute a module inside iex you can run
r <module-name> - You can see methods available in an object i.e.
h String.(then hit Tab to see available methods)
IO.inspect <data>prints and return a data structure
- There's a shortened syntax for functions
def log(data), do: IO.inspect data - Function clauses are functions with the same name and arity, elixir will match the arguments and run the function that matches
def route(conv, "/wildthings") do
%{ conv | resp_body: "Bears, Lions and Tigers"}
end
def route(conv, "/bears") do
%{ conv | resp_body: "Teddy, Smokey and Paddington"}
end-
Depending on the second argument we will run one or another version of the function.
-
We could also do the clause by pattern matching
def route(%{method: "GET", path: "/wildthings"} = conv) do
%{ conv | resp_body: "Bears, Lions, Tigers" }
end
def route(%{method: "GET", path: "/bears"} = conv) do
%{ conv | resp_body: "Teddy, Smokey, Paddington" }
end- You can create functions that are private to a module by using
defpinstead ofdev - Another way to shorten functions specially when piping is the capture syntax so you can turn this
Enum.sort(fn(b1, b2) -> Storm.order(b1, b2) end)intoEnum.sort(&Storm.order(&1, &2)) - You can make this even shorter by replacing the references at the end by the arity of the function you want to invoke
Enum.sort(&Storm.order/2) - You can also use the & operator to capture expressions. From
Enum.map([1,2,3], fn(x) -> x * 3 end)toEnum.map([1,2,3], &(&1 * 3)) - The result of
&(&1 * 3)is an anonymous function - Alternatively, you can capture the expression as an anonymous function, bind it to a variable, and then pass the function to the higher-order map function:
triple = &(&1 * 3)
Enum.map([1,2,3], triple)- Functions in Elixir act as closures. When a function is defined, it "closes around" the bindings of variables in the scope in which the function was defined. (same as Javascript)
- Here's how to define a regular expression literal in Elixir
~r{regexp} - The ~r is called a sigil and the braces { } are delimiters for the regular expression itself.
- Example regex:
regex = ~r{\/(\w+)\?id=(\d+)}it matches a literal / character followed by one or more word characters, followed by the literal?id=followed by one or more digits. - The match method on the Regex module will return a boolean
- We can capture matching values in a regular expression like this:
regex = ~r{\/(?<thing>\w+)\?id=(?<id>\d+)} - Notice we added ? before the word characters and ? before the digit characters. This says we want to capture the word characters as thing and the digit characters as id.
- Now we can call the named_captures function which returns the given captures as a map:
Regex.named_captures(regex, path)
%{"id" => "1", "thing" => "bears"}- If no captures are found it will return
nil
- Maps are generic data structures, a struct provides more structure
- We define a struct in its own module (only one struct per module)
defmodule Servy.Conv do
defstruct [ method: "", path: ""]
endmethodandpathare the key names, and the empty strings are the default values- Since the array is the only argument to defstruct, we can remove the square brackets:
defmodule Servy.Conv do
defstruct method: "", path: ""
end-
We can create maps like this
map = %{} -
To create a struct we need to give the name of the struct:
conv = %Servy.Conv{}if we want to override defaultsconv = %Servy.Conv{method: "GET", path: "/bears"} -
The type constraint is checked at compile time
-
We can use dot notation
conv.pathto access key from a struct, not the square bracketsconv[:path]
We can use URI.decode_query to get an map of key values from the request body, in this map the keys are strings not atoms. It's not a good idea to convert these into atoms, cause this takes data from users and atoms are not garbage collected.
- Templates use the
eexextension - You can use
<=% inspect(object) =>to output the results from an object as a string
- Seem powerful but didn't quite get how they work, do more research on them.
- You can run a specific test like this
mix test test/my_test.exs. You can also pass multiple paths at the end to run several tests. - You can run all the tests at once with
mix testIt will run all the tests in thetestdirectory. - Sometimes you just want to run a test you can do that by passing the line number where the test you want to run is located
mix test test/my_test.exs:7(You can geet the line number from the message you get from a failing test) - By default, ExUnit executes each test case (test file/module) serially. Thus, as the number of test cases increases, so does the time it takes to execute those tests. You can speed up the execution of multiple test cases by running them concurrently rather than serially. To allow a test case to run concurrently, simply set the async option to true on this existing line:
Use ExUnit.Case, async: true - It's important to note that individual tests within the test case are always run serially.
- There are two ways to run Doctests:
- By adding this line into the module test file
doctest <APPNAME>.<MODULE_NAME> - By defining a new test file to aggregate all the Doctests
defmodule DocTest do use ExUnit.Case doctest <APPNAME>.<MODULE_1> doctest <APPNAME>.<MODULE_2> doctest <APPNAME>.<MODULE_3> end
- By adding this line into the module test file
- You can open erlang monitoring from iex by using the
observererlang module with:observer.start()
You can spawn a process with an anonymous function pid = spawn(fn -> Servy.HttpServer.start(4000) end) or with a named function by passing three arguments (name of the module, name of the method and a list of arguments) pid = spawn(Servy.HttpServer, :start, [4000])
spawnreturns a PID id- Each process has a message queue where we can send messages using
send(parent_pid, message)messages can be anything but it is normal to use a tuple in the form{:result, message} - Inside the PID we can use
receiveto listen for messages sent to that process and then pattern match, i.e.
message = receive do
{:result, message} -> message
end
-
The messages will remain in a process message queue until we explicitly
receivethem or we terminate the process. -
We can also use
flush()to empty a process queue -
This is known as the actor model of concurrency
-
If we call
receiveand there are no messages in the queue, the current process will suspend until a matching message arrives -
The queue is FIFO
-
Process.list |> Enum.countin iex will give you the number of current running processes -
You can also use the Erlang module
system_info:erlang.system_info(:process_count) -
There's a lot of info you can get from `erlang.system_info()
-
You can also spawn processess from iex, i.e. a server that does not block the current session) like this
spawn(ModuleName, :method, [list_of_arguments]) -
You can get the PID of a process by calling
self() -
You can get info about a process using
Process.info(PID)