163 lines
5.1 KiB
Org Mode
163 lines
5.1 KiB
Org Mode
#+TITLE: Practicing Elixir by Building Concurrent, Distributed, Multiplayer Games in the Terminal
|
|
#+AUTHOR: Lizzy Hunt (Simponic)
|
|
|
|
* 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 - Functional Programming & Meta-Programming
|
|
Elixir is a self-proclaimed "dynamic, functional language for building scalable and maintainable applications". Obviously, one of Elixir's
|
|
main selling points is that of its functional paradigm - it's the second in the list.
|
|
|
|
We'll take a quick look at some features of Elixir, and find that functional programming brings a lot to the table.
|
|
|
|
** 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 one of Elixir's unique features of pattern matching similar to that found in Rust's ~match~ or Scala's ~case~.
|
|
|
|
|
|
|
|
* 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 very brief and quick exploration into concurrency with Elixir, we can now explore the architecture of CheSSH,
|
|
and the hardware cluster it runs on:
|
|
|
|
[[./pis.jpeg]]
|
|
|
|
|