Actor Model, Modules, Functions!
Hey, nice to see you around! If you haven’t read the first episode of Learn Elixir with a Rubyist, I totally recommend you to check it out before reading through this, we discussed some cool stuff in there about what Elixir is, what being a Functional language means (at least basically, we’ll get deeper on that), and we wrapped it up learning a little bit about two of the most famous Elixir features, the Pipe Operator and Pattern Matching. If you haven’t checked episode I, do it:
- Episode I - Elixir, Pipe Operator and Pattern Matching
- Episode II - Actor Model, Modules and functions
- Episode III - Maps, Functions + Pattern Matching = ❤️
- Episode IV - Elixir Types, Data Structures and Underscore
- Episode V - Concurrency, Processes and Recursion
- Episode VI - Head + Tail and List Comprehension
- Episode VII - Processes Communication
In this episode I’ll dive into Elixir main code concepts, Modules and Functions, we’ll also discuss the implications around being a functional language, by adding a new rule together with the first one we established on episode I: “Do not override any variable”. Grab a beer and bear with me!
Keeping code together!
When programming Ruby, most of code we write is organized around Objects, represented through Classes and its Instances, that’s the correct structure to have a well organized code when using Object-Oriented Programming (OOP), but Elixir is a Functional Language and because of that we need to change the way we organize our code.
In order to keep our code organized, Elixir brings a lot of different cool tools, like
behaviors and other stuff, but for now let’s stick with the two main ones: Functions and Modules.
In Ruby we had methods, they could exist in or out classes, and update variables all over the place. But in Elixir, functions are the most essential way of organizing code, in general, when using a functional language you want to aim for compact, readable and easy-to-use functions. In order to achieve that, and follow the functional paradigm, we’ll establish a new rule:
Functions shouldn’t update nor depend of external variables (other the it’s arguments)
Simple right? You’ll even realize that this will make your code easier to understand and debug. You might wonder why follow this rule, but stick with me, bellow we will see the benefits of it.
In Elixir, functions can’t exist on it’s own, it need to be encapsulated by a Module, modules are wrappers, they exist to keep functions together, a good example is the default String module, inside of it you will find all string related functions, let’s check some comparisons:
# Really straighrforward, just a new variable # with a string as its value var = "Elixir with a Rubyist" # Ruby is a OOP language, therefore things are # supposed to be objects, here our variable is an # instance of the String class, because of that, we can # call all of its instance methods var.instance_of? String # => true # As usual we call methods by using `.` var.downcase # => "elixir with a rubyist" # We can also chain methods like this, because ruby is # actually calling the method on the previous method's return var.downcase.slice(0..5) # => "elixir"
# Same variable, same value var = "Elixir with a Rubyist" # Here instead of having the string as an instance of # a String class, we will use the String module, that keeps # the string-related functions together and call its # functions, passing our variable as argument String.downcase(var) # "elixir with a rubyist" # We can chain methods as in Ruby, do you remeber the Pipe Operator # we used on last episode? That's the best time to start using it. # In case you forgot, it passes the result from a function as the # first argument of the next one String.downcase(var) |> String.slice(0..5) # "elixir"
Elixir as Functional Language
We already know Elixir is based on Erlang VM (Virtual Machine), and that’s one of things that makes Elixir awesome, not only because it takes advantage of a 30 year-old and well proven virtual machine (what makes it really reliable), but also because the way the Erlang VM is designed.
When developing a virtual machine you can re-implement some concepts that exists in regular systems, and that’s what Ericson did when building Erlang VM, they decided to re-design the way the VM would handle it’s own processes, by following an already known design called Actor Model, this means that Erlang processes aren’t the same kind of process you would have on a regular machine nor the same kind of process your Ruby application would trigger when being executed.
The Actor Model is pretty simple, it basically treats every process as an actor and these actors communicate between each other by exchanging messages (small chunks of information). This processes can be as simple as a single function call.
Now we’ll see the pay off of our new rule about not update or depend on external variables, because, if functions do not depend on anything, why not run it concurrently? Exactly, run multiple function at the same time! The Erlang VM enable us to run it’s processes concurrently (and a LOT of them), they do not share memory between each other (and we will get to that on future episodes), by doing functional programming we avoid the usual errors like race conditions, but we will also get more into this on other episodes, for now let’s check some comparisons and examples, which what we already know:
# This is a pretty common scenario when developing an application, # you need to use the result from different methods into # a new operation to get a new result, we will use `puts` # to check in what order the methods will be called # A method that sum two number together def sum(x, y) puts "==== starting sum" sleep(1) puts "finishing sum" x + y end # A Method that multiplies two numbers def mult(x, y) puts "==== starting mult" sleep(1) puts "finishing mult" x * y end # A third operation that uses the result from both # methods above to perform a new calcualtion number = sum(1, 2) - mult(3, 4) # ==== starting sum # finishing sum # ==== starting mult # finishing mult # => -9 # # As you can see above, the Ruby process executed first # the`sum` method, and only after it, the `mult` one, # and only after both, it was able to performthe third operation
# As we know, in Elixir, functions can not exist outside a module # because of that we are creating a new one called MathTasks # The short version of a module's purpose is to keep related # function together defmodule MathTasks do # Private function that sum two numbers together defp sum(x, y) do IO.puts "==== starting sum" :timer.sleep(1000) IO.puts "finishing sum" x + y end # Private function that mutiply two numbers together defp mult(x, y) do IO.puts "==== starting mult" :timer.sleep(1000) IO.puts "finishing mult" x * y end # This function is the one that perform the operation we want def perform do # Let's create a new process for each function, by using the # Task.async/1 function. These processes will be really # lightweight Erlang processes that can run concurrently sum_task = Task.async(fn -> sum(1,2) end) multi_task = Task.async(fn -> mult(3,4) end) # Let's get those new processes result before performing the # thrid operation sum = Task.await(sum_task) multi = Task.await(multi_task) # A third operation that uses the result from both methods above # to perform a new calculation sum - multi end end # Let's call the perform function to check how it's going to behave MathTasks.perform # ==== starting sum # ==== starting mult # finishing sum # finishing mult # -9 # # That's totally different from the Ruby result, as you can # see above both functions `sum` and `mult` where executed # concurrently, in 'parallel'. The third operation still waited # for both to finish before moving on, but one function didn't # wait for nother. and that's awesome!
You can run your code and this examples on a Elixir Interactive terminal
iex but also save it as a file with
.exs extension and run it using
We can now put together all we saw so far and start doing some nice Elixir code, and we’ll do it, but on our next episode, while also trying to get deeper into how pattern matching fit into functions.
This is the second episode of a series of posts around Elixir, it’s features and how to use it, it’s aimed to help mostly Ruby developers trying to understand it, and it’s supposed to sounds like short bar-like conversations, if you liked please let me know on the comments bellow and over twitter.