summaryrefslogtreecommitdiff
path: root/3 resources/programming/Elixir.md
blob: b106ae4c8cc44301a2afb091f5fdd52308c6d000 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
Elixir supports "macro's", which is Elixir code that runs at compile time. They receive the AST of the source code as input and can apply transformations to it. This is how Elixir is written itself, using Elixir macro's.

Elixir functions can be organized into [[Elixir - modules]].
```
defmodule MyModule do
	# Comment
	@moduledoc "Documentation for the module"
	import IO
	alias IO, as MyIO # Alias import

	@pi 3.14 # Module attribute

	@doc "Describe what the function does"
	def hello do
	    MyIO.puts("blaat")
	    3*@pi # Module attribute reference
	done

	@spec hello_private(string) # typespec, can be used by dialyzer, very useful
	defp hello_private(hello) do
		puts(hello)
	done
done
```

The [[Elixir - Kernel]] module is always imported, so functions we use without prefix come from Kernel.

Elixir introduces a concept called [[Elixir - Atoms]]. They are named constants, like enumerations in C. 
```
:blaat
:"Bla at"
Blaat == :"Elixir.Blaat" # Alias

var = :blaat # var contains only a reference to the atom, thus is small and fast.
```

[[Elixir - Aliases]] are internally represented as "Elixir.RealThing", in the case of the module alias above `MyIO == Elixir.IO`.

[[Elixir - Tuples]] group a, usually small, fixed number of elements together. Kernel.elem/2 for access, Kernel.put_elem/3 for updating.

[[Elixir - Lists]] are a recursive structure with a head of any type and a tail which is another list. They are notated as regular arrays in other languages, but can be written as `[head | tail]`. Therefore it's easy and efficient to push a new item to the top, we can use the head | tail notation -> 
https://hexdocs.pm/elixir/List.html
https://hexdocs.pm/elixir/Enum.html
```
list = [1, 2, 3]
list = [4 | list]
list
[4, 1, 2, 3]
```

[[Elixir - Immutable functions]] give a form of atomicity, because operations (other functions) they call do not mutate data if anything fails we can just return the original data without having changed anything.

[[Elixir - Maps]] %{}, can also be created with Map.new([{1, 2}, {3, 4}]) (2-tuples).
https://hexdocs.pm/elixir/Map.html
```
a = %{1 => 1}
a[1]
1
```
Maps can also be used to define structures,
```
person = %{name: "Jasper"}
```

[[Elixir - Binaries]] are consecutive sequences of bytes.
```
<<1, 1>> is a two-byte binary where each byte has value of, thus
0000 0001 0000 0001
<<1>> <> <<2>> concatenate two binaries
0000 0001 0000 0010
```

[[Elixir - Strings]]
```
"This is a stringt"
"
Multiline
string
"
a = 1337
"Print number #{a}" # #{} allows evaluation of values strings

~s(This is a sigil which is also a string)
~s(Is useful to "use" quotes)
str = "INTERPOLATION!"
~S(Capital-S sigil allows prevention of string #{str} and \nescaping)
"Capital-S sigil allows prevention of string \#{str} and \\nescaping"

"Blaat" <> " henk" # Concatenation works like binaries, because strings are binaries
```

[[Elixir - Lambda]]
```
square = fn x -> # lambda's use fn
	x*x
end

square.(5) # lambda is called with name period arguments enclosed by parens. The dot is to make it known that we are calling a lambda and not a regular function.
```

For cases where the lambda just forwards its arguments to another function there is a special syntax, example:
[[Elixir - capture operator]]
```
Enum.each([1, 2, 3], fn x -> IO.puts(x) end)
Enum.each([1, 2, 3], &IO.puts/1) # The & is called the "capture operator" and can also be used to shorten a lambda definition:
lambda = fn x, y, z -> x * y + z end
lambda = &(&1 * &2 + &3) # Like bash arguments ${1} ${2} etc
```

[[Elixir - Closure]]
A lambda can reference variables from the outside scope. If we rebind the variable in the parent scope, the lambda will still reference the old one.
```
outside = "Abc"
lambda = &IO.puts/1
outside = "cdef"
lambda.()
"Abc"
```

Range
0..1, internally represented as a map with bounds set, therefore small no matter how "big" the range. Is also an enumeration so can use the Enum module.

Keyword list
List of 2-tuple where the 1st element is an atom. E.g. `[{:monday, 1}, {:tuesday, 2}]` can be written more elegant as `[monday: 1, tuesday: 2]`
https://hexdocs.pm/elixir/Keyword.html
Can be used as kwargs like in python.

MapSet
https://hexdocs.pm/elixir/MapSet.html
Also an enumeration. 
Initialize with MapSet.new

Times and Dates
Have modules: Date, Time, DateTime, NaiveDateTime
Created with sigil ~D for dates, and ~T for time
```
dt = ~D[2023-01-01]
dt.year
2023

tm = ~T[19:03:32]
tm.second
32
```

IO lists
Are lists that can consist of one of three types:
- Int in range 0..255
- Binaries
- Another IO list
It is thus a tree. Input operations are O(1)


Pattern matching
The '=' operator is not an assignment operator, but a match operator. 
pattern = expression
Pattern can be list, map, tuple, variable, binaries, binary strings
constants, atom can be matched to discriminate results of expressions.
{:ok, result} = expr, fails is expr returns for example {:error, result}
Patterns can be nested: `{_, {hour, _, }, _} = :calendar.local_time()`

Maps can partial match, to extract a property from a complex map.
Lists can abuse their recursive naturs: `[head | tail] = [1, 2, 3] head = 1, tail = [2,3]`
Pin-operator `^` is used to match against the value of a variable:
```
a = "Bob"
{^a, _} = {"Bob", 25} <- Matches because the value of a is "Bob"
{^a, _} = {"Alice", 25} <- Doesn't match
```

Pattern matching using strings it's possible to match the beginning of a string and assign the rest to a var:
```
command = "ping www.hostnet.nl"
"ping " <> url = command
url = "www.hostnet.nl"
```

Pattern matching can be done in function arguments and enabled "multiclause functions", which is a sort of function overloading. It's multiple definitions of the same function, with the same arity, but with different argument patterns. They are treated as a single function, so with the capture operator you can use all "variants".
```
defmodule Geo do
	def area({:square, a, b}), do a * b end
	def area({:circle, r}), do r * r * pi end
	def area(unknown), do {:error, {:unknown_shape, unknown}} end
	# ^ do mind that the arity has to match for this catch-all error, also
	# ordering is important. The runtime matches from top to bottom.
end

fn = &Geo.area/1

fn.({:square, 1, 2})
2
fn.({:circle, 23})
whatever this is
```

Conditionals can be implemented using multiclause functions, but also with the regular if..else statements, cond do .. end and case expression do ... end.

A with-clause can be used to match multiple patterns in order and halt if a pattern doesn't match.
```
defp extract_login(%{"login" => login}%) do, %{:ok, login} end
defp extract_login(_) do, {:error, "login missing"} end

defp extract_email(%{"email" => email}) do, %{:ok, email} end
defp extract_email(_) do, %{:error, "email missing"} end

def extract_user(user) do
	case extract_login(user) do
		{:error, reason} -> {:error, reason}
		{:ok, login} ->
		case extract_email(user) do
			{:error, reason} -> {:error, reason}
			{ok, email} -> %{login: login, email: email}
		end
	end
end

# can be written as

def extract_user(user) do
	with {:ok, login} <- extract_login(user),
		 {:ok, email} <- extract_email(user) do
		{:ok, %{login: login, email: email}}
	end
end	
``` 

Looping is mainly implemented via recursion. The break condition is implemented via a multiclause function matching the condition that you want to break at.
Recursion can be expensive, unless the recursive call is at the end of a function, which is called a tail-call. Tail-calls are optimized to not require any additional memory, because their result is also the result of the caller, so we don't need to come back to the caller.