summaryrefslogtreecommitdiff
path: root/presentation/chessh.org
blob: f425f3c3925b96adceca4f157bcf130226b24247 (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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#+TITLE: Practicing Elixir by Building Concurrent, Distributed, Multiplayer Games in the Terminal
#+AUTHOR: Lizzy Hunt (Simponic)
#+STARTUP: fold inlineimages

* Reminder: linux.usu.edu
This meeting should be being streamed live at [[https://linux.usu.edu/streams]].

(UPDATE: It is now archived at [[https://linux.usu.edu/stream/12]])

* Introduction
#+BEGIN_SRC elixir
  defmodule Hello do
    def hello() do
      "Hello, Linux Club!"
      |> IO.puts
    end
  end

  Hello.hello()
#+END_SRC

** CheSSH
CheSSH is a multiplayer distributed game of chess over SSH - let's take a quick look before diving into Elixir!

[[https://chessh.linux.usu.edu]]

* Elixir - What You Need
Elixir is a self-proclaimed "dynamic, functional language for building scalable and maintainable applications".

** Basic Data Types
1. ~int~'s, ~bool~'s, ~string~'s are all here
   + ~1~, ~true~, ~"Hello"~
2. Atoms: prefixed with ":" are named constants whose name is their value, similar to symbols in LISP
   + ~:x~, ~:three~
4. Maps: regular key-value store; keys can be literally anything, including other maps
   + ~%{%{a: 1}: 2, %{a: 2}: :an_atom}~
5. Lists: lists are singly-linked elements of "stuff"
   + ~[1,2,3]~, ~[]~, ~[1, [2, :three, %{}]]~
6. Tuples: tuples are fixed-size collections of "stuff"
   + ~{1,2,3}~, ~{1, {2, 3}}~

** Pattern Matching
The match operator "=" does not mean its convential meaning of assignment, but instead an assertion of equivalence. This gives way to a unique
feature of Elixir - pattern matching (similar to that found in Rust's ~match~ or Scala's ~case~).

With pattern matching we can access data from complex structures declaratively.

For example:
#+BEGIN_SRC elixir
  [head | tail] = [1,2,3]
  %{a: a_value} = %{a: 10}
  {:ok, result} = {:ok, 2}
  [head, tail, a_value, result]
#+END_SRC

And will raise an exception when the pattern cannot match:

#+BEGIN_SRC elixir
  %{a: a_value} = %{b: 10}
#+END_SRC

*** Error Handling
Functions that can error will typically return a two-tuple, the first element of which is either the atom ~:ok~ or ~:error~, and the second is the
error info or value.

For many scenarios, the fact that a failed pattern match raises an exception is enough information to know we shouldn't execute further.
#+BEGIN_SRC elixir
  defmodule Sequences do
    def fib(n) when n < 0, do: {:error, :too_small}
    def fib(n) when n <= 1, do: {:ok, n} 
    def fib(n) when n > 1 do
      {:ok, n1} = fib(n-1)
      {:ok, n2} = fib(n-2)

      {:ok, n1 + n2}
    end
  end

  {:ok, f10} = Sequences.fib(10)
  {:ok, fn1} = Sequences.fib(-1)

  IO.puts(fn1)
#+END_SRC

But sometimes we do want to capture that error information! In this case, we use ~case~!

#+BEGIN_SRC elixir
  case Sequences.fib(-1) do
    {:ok, val} -> val
    {:error, err} ->
      IO.puts("Ran into :error #{inspect(err)}")
      0
  end
#+END_SRC

** Piping
Elixir's pipe operator ~|>~ allows programmers to easily write statements as a composition of functions. It simply takes the value of the
function on the left, and passes it as the first argument to the function on the right.

For example, to find the length of the longest string in a list of strings:
#+BEGIN_SRC elixir
  ["Hello, world", "Another string", "Where are all these strings coming from"]
  |> Enum.map(&String.length/1)
  |> Enum.max()
#+END_SRC

** Meta-programming
Akin to my favorite language of all time, LISP, Elixir provides a way to interact directly with code as data (and thus the AST) via a powerful macro system.

However, they are not as elegant, and for that reason, Chris McCord suggests in his book "Metaprogramming Elixir":

#+BEGIN_QUOTE
Rule 1 : Don't Write Macros
#+END_QUOTE

The main reasoning is that it becomes difficult to debug, and hides too much from the user. These are fine trade-offs when you're working alone.

*** when-prime the functional way
#+BEGIN_SRC elixir
  defmodule Prime do
    def is_prime(2), do: true
    def is_prime(n) when rem(n, 2) == 0 or n <= 1, do: false
    def is_prime(n) do
      is_prime_helper(n, 3)
    end

    defp is_prime_helper(n, i) when i * i > n, do: true
    defp is_prime_helper(n, i) when rem(n, i) == 0, do: false
    defp is_prime_helper(n, i) do
      is_prime_helper(n, i + 2)
    end
  end
#+END_SRC

#+BEGIN_SRC elixir
  when_prime_do = fn n, when_true, when_false ->
    if Prime.is_prime(n) do
      when_true.()
    else
      when_false.()
    end
  end

  when_prime_do.(10, fn -> "10 is prime" end, fn -> "10 is not prime" end)
#+END_SRC

*** when-prime the metaprogramming way
#+BEGIN_SRC elixir
  defmodule When do
    defmacro prime(n, do: true_body, else: false_body) do
      quote do
        if Prime.is_prime(unquote(n)), do: unquote(true_body), else: unquote(false_body)
      end
    end
  end

  require When
  When.prime 10, do: "10 is prime", else: "10 is not prime"
#+END_SRC

*** Real-world use-case: ~use~
One such use case for macros (besides those covered previously in my LISP presentation) is to emulate module "inheritance" to share functions.

We can think of a module in Elixir as a set of functions. Then, we can perform unions of modules by the ~use~ macros.

Additionally, with ~behaviours~ we can define callbacks to implement in each unioned module.

#+BEGIN_SRC elixir
  defmodule Animal do
    @callback noise() :: String.t()

    defmacro __using__(_opts) do
      quote do
        @behaviour Animal

        def speak() do
          IO.puts("#{__MODULE__} says #{noise()}")
        end
      end
    end
  end

  defmodule Dog do
    use Animal

    def noise() do
      "Bark"
    end
  end

  defmodule Cat do
    use Animal

    def noise() do
      "Meow"
    end
  end

  Cat.speak()
  Dog.speak()
#+END_SRC

* Elixir - Concurrency
Elixir is built on top of (and completely interoperable with) Erlang - a language developed to build massively fault-tolerant systems in the 80's
for large telephone exchanges with hundreds of thousands of users.

You can imagine (if you look past the many problems with this statement), Elixir and Erlang to be analogous to Python and C, respectively - but
without the massive performance penalty.

** The BEAM
The BEAM powers Elixir's concurrency magic; by running a VM executing Erlang bytecode that holds one OS thread per core,
and a separate process scheduler (and queue) on each. 

Imagine an army of little goblins, and you give each a todo list. The goblins then go complete the tasks in the order best
suited for them, and have the added benefit that they can talk to each other.

** Concurrency - Demo!
Here we will open up two terminals: one running an Elixir REPL on my machine, and another to SSH into my android:

#+BEGIN_SRC python
  import subprocess
  import string
  import random
  cookie = ''.join(random.choices(string.ascii_uppercase +
                                  string.digits, k=32))
  host = "host"
  android = "a02364151-23.bluezone.usu.edu"
  
  h = subprocess.Popen(f"alacritty -e rlwrap --always-readline iex --name lizzy@{host} --cookie {cookie}".split())
  a = subprocess.Popen(f"alacritty -e ssh u0_a308@{android} -p 2222 rlwrap --always-readline iex --name android@{android} --cookie {cookie}".split())
#+END_SRC

#+BEGIN_SRC elixir
  defmodule SpeakServer do
    @sleep_between_msg 2000

    def loop(queue \\ []) do
      case queue do
        [head | tail] ->
          speak(head)

          :timer.sleep(@sleep_between_msg)
          loop(tail)
        [] ->
          receive do
            msg ->
              loop(queue ++ [msg])
          end
      end
    end

    defp speak(msg) do
      System.cmd("espeak", [msg])
    end
  end
#+END_SRC

#+BEGIN_SRC elixir
  defmodule KVServer do
    require Logger
    @max_len_msg 32

    def start(speak_server_pid, port) do
      {:ok, socket} =
        :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true])

      loop_acceptor(socket, speak_server_pid)
    end

    defp loop_acceptor(socket, speak_server_pid) do
      {:ok, client} = :gen_tcp.accept(socket)
      Task.start_link(fn -> serve(client, speak_server_pid) end)

      loop_acceptor(socket, speak_server_pid)
    end

    defp serve(socket, speak_server_pid) do
      msg = socket
      |> read_line()
      |> String.trim()

      if valid_msg(msg) do
        send(speak_server_pid, msg)
      end

      serve(socket, speak_server_pid)
    end

    defp read_line(socket) do
      {:ok, data} = :gen_tcp.recv(socket, 0)
      data
    end

    defp valid_msg(msg), do: String.length(msg) < @max_len_msg && String.match?(msg, ~r/^[A-Za-z ]+$/)
  end

  android = :"android@a02364151-23.bluezone.usu.edu"

  Node.connect(android)
  speak_server_pid = Node.spawn(android, &SpeakServer.loop/0)

  KVServer.start(speak_server_pid, 42069)
#+END_SRC

This demo shows how we can:
+ Connect nodes running Elixir
+ Spawn processes on nodes and inter process communication
+ Basic Elixir constructs (pattern matching, atoms, function calls, referencing functions)

* CheSSH
With a brief quick exploration into concurrency with Elixir, we can now explore the architecture of CheSSH,
and the hardware cluster it runs on:

[[./pis.jpeg]]

** Erlang SSH Module - (maybe) building a tic tac toe game!
So much networking stuff is built on top of Erlang that its standard library - OTP - has implementations for tons of stuff you'd regularly reach for a library to help; ssh, snmp,
ftp, are all built in "OTP Applications".

It requires a little bit of time with headaches, but the docs are generally pretty good (with occasional source code browsing): [[https://www.erlang.org/doc/man/ssh.html]]

** Architecture
[[./architecture.png]]

** Lessons Learned
1. Use Kubernetes (~buildscripts~ is so horribly janky it's actually funny)
2. Docker was a great idea
3. Don't hardcode IP's
4. Don't try to use Multicast
5. Load balancing SSH