Functional Programming and Actor Model with Elixir and the BEAM

11.04.2022Raffael Schneider
Cloud Elixir Realtime Distributed Systems concurrent Otp Erlang Beam functional-paradigm actor-model b-nova techup Stay Tuned

For me personally, the year 2022 is all about Distributed Systems. For this reason, in a new TechUp series, I will deal with distributed systems in general, as well as their conception with Elixir and its ecosystem. First, we will focus on Elixir in order to get a practical grasp of the basic concepts of distri`uted systems using a programming language. After that, we will look at theoretical concepts as well as more advanced topics around distributed systems. This will be divided into the following two TechUp series:

  • Elixir Series
  • Distributed System Series

This is part 2 of the Elixir Series. If you’re not already coming from there, I highly recommend reading Part 1 first, where we focused on Elixir to practically ground basic concepts of distributed systems with a programming language. Now we will look at the theoretical concepts as well as more advanced topics around distributed systems and see Elixir in action with a card game project. Have fun!

Elixir and its debut

Elixir is just like Erlang a programming language that runs on the BEAM. Elixir is Erlang for the 20th century, so to speak, and was conceived and developed by José Valim in 2014 as part of the Brazilian company Plataformatec. The original idea behind Elixir was to make the BEAM platform more accessible by developing a language that is easily extensible and quickly productive. In addition, the language with the Erlang backpack should be suitable for building robust large-scale web applications.

A Hello World in Elixir is the easiest and can be written directly in the REPL:

1
2
iex> IO.puts("Hello World!")
Hello World!

Elixir is primarily characterised by the following 4 core features:

Functional programming: Just like Erlang, Elixir is a functional language. What this means is explained here immediately afterwards.

  • BEAM: BEAM is based on the Actor Model, which we will look at in more detail later on.
  • Ruby-like syntax: Elixir relies on contemporary syntax and is oriented towards Ruby and its readability.
  • Dynamic typing: Elixir has a strict but dynamic typing.

Now we will work through the various points that might seem a little exotic about Elixir, explaining what it is all about, and finally making it clear that Elixir is not rocket science and can be easily learned with a little time and patience.

The Functional Paradigm

Functional programming is definitely not a foreign word in the programming world, yet it is still uncharted territory for many. In the context of this TechUp, I would like to shed some light on this aspect and show the principles of the functional programming paradigm. The functional paradigm is so fundamentally different that the conventional way of programming must be considered first. Conventional programming starts from the model of the Von Neumann architecture, which puts the computer, the calculator, in the foreground and makes the feed of a sequence of instructions to the calculating unit, the CPU, via an input input.

The programming language C embodies this principle of processing sequential instructions, which is also known as the procedural or imperative paradigm. The majority of object-oriented languages build on the procedural paradigm and extend this sequential processing with instantiation, called object, of data structures known in advance, called classes. Functional is different because it is not based on the same principles.

In the early 1930s, Alonzo Church, an American mathematician, put forward a thesis. It is the Church-Turing thesis, better known as Lambda Calculus. In layman’s terms, his thesis consists of the basic assumption that a Turing machine can be completely described with an arbitrary variety but exclusively of mathematical functions. This has the consequence that the idea of a mathematical function can be transferred to the programming world. In mathematics, a function is the relation that assigns an element of a given set to one or more elements of a second set. This is what is meant by the expression f(x) = y, where x is the argument (input) and variable of the initial set and y is the element (output) in the target set.

1
f(x) = y

Thus, a function is a relationship between at least two elements from separate sets. A function or relation is always given in one direction only.

Transferred to functional programming, one speaks of referential transparency when a function behaves like the image above. Referential transparency means that the function is pure and does not allow side-effects. Now you might object that every function in any programming language behaves exactly like this. But this is not the case, since the imperative paradigm often has to have an implicit state in order for its instructions to run in a meaningful way. Perhaps the best way to illustrate this is with an example I borrow from the book Get Programming with Haskell by Will Kurt:

1
2
3
4
5
6
7
8
variable global_var = 0

function impure_func() {
  tick()
  if(timeToReset){
    reset()
  }
}

What is evident here in pseudocode above is the fact that the function impure_func() cannot ensure that the sequence of its inner declarations have no state.

1
2
3
myList = [1,2,3]
myList.reverse()
newList = myList.reverse()

The above snippet is valid code in 3 different programming languages, namely Ruby, Python and JavaScript. Surprisingly, it is not obvious what the results will be without knowing the implementation of the respective runtime. See and be amazed yourself:

1
2
3
Ruby -> [3,2,1]
Python -> None
JavaScript -> [1,2,3]

First was Lambda Calculus - a genealogy of the functional paradigm

The functional paradigm is not a new development, but an expression of a process that began in theoretical mathematics in the 1930s, as described above. After Alonzo Church with his Lambda Calculus, Curry Haskell also refined the theoretical findings in the 1930s and designed the main axioms of combinatorial logic, which form the basis for the computability of functions without variables and thus further the basis for functional programming.

Another word about lambda: A lambda is a synonym for an abstracted function. When one speaks of lambdas, one always means a quasi-contextless function, which ideally transforms an input into an output that is known ahead of time. The reason: The name of the function no longer plays a role and is thus perfect for resolving complex transformation processes into anonymous subfunctions - lambda calculus, after all 😉 !

Figure: Source: Why Elixir Matters: A Genealogy of Functional Programming - Osayame Gaius-Obaseki, ElixirDaze 2018

Here I will explain some milestones of functional programming language development and relate them to today’s application:

  • Lisp (1958): Lisp is the first formalisation of a lambda calculus-based programming language, which forms a family of various Lisp-based dialects such as Common LISP or Scheme. Worth mentioning about Lisp is the fact that code itself represents data and conversely data can in turn be code, also called macro. Lisp stands for list processor, Lisp languages are usually untyped. Finally, it should perhaps be mentioned that Lisp is the oldest programming language still in use after Fortran (1957).
  • ML (1973): Just like Lisp, ML is a formalisation of the lambda calculus and represents a family of further dialects such as Standard ML or CAML. In contrast to Lisp, ML is statically typed and has a stricter evaluation. Thus, the focus of ML is on the data types and their functional evaluation. Lisp and ML represent two different families within the family of functional programming languages, but they complement each other significantly in the development of later programming languages. ML also stands for Meta Language.
  • First non-academic development of a functional programming language by Ericsson, whereby the choice of the functional paradigm was more a result of the necessary requirements. For this reason, the functional paradigm is implemented very pragmatically and is not too strict in the evaluation and implementation of classical functional principles.
  • Haskell (1990) is perhaps the best known and most controversial implementation of the functional paradigm. Haskell is strictly functional and allows only pure functions without side-effects, as well as a very strict but generic type system. Haskell has by far extended the functional paradigm the most and provided features that have influenced all functional as well as non-functional languages. It is thanks to Haskell that we can use generics (anonymous), as well as high-order functions in Java or JavaScript. Haskell is still my personal favourite and I consider Haskell to be one of the most innovative programming languages.
  • F#, Scala, Clojure (from 2000): What Osayame Gaius-Obaseki (see source reference above) called the renaissance started with F# and continues to this day with new releases like Elm or PureScript. Here, partly functional to fully functional programming languages are used to address common runtimes like the JVM with Scala and Clojure or the .NET with F#. Another example would be the V8 as a target runtime with Elm, PureScript, ClojureScript or Reason which are transpiled to EcmaScript (i.e. JavaScript). The trend of addressing well-known, established platforms such as JVM, .NET or V8 with new, quasi-functional programming languages is still going on today and has also had some success in specific domains.
  • Akka (2009): Although Akka is not a programming language, it should nevertheless find its own entry in the context of Erlang and Elixir, since as of 2009 the fusion of functional expressions and a parallelised (concurrent) runtime made it possible to write very robust applications with Akka. Akka is a framework (toolkit) that makes it possible to use an actor model in JVM languages such as Java or Scala. Akka as an additional layer on top of JVM thus offers what the OTP offers with Elixir/Erlang. Akka has been used successfully in fintech and wherever complex constellations of type dependencies are required.
  • Elixir (2014): Lastly, of course, it should be noted that Elixir is also at the current spearhead of decades of development, offering the benefits of Erlang to a new audience.

Object-oriented approach

In order to show what distinguishes functional programming, I have decided to make a comparison between a classical object-oriented and a functional approach. The starting point is given here by a card game. A pack of cards consists of individual cards, which together make up the stack of cards, or the deck of cards.

In the object-oriented paradigm, objects are instantiated at runtime to represent a given domain. This domain here would be a multitude of map objects, which is declaratively described by a map class. We see this instance in purple in the diagram below. It instantiates the class, which obviously correctly maps the map with two fields this.suit and this.value, both by data type a string (here simply called string).

The instance of a card deck is a multitude of cards, which corresponds to a data field, or an array of card objects. This is exemplified by <Cards>[] and is part of the Deck object, below in pink. This means that this.cards is a field of the Deck object, which contains further methods, i.e. sequences of instructions that have the aim of mapping or changing the state of the object. These methods are this.shuffle(), this.save() and this.load().

We thus have objects which are created in a given state at runtime and are used for the execution of further out-of-context instructions. In an object-oriented programming environment, the object is the primary resource with which we try to map a desired functionality. Thus, we think in terms of objects and the runtime, or the lifecycle of these objects. This is also the most common way of programming today. Not because it is necessarily the best or the most efficient way, but because it has been able to establish itself most effectively over time. And rightly so; it is quite intuitive to perceive things as objects.

Functional approach

As a counterpart to object orientation, here is a functional approach. We have a module Cards. Module is not a class and a designation used in Elixir to group functions thematically. You can also think of the module as a domain or namespace in which a number of thematically related functions can be found. So, in our example, the module Cards provides four functions to provide a similar functionality as the example above with its objects.

create_deck() is our first function and provides a list of strings <string>[] on each call. This call does not instantiate an object from a multitude of cards, but simply allocates an area in memory with a data field of strings and makes it available for further use. With each call, a further area in memory is occupied and not the previous area referenced. It should be noted that the runtime of the respective implementation can technically do this in order to work as effectively and efficiently as possible, since it knows that nothing has changed in the overall state. However, this very behaviour is abstracted to the extent that this thinking is no longer necessary. This is immutability, and an important property of functional programming that functions prefer to work with. The output of create_deck() is in turn the input of shuffle(), since shuffle() expects such a data structure and provides another independent data structure as output, in which the same number of cards is output but in a different order, provided shuffle() has been implemented correctly.

Functional programming works most intuitively when transformations are performed on data whose input and output are known in advance. This is the idea of minimising side-effects; namely, that there are no external contexts that could change the state of the data. Basically, when mapping business logic, it is the case that the transformations can be mapped well, but just the IO cannot. save() and load() would like to dump and load the state against a persistence solution of choice (local file system, database, REST interface). Fortunately, languages like Erlang and Elixir are very tractable and pragmatic, whereas a pure functional programming language like Haskell relies on complex transformation methods like the dreaded monad. We won’t explain the monad here but we will note that the more strict the functional, the more unwieldy the implementation of necessary side-effects.

So without instances and objects, one can also perform a sequence of transformations on card data structures in order to map the use case of a card game. It’s all a matter of perspective. 🤓

Main features of functional programming

There are certain common features that all programming languages that partially or fully apply the functional paradigm have. I would like to briefly list these characteristics here and explain what they are.

The main features of functional programming are thus the following. They are not all necessary in this form to be allowed to call a given language functional, but the more use is made of them and they play a role in actual programming, the more likely the language is to be located in a functional paradigm. Furthermore, it should be noted that the definitions here are very superficial, or placed in the practical context, in order to make the functional properties as understandable as possible.

  • High-order functions: If a function can be passed as a parameter of another function and thus the implementation of a function can be kept arbitrarily generic, we speak of high-order functions.
  • Pure functions: A pure function is given if a function returns the same result at any time with the same input parameter. This always implicitly assumes that a given function does not have an eigenstate and that external influences cannot affect the result.
  • Immutable data: Immutability of data sets is another feature. There are no references or shared data. Every necessary data structure is newly generated and allocated in the memory.
  • Statelessness: There are no different states inside or outside a function. Runtimes like BEAM provide application state as a matter of course and pure languages like Haskell have constructs like the monad to accomplish this.
  • No side-effects: There are no external influences that make results look different.
  • List manipulation: Lists are the data structure of choice because they are the best way to represent structured data. List manipulation is also often the main focus.
  • Recursion: The idea that a function calls itself to make the implementation simpler and more concise (and ultimately mathematically correct) is often a core feature.
  • Lazy evalutation: Although Elixir, unlike Haskell, does not have explicit lazy evalutation, it is often a feature and means that an expression is not evaluated until it becomes relevant or significant to the execution.

So, this was our short and hopefully understandable excursion into the world of functional programming. I hope that the basic principles are now clear. There are other principles that are applied in functional programming languages; these certainly include generics, macros, monads and many other features. Macros in particular are used generously in Elixir and are actually the cornerstones of many Syntactic Sugars and DSL-enabled features.

💡 A quick word on generics:

The idea of generics occurred long before Java in ML and only really with the concept of extensible types classes in Haskell. Generics are a specific implementation of Parametric Polymorphism and even generally form their own programming language paradigm.

The Actor Model

Another important concept to better understand Elixir and the BEAM is the Actor Model. We had already shown the robustness of the BEAM, the Erlang VM, with a practical test in Part 1 of the series, and since then we have mentioned several times that the BEAM runs processes by scheduling. The principle used here is a special form of an actor model.

The definition of an actor model is essentially quite simple and can be stated as follows: There are actors, i.e. representing or acting units, which communicate with each other via messages. Each individual actor has a state that is mapped completely separately from other actors in the respective memory areas. The sum of all running actors represents the overall state of a given actor constellation and can be understood in practice as the application state.

This actor-based structuring of information processing can provide for wide-ranging concurrency and thus parallelisation of process processing. In this system, an actuator can perform three different reactions:

  • Sending or receiving messages to other actuators.
  • Request the creation of new actuators
  • Change its own behaviour, or more precisely its own state.

Furthermore, it is certainly a nice anecdote that the idea of objects in Object Oriented Programming originally comes from the fact that in the first OOP programming language by Alan Kay, Smalltalk objects are units that exchange messages with each other and are supposed to map a desired functionality with this behaviour in total. Put simply, the basic idea of OOP is a generic from of the Actor Model, so it is not primarily characterised by class-based polymorphism and encapsulation. But yes, the story eventually turned out differently and today perceptions and the corresponding definitions have changed.

The supervisor tree

The BEAM uses a supervisor structure. This structure is built in such a way that a supervisor has a given number of entities and optionally further sub-supervisors or processes. The totality of these connections is built up as a so-called supervisor tree. This means that the connections build on each other like a tree. The supervisor tree is the technical specification of a special form of the Actor Model Implementation.

The diagram below shows this tree constellation. For example, a top-level supervisor monitors two further subordinate supervisors, which in turn monitor two processes each. As soon as a node, supervisor or process fails, i.e. it is no longer accessible because a serious error occurred at runtime, a mechanism comes into effect that strives for a healed state by reviving the node via a restart.

The revival mechanism is subject to a predefined strategy, usually explicitly declared by the developer. These strategies are given by the BEAM, the documentation for this is therefore on the Erlang level.

  • one_for_one: If a child process terminates, only that process is restarted.
  • one_for_all: If a child process terminates, all other child processes are terminated, and then all child processes, including the terminated one, are restarted.
  • rest_for_one: If a child process terminates, the rest of the child processes (that is, the child processes after the terminated process in start order) are terminated. Then the terminated child process and the rest of the child processes are restarted.
  • simple_one_for_one: A supervisor with restart strategy simple_one_for_one is a simplified one_for_one supervisor, where all child processes are dynamically added instances of the same process.

This strategy is typically declared directly in an lib/Application.ex in the respective opts = [strategy: :one_for_one, ...] and defines the supervisor behaviour of the application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
defmodule MyModule.Application do
  use Application
  require Logger

  def start(_type, _args) do
    children = [
      {Plug.Cowboy, scheme: :http, plug: MyModule.Router, options: [port: cowboy_port()]}
    ]
    opts = [strategy: :one_for_one, name: MyModule.Supervisor]

    Logger.info("Starting application...")
    
    Supervisor.start_link(children, opts)
  end

  defp cowboy_port, do: Application.get_env(:my_module, :cowbody_port, 8080)
end

The advantages of Elixir

Once again, a brief summary of what makes Elixir stand out:

  • Elixir runs on BEAM, a runtime based on Actor Model.
  • Elixir has a Ruby-like syntax
  • Elixir is subject to the functional paradigm
  • Elixir uses dynamic typing

Elixir in action

For this TechUp, I specially completed a Udemy course on Elixir and Phoenix. In this 18-hour course, The Complete Elixir and Phoenix Bootcamp by Stephen Grider, Elixir is illustrated with a playing card project. I would like to use this project here to show you the basics and syntax of Elixir. The corresponding Git repository can be found at here. The problem is that an older version of BEAM was used at the time, which has only been updated to a limited extent since then. Therefore, you can use our b-nova fork of it to join in here.

Texas Hold’em with Elixir

First we tell mix that we want to create a new project called cards. With the command mix new cards below, the project will be created in a new cards directory as expected and provided with the most important files we need to compile the project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 mix new cards
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/cards.ex
* creating test
* creating test/test_helper.exs
* creating test/cards_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd cards
    mix test

Run "mix help" for more commands.

Besides a README.md, a .gitignore, mix creates Elixir-specific files and directories. There is a test/\ directory as well as a lib/\ directory. In the latter is our entry point into the programme with a cards.ex Elixir source file. This lib/cards.ex file is provided with template code and allows us to start coding right away.

lib/cards.ex
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
defmodule Cards do
  @moduledoc """
  Documentation for `Cards`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Cards.hello()
      :world

  """
  def hello do
    :world
  end
end

Now we create our first function in the Cards module. This function is to create a complete deck of French playing cards for us. In Elixir, function names are not to be named with CamelCase like in Java, but follow the underscore-separated naming convention.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  @doc """
    Returns a list of strings representing a deck of playing cards
  """
  def create_deck do
    values = ["Ace", "Two", "Three", "Four", "Five"]
    suits = ["Spades", "Clubs", "Hearts", "Diamonds"]

    for suit <- suits, value <- values do
      "#{value} of #{suit}"
    end
  end

With the Elixir Interactive Shell, IEx for short, compiled modules as well as the BEAM can be addressed at any time.

1
2
3
4
5
 iex -S mix
Erlang/OTP 24 [erts-12.1.5] [source] [64-bit] [smp:10:10] [ds:10:10] [async-threads:1] [dtrace]

Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

Now let’s try calling the cards module with our first function create_deck.

1
2
3
4
5
6
iex(1)> Cards.create_deck
["Ace of Spades", "Two of Spades", "Three of Spades", "Four of Spades",
 "Five of Spades", "Ace of Clubs", "Two of Clubs", "Three of Clubs",
 "Four of Clubs", "Five of Clubs", "Ace of Hearts", "Two of Hearts",
 "Three of Hearts", "Four of Hearts", "Five of Hearts", "Ace of Diamonds",
 "Two of Diamonds", "Three of Diamonds", "Four of Diamonds", "Five of Diamonds"]

The finished cards module should finally look like this:

 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
defmodule Cards do
  @moduledoc """
    Provides methods for creating and handling a deck of cards
  """

  @doc """
    Returns a list of strings representing a deck of playing cards
  """
  def create_deck do
    values = ["Ace", "Two", "Three", "Four", "Five"]
    suits = ["Spades", "Clubs", "Hearts", "Diamonds"]

    for suit <- suits, value <- values do
      "#{value} of #{suit}"
    end
  end

  def shuffle(deck) do
    Enum.shuffle(deck)
  end

  @doc """
    Determines whether a deck contains a given card

  ## Examples

      iex> deck = Cards.create_deck
      iex> Cards.contains?(deck, "Ace of Spades")
      true

  """
  def contains?(deck, card) do
    Enum.member?(deck, card)
  end

  @doc """
    Divides a deck into a hand and the remainder of the deck.
    The `hand_size` argument indicates how many cards should
    be in the hand.

  ## Examples

      iex> deck = Cards.create_deck
      iex> {hand, deck} = Cards.deal(deck, 1)
      iex> hand
      ["Ace of Spades"]

  """
  def deal(deck, hand_size) do
    Enum.split(deck, hand_size)
  end

  def save(deck, filename) do
    binary = :erlang.term_to_binary(deck)
    File.write(filename, binary)
  end

  def load(filename) do
    case File.read(filename) do
      {:ok, binary} -> :erlang.binary_to_term binary
      {:error, _reason} -> "That file does not exist"
    end
  end

  def create_hand(hand_size) do
    Cards.create_deck
    |> Cards.shuffle
    |> Cards.deal(hand_size)
  end
end

Under defps deps do we now add a new dependency that allows us to generate the documentation at compile time. The dependency is defined with the expression {:ex_doc, "~> 0.21"}.

mix.exs
 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
defmodule Cards.MixProject do
  use Mix.Project

  def project do
    [
      app: :cards,
      version: "0.1.0",
      elixir: "~> 1.13",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:ex_doc, "~> 0.21"}
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

💡 mix deps.get gets the dependencies and resolves the necessary third-party packages with a package manager called Hex. Hex is both the package manager and the artifact repository. The public packages can be viewed on Hex here.

Compilation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
Upgraded:
  ex_doc 0.14.3 => 0.26.0 (minor)
New:
  earmark_parser 1.4.18
  makeup 1.0.5
  makeup_elixir 0.15.2
  makeup_erlang 0.1.1
  nimble_parsec 1.2.0
* Updating ex_doc (Hex package)
* Getting earmark_parser (Hex package)
* Getting makeup_elixir (Hex package)
* Getting makeup_erlang (Hex package)
* Getting makeup (Hex package)
* Getting nimble_parsec (Hex package)

Creating documentation:

 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
 mix docs
==> earmark_parser
Compiling 3 files (.erl)
Compiling 33 files (.ex)
Generated earmark_parser app
==> nimble_parsec
Compiling 4 files (.ex)
Generated nimble_parsec app
==> makeup
Compiling 44 files (.ex)
Generated makeup app
==> makeup_elixir
Compiling 6 files (.ex)
Generated makeup_elixir app
==> makeup_erlang
Compiling 3 files (.ex)
Generated makeup_erlang app
==> ex_doc
Compiling 26 files (.ex)
Generated ex_doc app
==> cards
Generated cards app
Generating docs...
View "html" docs at "doc/index.html"
View "epub" docs at "doc/cards.epub"

Call in browser

1
 open doc/index.html

Similar to Rust and Cargo, a full documentation including styling is generated:

Conclusion

Today we have gained an overview of the basics of functional programming in general, as well as the features associated with it. We have seen how functional programming differs from object-oriented programming and that the functional paradigm has some decisive advantages. We also learned about two other important features of Elixir and the BEAM, namely the Actor Model and Supervisor Trees. Finally, we deepened our newly gained knowledge and saw how the implementation of a card game in Elixir could look like. Fun, isn’t it? 🤩

In Part 3 of the Elixir series, we take a look at Elixir’s main framework, namely the Phoenix framework - a Big Gun in terms of web application development. Stay tuned! 💪