- Notifications
You must be signed in to change notification settings - Fork113
💧 Learn the Elixir programming language to build functional, fast, scalable and maintainable web applications!
dwyl/learn-elixir
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- Learn
- Scalability
- Speed
- Compiled and run on theErlang VM ("BEAM").(Renowned for efficiency)
- Much better"garbage collection" than virtually any other VM
- Many tiny processes (as opposed to "threads"which are more difficult to manage)
- Functional language withdynamic typing
- Immutable data so"state" is alwayspredictable!
- High reliability, availability and fault tolerance (because of Erlang)means apps built with
Elixir
are run in production foryearswithout any "downtime"! - Real-time web apps are "easy"(or at least easier than many other languages!) asWebSockets & streaming are baked-in
Thingswill go wrong withcode, andElixir
provides supervisors which describe how to restart parts ofyour system when things don't go as planned.
If you have the time, these videos give a nice contextual introduction into whatElixir
is, what it's used for and how it works:
- Code School'sTry Elixir, 3 videos (25mins 🎥 plus exercises, totalling 90mins). The 'Try' course is free (there is an extended paid for course).
- Pete Broderick'sIntro to Elixir (41 mins 🎥)
- Jessica Kerr'sElixir Should Take Over the World (58 mins 🎥)
Not a video learner? Looking for a specific learning?https://elixirschool.com/ is an excellent, free, open-source resource that explains all thingsElixir
📖 ❤️.
Before you learnElixir
as a language you will need to have it installed on your machine.
To do so you can go tohttp://elixir-lang.org/install.html or follow our guide here:
Using theHomebrew package manager:brew install elixir
If you have any trouble with
ssl
when running anElixir
App on yourMac
,see:/install-mac.md
- Add the Erlang Solutions repo:
wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && sudo dpkg -i erlang-solutions_2.0_all.deb
- Run:
sudo apt-get update
- Install the Erlang/OTP platform and all of its applications:
sudo apt-get install esl-erlang
- Install Elixir:
sudo apt-get install elixir
Web installer
Erlang installer
Download the Windows installer for Erlang (32 or 64-bit):erlang.org/downloads
Click next, next,..., close
Elixir installer
Download the Elixir installer matching your Erlang version: [github.com/elixir-lang/elixir/releases/download/v1.17.3]https://github.com/elixir-lang/elixir/releases/download/v1.17.3/elixir-otp-27.exe
Click next, install, ..., close
Check to see if successful
Run
elixir -v
in your terminalShould output Erlang & Elixir versions
Chocolatey (Package Manager)
choco install elixir
- Easy peasy if you have
Elixir
installed. Just click below
OnceLivebook
installed on your machine, just click on the button below (or fork and run it):
- Alternatively, you can run a
Docker
image, no need to installElixir
orLivebook
. LaunchDocker
and run theLivebook
image:
docker run -p 8080:8080 -p 8081:8081 --pull always -e LIVEBOOK_PASSWORD="securesecret" livebook/livebook
and in another terminal you launch the browser (you will need to authneticate with "securesecret") with the command:
open http://localhost:8080/import?url=https://github.com/dwyl/learn-elixir/blob/main/learn-elixir-on-livebook.livemd
- Finally, if you don't have
Docker
norElixir
andLivebook
installed, you can run a remote version in the cloud. Follow this!
You (right-)click on the grey button"Run in Livebook" below.
❗ Youright-click" to keep this reminder page open 😉 because you will need to remember to do 2 things:
- firstly, look at the bottom for the link "see source" as showed below, 🤔, and click.
- and finally, select the file [dwyl-learn-elixir.livemd]. It should be printed in green, and "join session". 🤗
Happy learning! 🥳
This links to the remote Livebook: 👉
After installing
Elixir
you can open the interactive shell by typingiex
.This allows you to type in anyElixir
expression and see the result in the terminal.Type in
h
followed by thefunction
name at any time to see documentation information about any given built-in function and how to use it. E.g If you typeh round
into the (iex) terminal you should seesomething like this:
- Typing
i
followed by the value name will give you information about a value in your code:
This section brings together the key information from Elixir'sGetting Starteddocumentation and multiple other sources. It will take you through some examples to practice using and familiarise yourself with Elixir's 7 basic types.
Elixir's 7 basic types:
integers
floats
booleans
atoms
strings
lists
tuples
Type1 + 2
into the terminal (after openingiex
):
iex>1+23
More examples:
iex>5*525iex>10/25.0# When using the `/` with two integers this gives a `float` (5.0).# If you want to do integer division or get the division remainder# you can use the `div` or `rem` functionsiex>div(10,2)5iex>div10,25iex>rem10,31
Elixir supportstrue
andfalse
as booleans.
iex>truetrueiex>falsefalseiex>is_boolean(true)trueiex>is_boolean(1)false
Besides the booleanstrue
andfalse
Elixir
also has theconcept of a "truthy" or "falsy" value.
- a value is truthy when it is neither
false
nornil
- a value is falsy when it is
false
ornil
Elixir has functions, likeand/2
, thatonly work withbooleans, but also functions that work with thesetruthy/falsy values, like&&/2
and!/1
.
The syntax<function_name>/<number>
is the conventionused in Elixir to identify a function named<function_name>
that takes<number>
parameters.The value<number>
is also referred to as the functionarity.In Elixir each function is identified univocally both byits name and its arity. More information can be foundhere.We can check the truthiness of a value by using the!/1
function twice.
Truthy values:
iex>!!truetrueiex>!!5trueiex>!![1,2]trueiex>!!"foo"true
Falsy values (of which there are exactly two):
iex>!!falsefalseiex>!!nilfalse
Atoms are constants where their name is their own value(some other languages call these Symbols).
iex>:hello:helloiex>:hello==:worldfalse
true
andfalse
are actually atoms in Elixir
Names ofmodules inElixir
are also atoms.MyApp.MyModule
is a valid atom, even if no such module has been declared yet.
iex>is_atom(MyApp.MyModule)true
Atoms are also used to reference modules from Erlang libraries,including built-in ones.
iex>:crypto.strong_rand_bytes3<<23,104,108>>
One popular use of atoms inElixir
is to use them as messagesforpattern matching.Let's say you have a function which processes anhttp
request.The outcome of this process is either going to be a success or an error.You could therefore use atoms to indicate whetheror not this process is successful.
defprocess(file)dolines=file|>split_linescaselinesdonil->{:error,"failed to process file"}lines->{:ok,lines}endend
Here we are saying that the method,process/1
will return atuple response.If the result of our process is successful, it will return{:ok, lines}
,however if it fails (e.g. returns nil) then it will return an error.This will allows us topattern match on this result.
{:ok,lines}=process('text.txt')
Thus, we can be sure that we will always have the lines returned to usand never anil value (because it will throw an error).This becomes extremely useful when piping multiple methods together.
Strings are surrounded by double quotes.
iex>"Hello World""Hello world"# You can print a string using the `IO` moduleiex>IO.puts"Hello world""Hello world":ok
Elixir uses square brackets to make a list.
iex>myList=[1,2,3]iex>myList[1,2,3]iex>length(myList)3# concatenating lists togetheriex>[1,2,3]++[4,5,6][1,2,3,4,5,6]# removing items from a listiex>[1,true,2,false,3,true]--[true,false][1,2,3,true]
Lists areenumerable and can use theEnummodule to perform iterative functions such as mapping.
Elixir uses curly brackets to make a tuple.
Tuples are similar to lists but arenot suited to data sets that need to be updated or added to regularly.
iex>tuple={:ok,"hello"}{:ok,"hello"}# get element at index 1iex>elem(tuple,1)"hello"# get the size of the tupleiex>tuple_size(tuple)2
Tuples arenot enumerable and there are far fewer functions availablein theTuple module. You can reference tuple values by index butyou cannot iterate over them.If you must treat your tuple as a list,then convert it usingTuple.to_list(your_tuple)
If you need to iterate over the values use a list.
When dealing withlarge lists or tuples:
Updating
alist
(adding or removing elements) isfastUpdating
atuple
isslowReading
alist
(getting its length or selecting an element) isslowReading
atuple
isfast
source:http://stackoverflow.com/questions/31192923/lists-vs-tuples-what-to-use-and-when
Anonymous functions start withfn
and end withend
.
iex>add=fna,b->a+bendiex>add.(1,2)3
Note a dot.
between the variableadd
and parenthesis is requiredto invoke an anonymous function.
In Elixir, functions arefirst class citizens
meaning that they canbe passed as arguments to other functions the same way integers and strings can.
iex>is_function(add)true
This uses the inbuilt functionis_function
which checks to see ifthe parameter passed is a function and returns a bool.
Anonymous functions areclosures (named functions are not)and as such they can access variablesthat are in scope when the function is defined.You can define a new anonymous function that uses theadd
anonymous function we have previously defined:
iex>double=fna->add.(a,a)endiex>double.(5)10
These functions can be useful but will no longer be available to you.If you want to make something more permanent then you can create amodule
.
With modules you're able to group several functions together.Most of the time it is convenient to write modules into filesso they can be compiled and reused.
Get started by creating a file namedmath.ex
,open it in your text editor and add the following code
defmoduleMathdodefsum(a,b)doa+bendend
In order to create your own modules in Elixir, use thedefmodule
macro,then use thedef
macro to define functions in that module.So in this case the module isMath
and the function issum
.
Once this is saved the file can be compiled by typingelixirc
into the terminal followed by the file name.
$ elixirc math.ex
This will generate a file namedElixir.Math.beam
containing the bytecodefor the defined module. If we startiex
again, our module definitionwill be available (provided thatiex
is startedin the same directory the bytecode file is in):
iex>Math.sum(1,2)3
To get startedwith your firstElixir
projectyou need to make use of theMix
build tool that comes withElixir
.Mix allows you to do a number of things including:
- Create projects
- Compile projects
- Run tasks
- Testing
- Generate documentation
- Manage dependencies
To generate a new project follow these steps:
Initialise a project by typing the following command in your terminal,replacing [project_name] with the name of your project:
mix new [project_name]
e.g:
mix new animals
We have chosen to call our project 'animals'
This will create a new folderwith the given name of your projectand should also print somethingthat looks like this to the command line:
* creating README.md* creating .formatter.exs* creating .gitignore* creating mix.exs* creating lib* creating lib/animals.ex* creatingtest* creating test/test_helper.exs* creating test/animals_test.exsYour Mix project was created successfully.You can use"mix" to compile it,test it, and more:cd animals mixtestRun"mix help"for more commands.
Navigate to your newly created directory:
>cd animals
Open the directory in your text editor. You will be able to see thatElixir
hasgenerated a few files for us that are specific to our project:
lib/animals.ex
test/animals_test.ex
Open up theanimals.ex
filein the lib directory.You should already see somehello-world
boilerplate like this:
defmoduleAnimalsdo@moduledoc""" Documentation for Animals. """@doc""" Hello world. ## Examples iex> Animals.hello() :world """defhellodo:worldendend
Elixir
has created a modulewith the name of your projectalong with a functionthat prints out a:world
atom when called.It's also added boilerplate formodule and function documentation - the first part of the file.(we will go into more detail about documentation later)
Let's test out the boilerplate code.In your project directory type the following command:
> iex -S mix
What this means is:"Start theElixir
REPLand compile with the context of my current project".This allows you to access modules andfunctions created within the file tree.
Call thehello-world
function given to us byElixir
.It should print out the:world
atom to the command line:
> Animals.hello# :world
Let's start to create our own methods in theAnimals
module.Replace thehello-world
function with the following:
@doc"""create_zoo returns a list of zoo animals## Examples iex> Animals.create_zoo ["lion", "tiger", "gorilla", "elephant", "monkey", "giraffe"]"""defcreate_zoodo["lion","tiger","gorilla","elephant","monkey","giraffe"]end
To run our new code we will first have to recompile ouriex
.This can be done by typing:
>recompile()
Now we will have access to thecreate_zoo
method. Try it out in the command line:
> Animals.create_zoo# ["lion", "tiger", "gorilla", "elephant", "monkey", "giraffe"]
Let'sextend theAnimals
module.Imaging you're visiting the zoobut you can't decide which order to view the animals.We can create arandomise
functionbefore the finalend
that takes a list of animalsand returns a new list with a random order:
@doc"""randomise takes a list of zoo animals and returns a new randomised list withthe same elements as the first.## Examples iex> zoo = Animals.create_zoo iex> Animals.randomise(zoo) ["monkey", "tiger", "elephant", "gorilla", "giraffe", "lion"]"""defrandomise(zoo)doEnum.shuffle(zoo)end
Note: we are making useof a pre-built module called
Enum
which has a list of functionsthat you can use on enumerables such as lists.Documentation available at:hexdocs.pm/elixir/Enum.html
Let's add another functionto theAnimals
module.We want to find outif our zoo contains an animal:
@doc"""contains? takes a list of zoo animals and a single animal and returns a booleanas to whether or not the list contains the given animal.## Examples iex> zoo = Animals.create_zoo iex> Animals.contains?(zoo, "gorilla") true"""defcontains?(zoo,animal)doEnum.member?(zoo,animal)end
NOTE: It's convention when writing a function that returns a boolean to add a questionmark after the name of the method.
Create a functionthat takes a list of animalsand the number of animalsthat you'd like to seeand returns a list of animals:
@doc"""see_animals takes a list of zoo animals and the number of animals thatyou want to see and then returns a list## Examples iex> zoo = Animals.create_zoo iex> Animals.see_animals(zoo, 2) ["monkey", "giraffe"]"""defsee_animals(zoo,count)do# Enum.split returns a tuple so we have to pattern match on the result# to get the value we want out{_seen,to_see}=Enum.split(zoo,-count)to_seeend
Write a functionthat willsave our list of animals to a file:
@doc"""save takes a list of zoo animals and a filename and saves the list to that file## Examples iex> zoo = Animals.create_zoo iex> Animals.save(zoo, "my_animals") :ok"""defsave(zoo,filename)do# erlang is converting the zoo list to something that can be written to the# file systembinary=:erlang.term_to_binary(zoo)File.write(filename,binary)end
In your command line,run the following after recompiling:
> zoo = Animals.create_zoo> Animals.save(zoo,"my_animals")
This will create a new filein your file tree with the name of the file thatyou specified in the function.It will contain some odd characters:
�l\����m����lionm����tigerm����gorillam����elephantm����monkeym����giraffej
Write a functionthat will fetch data from the file:
@doc"""load takes filename and returns a list of animals if the file exists## Examples iex> Animals.load("my_animals") ["lion", "tiger", "gorilla", "elephant", "monkey", "giraffe"] iex> Animals.load("aglkjhdfg") "File does not exist""""defload(filename)do# here we are running a case expression on the result of File.read(filename)# if we receive an :ok then we want to return the list# if we receive an error then we want to give the user an error-friendly messagecaseFile.read(filename)do{:ok,binary}->:erlang.binary_to_term(binary){:error,_reason}->"File does not exist"endend
Thecase
expressionallows us to pattern matchagainst various options and react accordingly.
What if we wanted to call some of our functions in succession to another? Let's create a function that creates a zoo, randomises it and then returns a selected number of animals to go and see:
@doc"""selection takes a number, creates a zoo, randomises it and then returns a listof animals of length selected## Examples iex> Animals.selection(2) ["gorilla", "giraffe"]"""defselection(number_of_animals)do# We are using the pipe operator here. It takes the value returned from# the expression and passes it down as the first argument in the expression# below. see_animals takes two arguments but only one needs to be specified# as the first is provided by the pipe operatorAnimals.create_zoo()|>Animals.randomise()|>Animals.see_animals(number_of_animals)end
Now that we have the functionality for our module,let's take a look at the documentationthat we have writtenand how we can maximise its use.
When we created a new project with mix, it created a file for us calledmix.exs
which is referred to as the 'MixFile'. This file holds information about ourproject and its dependencies.
At the bottom of the file it gives us a function calleddeps
which manages allof the dependencies in our project. To install a third party package we need tomanually write it in the deps function (accepts a tuple of the package name andthe version) and then install it in the command line. Let's installex_doc
asan example:
Add the following to the deps function in yourmix.exs
file:
defpdepsdo[{:ex_doc,"~> 0.21"}]end
Then in the command line quit youriex
shell and enter the following to installtheex_docs
dependency:
> mix deps.get
You might receive an error saying:
Could not find Hex, which is needed to build dependency :ex_docShall I install Hex? (if running non-interactively,use:"mix local.hex --force") [Yn]
If you do then just entery
and then press enter.This will install thedependencies that you need.
Onceex_docs
has been installed,run the following command to generatedocumentation (make sure you're not iniex
):
> mix docs
This will generate documentationthat can be viewedif you copy the file pathof theindex.html
filewithin the newly createddoc
folderand then paste it in your browser.If you have added documentationto your module and functionsas per the examples above,you should see something like the following:
It looks exactly like the formatof the officialElixir
docsbecause they used the same tool to create theirs.Here is what the method documentationshould look like if you click onAnimals
:
This is an incredibly powerful toolthat comes 'baked in' with elixir.It means that other developerswho are joining the projectcan be brought up to speed incredibly quickly!
When you generate a project withElixir
it automatically gives you a number offiles and directories. One of these directories is calledtest
and it holds twofiles that should have names like:
[project_name]_test.exs
test_helper.exs
Our first file was calledanimals_test.exs
and it contained some boilerplate thatlooks like:
defmoduleAnimalsTestdouseExUnit.CasedoctestAnimalstest"greets the world"doassertAnimals.hello()==:worldendend
NOTE: It automatically includes a line calleddoctest Animals
. What this meansis that it can run tests from the examples in the documentation that you write foryour functions
To run the tests enter the following in your terminal:mix test
It should print out whether the tests pass or fail.
Let's add some tests of our own. Firstly let's write a test for theAnimals.randomise
function. The reason why we wouldn't want to write a doctest for this is becausethe output value changes everytime you call it. Here's how we would write a testfor that type of function:
In theanimals_test.exs
file, remove the boilerplate "greets the world" test and thenadd this to test that the order of the animals inzoo
changes (is randomised):
test"randomise"dozoo=Animals.create_zooassertzoo!=Animals.randomise(zoo)end
NOTE: You do not need to install and require any external testing frameworks.It all comes with theElixir
package. Simply writetest
followed by a stringrepresenting what you are trying to test and then write your assertion.
The test above isn't completely air-tight.Elixir
provides you with assertions thatcan help deal with things like this. The test could be re-written like so:
test"randomise"dozoo=Animals.create_zoorefutezoo==Animals.randomise(zoo)end
This is basically saying "prove to be false that zoo is equal to Animals.randomise(zoo)"
If you want to learn about code coverage then check out the followingtutorial,https://github.com/dwyl/learn-elixir/tree/master/codecov_example
InElixir
version 1.6 themix format
task was introduced.see:elixir-lang/elixir#6643
mix format
is abuilt-in way to format yourElixir
codeaccording to the community-agreed consistent style.This meansall code will look consistent across projects(personal, "work" & hex.pm packages)which makes learning faster and maintainability easier!At present, using the formatter isoptional,howevermostElixir
projects have adopted it.
Touse the mix task in your project,you can either check filesindividually, e.g:
mix format path/to/file.ex
Or you can define apattern for types of filesyou want to check the format of:
mix format"lib/**/*.{ex,exs}"
will check all the.ex
and.exs
files in thelib/
directory.
Having to type this pattern each timeyou want to check the files istedious.Thankfully,Elixir
has you covered.
In the root of yourElixir
project, you will find a.formatter.exs
config file with the following code:
# Used by "mix format"[inputs:["{mix,.formatter}.exs","{config,lib,test}/**/*.{ex,exs}"]]
This means that if you runmix format
it will check themix.exs
fileandall.ex
and.exs
files in theconfig
,lib/
andtest
directories.
This is the most common pattern for running mix format.Unless you have areason to "deviate" from it, it's a good practice to keep it as it is.
Simply run:
mix format
And your code will now follow Elixir's formatting guidelines.
We recommend installing a plugin in your Text Editor to auto-format:
Atom Text Editor Auto-formatter:https://atom.io/packages/atom-elixir-formatter
Vim
Elixir
Formatter:https://github.com/mhinz/vim-mix-formatVSCode:https://marketplace.visualstudio.com/items?itemName=sammkj.vscode-elixir-formatter
Read the
mix/tasks/format.ex
source tounderstand how it works:https://github.com/elixir-lang/elixir/blob/master/lib/mix/lib/mix/tasks/format.exhttps://hashrocket.com/blog/posts/format-your-elixir-code-now
https://devonestes.herokuapp.com/everything-you-need-to-know-about-elixirs-new-formatter
To publish yourElixir
package toHex.pm:
Check the version in
mix.exs
is up to date and that it follows thesemantic versioning format:MAJOR.MINOR.PATCH where
MAJOR version when you make incompatible API changes MINOR version when you add functionality in a backwards-compatible manner PATCH version when you make backwards-compatible bug fixes
Check that the main properties of the project are defined in
mix.exs
- name: The name of the package
- description: A short description of the package
- licenses: The names of the licenses of the package
- NB. dwyl's
cid
repo contains anexample of a more advancedmix.exs
file whereyou can see this in action
Create aHex.pm accountif you do not have one already.
Make sure thatex_docis added as a dependency in you project
defpdepsdo[{:ex_doc,"~> 0.21",only::dev}]end
When publishing a package, the documentation will be automatically generated.So if the dependencyex_doc
is not declared, the package won't be able to be published
- Run
mix hex.publish
and if all the information are correct replyY
If you have not logged into your Hex.pm accountin your command line before running the above command,you will be met with the following...
No authenticated user found. Do you want to authenticate now? [Yn]
You will need to replyY
and follow the on-screen instructionsto enter your Hex.pm username and password.
After you have been authenticated,Hex will ask you for a local password thatapplies only to the machine you are using for security purposes.
Create a password for thisand follow the onscreen instructions to enter it.
- Now that your package is published you can create a new git tag with the name of the version:
git tag -a 0.1.0 -m "0.1.0 release"
git push --tags
That's it, you've generated, formattedand published your firstElixir
project 🎉
If you want a more detailed exampleof publishing a real-world packageand re-using it in a real-world project,see:code-reuse-hexpm.md
Maps are very similar toObject
literals inJavaScript
.They have almost the samesyntax except for a%
symbol.They look like this:
animal=%{name:"Rex",type:"dog",legs:4}
Values can be accessed in a couple of ways.The first is by dot notation justlikeJavaScript
:
name=animal.name# Rex
The second way valuescan be accessed is by pattern matching.Let's say we wantedto assign values to variablesfor each of the key-value pairs in the map.We would write something that looks like this:
iex>%{name:name,type:type,legs:legs}=animal# we now have access to the values by typing the variable namesiex>name# "Rex"iex>type# "dog"iex>legs# 4
Due to the immutability ofElixir
,you cannot update a mapusing dot notationfor example:
iex>animal=%{name:"Rex",type:"dog",legs:4}iex>animal.name="Max"# this cannot be done!
InElixir
we can only create new data structures as opposed to manipulating existingones. So when weupdate a map, we are creating a new map with our new values.This can be done in a couple of ways:
- Function
- Syntax
- Using a function
We can update a map usingMap.put(map, key, value)
.This takes the map you want to updatefollowed by the key we want to reassignand lastly the value that we wantto reassign to the key:
iex>updatedAnimal=Map.put(animal,:name,"Max")iex>updatedAnimal# %{legs: 4, name: "Max", type: "dog"}
- Using syntax
We can use a special syntax for updating a map in Elixir.It looks like this:
iex>%{animals|name:"Max"}# %{legs: 4, name: "Max", type: "dog"}
NOTE: Unlike the function method above,this syntax can only be used to UPDATEa current key-value pair inside the map,it cannot add a new key value pair.
When looking intoElixir
you may have heard about itsprocessesand its support for concurrency.In fact we even mention processesas one of the key advantages.If you're anything like us,you're probably wonderingwhat this actually means for you and your code.This section aims to help youunderstand what they areand how they can help improveyourElixir
projects.
Elixir-lang
describes processes as:
In
Elixir
, all code runs inside processes.Processes are isolated from each other,run concurrent to one anotherand communicate via message passing.Processes are not only the basisfor concurrency inElixir
,but they also provide the meansfor building distributed and fault-tolerant programs.
Now that we have a definition,let's start by spawning our first process.
Create a file calledmath.exs
and insert the following code:
defmoduleMathdodefadd(a,b)doa+b|>IO.inspect()endend
Now head over to your terminaland enter the following,iex math.exs
.This will load yourMath
moduleinto youriex
session.Now iniex
type:
spawn(Math,:add,[1,2])
You will see something similar to:
3#PID<0.118.0>
This is your logfollowed by aprocess identifier
, PID for short.A PID is a unique id for a process.It could be unique among all processes in the world,but here it's just unique for your application.
So what just happened here.We called thespawn/
3functionand passed it 3 arguments.The module name,the function name (as an atom),and a list of the argumentsthat we want to give to our function.This one line of code spawneda process for us 🎉 🥳
Normally we would not see the resultof the function (3 in this case).The only reason we haveis because of theIO.inspect
in the add function.If we removed this the only log we would have is the PID itself.
This might make you wonder,what good is spawning a process if I can't getaccess to the data it returns.This is where messages come in.
Let's start by exitingiex
and removing the theIO.inspect
linefrom our code.Now that that is donelet's get our add functionto send its result in a message.
Update your file to the following:
defmoduleMathdodefadd(a,b)doreceivedosenders_pid->send(senders_pid,a+b)endenddefdouble(n)dospawn(Math,:add,[n,n])|>send(self())receivedodoubled->doubledendendend
Let's go through all the new code.We have added a new function called double. This function spawns theMath.add/2
function (as we did in theiex
shell in the previous example). Remember thespawn function returned a PID. We use this PID on the next line with|> send(self())
.send/2 takestwo arguments, a destination and a message. For us the destination is the PIDcreated by thespawn
function on the line above. The message isself/0
which returns the PIDof the calling process (the PID of double).
We then callreceive/1
which checks if there is a message matching the clauses in the current process.It works very similarly to acase
statement.Ourmessage
is simple and justreturns whatever the message was.
We have also updated ouradd/2
functionso that it also contains areceive
and asend
.Thisreceive
, receives the PID of the sender.Once it has that messageit calls the send functionto send a message back to the sender.The message it sends isa+b
.
This will trigger the receive block in our double function. As mentionedabove, it simply returns the message it receives which is the answer from add.
Let's test this code iniex
. Change to your terminal and typeiex math.exs
again. Iniex
typeMath.double(5)
.
You should see
10
VERY COOL.We just spawned a processwhich did a task for usand returned the data.
Now that we can create processes that can send messages to each other, let's seeif we can use them for something a little more intensive than doubling aninteger.
In this example we will aim to see exactly how concurrency can be used tospeed up a function (and in turn, hopefully a project).
We are going to do this by solving factorials using two different approaches.One will solve them on a single process and the other will solve them usingmultiple processes.
(A factorial is the product of an integer and all the integers below it;e.g.factorial(4) (4!)
is equal to1 * 2 * 3 * 4
or24
.)
Create a new file calledfactorial.exs
with the following code:
defmoduleFactorialdodefspawn(n)do1..n|>Enum.chunk_every(4)|>Enum.map(fn(list)->spawn(Factorial,:_spawn_function,[list])|>send(self())receivedon->nendend)|>calc_product()enddef_spawn_function(list)doreceivedosender->product=calc_product(list)send(sender,product)endend# used on the single processdefcalc_product(n)whenis_integer(n)doEnum.reduce(1..n,1,&(&1*&2))end# used with multiple processesdefcalc_product(list)doEnum.reduce(list,1,&(&1*&2))endend
The&
symbol is called thecapture operator,which can be used to quickly generate anonymous functions that expect at least one argument.The arguments can be accessed inside thecapture operator&()
with&X
, whereX
refers to the input number of the argument.
There is no difference between
add_capture=&(&1+&2)add_fn=fna,b->a+bend
Before we go any further let's take a quick look at thecalc_product
function.You will see that there are 2 definitions for this function. One which takes alist and another which takes an integer and turns it into a range. Other thanthis, the functions work in exactly the same way. They both call reduce on anenumerable and multiply the current value with the accumulator.
(The reason both work the same way is so that we could see the effect multipleprocesses running concurrently have on how long it takes for us to get theresults of our factorial. I didn't want differences in a functions approachto be the reason for changes in time. Also these factorial functions are notperfect and do not need to be. That is not what we are testing here.)
Now we can test if ourcalc_product
function works as expected. In youriex
shell load theFactorial
module withc("factorial.exs")
. Now typeFactorial.calc_product(5)
. If you get an output of120
then everything isworking as expected and you just solved a factorial on a single process.
This works well on a smaller scale but what if we need/want to work outfactorial(100_000)
. If we use this approach it will take quite some timebefore it we get the answer returned (something we will log a little later).The reason for this is because this massive sum is being run on a singleprocess.
This is where spawning multiple processes comes in. By spawning multipleprocesses, instead of giving all of the work to a single process, we can sharethe load between any number of processes. This way each process is only handlinga portion of the work and we should be able to get our solution faster.
This sounds good in theory but let's see if we can put it into practice.
First, let's look through thespawn
function and try to work out what it isdoing exactly.
The function starts by converting an integer into a range which it then'chunks' into a list oflists with 4 elements.The number 4 itself is not important, it could have been 5, 10, or 1000. What isimportant about it, is that it influences the number of processes we will be spawning.The larger the size of the 'chunks' the fewer processes are spawned.
Next, we map over the 'chunked' range and call the spawn function. Thisspawns a new process for each chunked list running the_spawn_function/1
.Afterwards, the process running thespawn/1
function sends the newlycreated process a message and waits for a response message.
The_spawn_function
function is pretty simple. It uses the exact same patternwe used in ouradd
function earlier. It receives a message with the sendersPID and then sends a message back to them. The message it sends back is theresult of thecalc_product
function.
Once each process in the map function has received a result, we then call thecalc_product
once more to turn the list of results from map into a single integer,the factorial.In total thespawn/1
function will end up callingcalc_product
for a list withn
elements:n % 4 + 1
ifn % 4 == 0
elsen % 4 + 2
times.
Remember, we split the original list into lists of 4 elements.The only exception is the last chunk, which may contain fewer elements:
[1, 2, 3, 4, 5] -> [[1, 2, 3, 4], [5]]
For each chunked list we callcalc_product
and to calculate the final result,the factorial, once.
Now that we have been through the code the only things left are to run the codeand to time the code.
To time the execution of our code,add the following to yourfactorial.exs
file:
# just a helper function used to time the other functionsdefrun(f_name,args)do:timer.tc(Factorial,f_name,args)|>elem(0)# only displays the time as I didn't want to log numbers that could have thousands of digits|>IO.inspect(label:"----->")end
You can feel free to comment out the|> elem(0)
line. I left it in because weare about to have a MASSIVE number log in our terminal and we don't really needto see it.
Now we have all the code we will need. All that's left is to call the code.
Let's go back to ouriex
shell and retype thec("factorial.exs")
command.Now type the followingFactorial.run(:calc_product, [10_000])
. You should geta log of the number of milliseconds it took to run the function.
Next typeFactorial.run(:spawn, [10_000])
. Compare to two logs. You shouldhave something that looks like this...
Feel free to try this test again with even larger numbers (if you don't mind thewait of course).
Note: this isdefinitelynot a "reason" to switch programminglanguages, butone of our (totally unscientific) reasons for decidingtoinvestigate other options for programming languages was the factthat JavaScript (with the introduction of ES2015) now hasSix Ways to Declare a Function:https://rainsoft.io/6-ways-to-declare-javascript-functions/which means that there isambiguity and "debate" as to which is"best practice", Go,
Elixir
and Rust don't suffer from this problem.Sure there are "anonymous" functions in Elixir(required for functional programming!) but there are still onlyTwo Waysto define afunction
(and both have specific use-cases),which isway easier to explain to abeginner than the JS approach.see:http://stackoverflow.com/questions/18011784/why-are-there-two-kinds-of-functions-in-elixir
- Crash Course in Elixir
- Elixir School,which is available translated at least partially in over20 languagesand functions as a great succinct guide to core concepts.
- 30 Days of Elixiris a walk through the
Elixir
language in 30 exercises. - Learn
Elixir
- List of Curated Resources - Explanation video ofPattern Matching in Elixir
- Sign up to:https://elixirweekly.net/ for regular (relevant) updates!
- List of moreuseful resources and sample apps
- If you want to know what'snext it's worth check outWhat's Ahead for Elixir? (53 mins)byJosé Valim (the creator of Elixir)
- Interview withJosé Valim (the creator of Elixir) onwhy he made it!https://www.sitepoint.com/an-interview-with-elixir-creator-jose-valim/
- What was "wrong" with just writing directly in Erlang? read:http://www.unlimitednovelty.com/2011/07/trouble-with-erlang-or-erlang-is-ghetto.html
- While
Elixir
byitself is prettyamazing,where the language really shines is in thePhoenix Web Framework!!So once you know the basics of the languageLearnPhoenix
Web Development.
About
💧 Learn the Elixir programming language to build functional, fast, scalable and maintainable web applications!
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.