diff options
Diffstat (limited to 'presentation/chessh.org')
-rw-r--r-- | presentation/chessh.org | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/presentation/chessh.org b/presentation/chessh.org new file mode 100644 index 0000000..ff13cfd --- /dev/null +++ b/presentation/chessh.org @@ -0,0 +1,328 @@ +#+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]]. + +* 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 |