iex
Atoms are like symbols. e.g. :hello
Strings are encoded in UTF-8.
Strings are represented by binaries which are a sequence of bytes
String module contains helpers, e.g. String.upcase("hello")
Can be created inline. Delimited by fn and end
add = fn a, b -> a + b end
dot (".") is required to invoke an anonymous function. Dot ensures there is no ambiguity between calling an anonymous function and a named function of the same name.
Concat (++) or subtract (--)
++/2 and --/2 operators
e.g. [1]++[2] or [1,2]--[1]
Linked list, head and tail of list
list = [1,2,3]
hd(list)
tl(list)
[11, 12, 13] will print as "char list", '\v\f\r', single quotes are char lists, double quotes are strings.
Curly brackets, can hold any value, like lists.
{1, true, :ok}
Tuples store elements contiguously in memory. Accessing by index is a fast operation. Elixir data types are immutable.
String concatentation is done with the <> operator
Has double equals and triple equals. 1 == 1.0 #=> true vs. 1 === 1.0 #=> false
1 < :atom works because datatypes have a sort order, and so sorting algorithms don't have to worry about types.
The match operator (=)
x=1
1=x
2=x does not match, a MatchError is raised.
Pattern matching on tuples. Count and type needs to match.
{a, b, c} = {:hello, "world", 42}
Use pin operator when you want to match against existing variable's value rather than rebinding the variable.
e.g. ^x when you want to preserve current value
cond is similar to if else statements in other languages
cond do
condition ->
condition ->
end
if and unless
"keyword list" syntax style
if true, do: 1 + 2
Get a character's code point by using ?char e.g. ?a in iex
Define a binary like this: <<1,2,3>>. A binary is a sequence of bytes.
A char list is a list of code points.
When we have a 2-item tuples, and first item is an atom, we call it a "keyword list", e.g.
list = [{:a, 1}, {:b, 2}]
- keys must be atoms
- keys are ordered, as specified by developer
- keys can be given more than once
Maps
- allow any value as a key
- maps keys do not follow any ordering
e.g. `%{:a => 1, 2 => :b}
When a map is used in a pattern, it will always match on a subset of the given value.
put_in/2 and update_in/2 macros, get_and_update_in/2 (extract value and update data)
defmodule Math do
def sum(a, b) do
a + b
end
end
Math.sum(1,2)
Retrieve functions as a function using the name/arity style
Capture syntax can be used as shorthand for creating functions.
e.g. fun = &(&1 + 1)
&(&1+1) is same as fn x -> x + 1 end
&1 represents first argument
Default arguments are supported. Weird thing about function without body, when there are multiple clauses.
Loops are different in a functional language compared with an imperative language A function may have multiple clauses. The function matches that matches the args passed to it.
Ranges are supported, e.g. 1..3
Functions in the Enum module are "eager", each operation generates an intermiedate list until we reach the result.
|> pipe operator
1..10_000 |> Enum.map(&(&1 * 3)) |> Enum.sum
Streams support lazy operations. "Streams are lazy composable enumerables". Does not generate intermediate list. Invoked when we pass the underlying stream to the Enum module.
For large, or possible infinite collections.
stream = Stream.cycle([1,2,3])
Enum.take(stream, 10)
Stream.resource/3
Not OS processes. Not like threads. They are lightweight in terms of memory and CPU. Can have 10s or even 100s of thousands of processes running simultaneously.
iex> pid = spawn fn -> 1 + 2 end
Process.alive?(pid)
Send and receive messages between processes, send/2 and receive/1
Process.link/1
spawn/1 and spawn_link/1 We link our processes to supervisors, which restart processes. Processes are isolated.
Task.start/1 Task.start_link/1
Use Task to get better error reports
Using processes to maintain state and name registration are very common patterns
Elixir provides an abstraction layer called an "Agent" around state
GenServer for building generic servers
File module
File.read/1, vs. File.read!/1 (latter returns contents of file instead of tuple, raises an error) Without bang is preferred when handling different outcomes
case File.read(file) do
{:ok, body} -> # do something with the `body`
{:error, reason} -> # handle the error caused by `reason`
end
Bang variation is good for handing errors
Processes and group leaders
Writing to stdio, actually sending message to group leader
Process.group_leader
iodata and chardata
alias, require and import ("directives", because they have lexical scope) Lexical scope means it may only be executed within the block where it is defined.
iex> import List, only: [duplicate: 2]
List
Module nesting Module attributes
- annotate the module
- hey work as constants
- hey work as temporary module storage to be used during compilation
@moduledoc
Module attributes as constants, e.g. initial state
defmodule MyServer do
@initial_state %{host: "147.0.0.1", port: 3456}
IO.inspect @initial_state
end
As temporary storage (module attribute values read at compilation time not runtime)
defmodule User do
defstruct name: "John", age: 25
end
%User{}
%User{age: 25, name: "Bill"}
john=%User{age: 25}
bill=%User{john | age: 30}
Structs are bare maps underneath
@enforce_keys to enforce certain keys are set
Mechanism to achieve polymorphism
defprotocol and defimpl
Deriving, use implementation for Any
e.g. Derive the Size protocol, using the Any implementation @derize [Size]
@fallback_to_any true
Enum, String.Chars (to_string), Inspect Protocol consolidation
- loop, filter, map into another list
- three parts: generators, filters, collectables
Pattern matching on the generator (left hand) side Only pick up the "good" values
iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]
Working with textual representations
~r create regular expressions
iex> h sigil_r
~s similar to creating double quoted strings, useful when string contains double quotes
~w (word list) e.g. ~w(foo bar baz)
Also support heredocs
try and after, after runs whether try was successful or not
Dynamically typed, though comes with typespecs, which:
- declare typed function signatures
- declare custom data types
@type directive allows us to create our own type We can use this type in the typespec Can perform static code analysis using the type information
Kind of like an interface, a set of function signatures a module has to implement
Formatted text output, :io.format/2
crypto module
digraph (directed graphs) module
Erlang Term Storage, ets and dets
math module
queue module, double-ended FIFO
rand module
zip and zlib
mix.exs file has project configuration
mix compile
Test files are elixir script files (no compilation necessary)
mix test
Mix supports environments:
:dev, :test, :prod
mix help
Can review processes section from getting started
Elixir is immutable where nothing is shared by default. To provide state, two main options:
- Processes
- ETS (Erlang Term Storage)
Agent (simple wrapper around state) GenServer (generic servers, "processes"), sync and async, code reloading GenEvent (generic event, "managers") Task - async units of computation, spawn process, retrieve results later
All implemented on top of processes
A genserver is implemented in two parts: the client API and the server callbacks
Two types of calls for genserver: calls and casts
Calls are synchronous, server must respond
Casts are async
When should we use monitors, and when should we use links?
Links are bi-directional (both sides crash). Monitor is uni-directional. Use monitors when you want to be informed of crashes, but not crash.
"Let it crash" - supervisor will start a fresh copy of the registry
iex -S mix vs. iex -S mix run --no-start to not start applications
Mix makes a distinction between projects and applications
Defines a new supervisor with a :simple_one_for_one strategy
No workers are started during the supervisor initialization
A new worker is started each time start_child/2 is started
"Supervision trees" - supervisors that supervise other supervisors
Start processes inside supervision trees so they are introspectable. Can use tools like Observer to introspect on them.
Erlang term storage - can be used as a cache Don't use prematurely
call is sync cast is async
Questions: is there a debugger? How to do basic logging?
We use the Hex package manager Plug is an HTTP API
Mix generates a Mix.lock file for repeatable builds
mix deps.get
For internal dependencies, can either use a git repo, or umbrella project.
- listens to port until available, and holds a socket
- wait for client connection on that port and accepts it
- read the client request and write a response back
First version, client connection (via telnet) and server crashes when telnet session ends Then we set up a Task to run the server again
Next we need a Task supervisor so we can support multiple client connections
One process is an acceptor that spawns other processes to serve requests
Indent examples, have specific requirements
Run server with mix run --no-halt
Use with to combine matching clauses
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
:capture_log
Routing table for distributed application
e.g.
[{?a..?m, :"foo@computer-name"},
{?n..?z, :"bar@computer-name"}]
Nodes need ~/.erlang.cookie and empd needs to be running
Need to start the VM with a name
iex --sname foo
Can use Node.spawn_link
Can use rpc: :rpc.call(:"foo@Andrews-MacBook-Pro", Hello, :world, [])
Can use GenServer call
Could use Tasks
async/await - async run code, get result later on
Distributed tasks
http://technotif.com/building-website-phoenix-framework/
Run server
iex -S mix phoenix.server
localhost:4000
mix phoenix.new blog
mix ecto.migrate
mix phoenix.server
mix phoenix.routes
iex -S mix phoenix.server and navigate to /posts