Elixir for Python programmersThis page attempts to illustrate some of the differences between Elixir and Python. |
|||
Python | Elixir | Notes | |
Overview | |||
Websites | |||
First appeared | 20 February 1991 | May 2012, running on a platform that appeared in 1986. | |
VM | Typically runs on its VM, whether CPython or PyPy. | Runs on the Erlang virtual machine (BEAM). | IronPython and Jython run in foreign VMs. |
Multi-core concurrency | Multi-core execution in the same Python process is possible with threads, but because of the global interpreter lock, only one thread can be executing Python bytecode. Other threads can wait on IO or call into C code, but they cannot run more Python code (exception: pypy-stm). | Multi-core execution is possible using Erlang processes, which are preempted by the Erlang VM. Erlang processes receive and send messsages without sharing data structures. They can be linked into a hierarchy, monitored with tools like etop, and support per-process memory limits. | |
Garbage collection |
|
The Erlang VM gives each Erlang process its own heap. For garbage collection, the Erlang VM uses a "a per-process generational semi-space copying collector using Cheney's copy collection algorithm together with a global large object space". | |
Pattern matching | No general pattern matching except through third-party libraries. |
Pattern matching is built into the VM and can be used with
= ,
case ,
function arguments, lists,
binaries, and more.
|
A pattern matching tutorial in Erlang. |
File extensions |
|
|
When to use .ex and .exs files |
REPL | |||
Start the REPL |
|
|
|
Exit the REPL |
|
|
|
Get last evaluation in REPL |
_ (underscore) |
v() |
|
Get help for a function |
help(func) |
h func |
|
Package and project managers | |||
pip and virtualenv |
mix, which puts dependencies in your project's deps/ folder
|
||
Auditing deps |
pip install executes code in downloaded dependencies without giving you a chance to audit them.
|
mix deps.get downloads but doesn't execute code in packages from Hex, giving you a chance to audit them before compiling (and thus executing) them.
|
|
Types and data structures | |||
Atoms | No direct equivalent |
:some_name is an atom, a constant symbol that does not need pre-declaration.
|
Atoms are not garbage-collected and there is a default maximum of 1,048,576 atoms, so you must be careful not to create a large quantity of atoms dynamically. |
Booleans and null |
True
False
None
|
true
false
nil
|
Elixir true , false , and nil are equivalent to the atoms :true , :false , and :nil .
|
Lists |
[1, 2, 3] is a mutable dynamic array
|
[1, 2, 3] is a linked list
|
|
Tuples |
(1, "2", 3)
|
{1, "2", 3}
|
|
Maps |
{"x": 3, "y": 3.5} is a mutable hash table
|
%{"x" => 3, "y" => 3.5} is an immutable map
|
Large Erlang/Elixir maps use a HAMT (hash-array mapped trie) |
Strings |
|
|
|
Syntax and common operations | |||
Calling |
Function calls require ()
|
Function calls usually don't require () , so IO.puts "hi" and IO.puts("hi") are equivalent.
|
|
Implicit return |
Functions return None if there is no return statement.
|
Functions return the last expression. Elixir has no return statement; instead, use if , case , or cond .
|
|
Named functions | Named functions can be defined at the top-level module scope, in a class, or in another function. |
Named functions can not be defined at the top-level scope (including the REPL) or in another function; they can only be in a defmodule MyModule do
def add(x, y) do
x + y
end
end
MyModule.add(2, 3)
# => 5
|
"It's because there is a bug in Erlang." |
Multiple declarations | Defining another function with the same name will just overwrite the previously defined function. |
Elixir supports defining multiple functions of the same name with different patterns or arities: defmodule MyModule do
def op("double", n) do
n + n
end
def op("square", n) do
n * n
end
def op(n) do
n * n * n
end
end
MyModule.op("double", 10)
# => 20
MyModule.op("square", 10)
# => 100
MyModule.op(10)
# => 1000
|
|
Optional arguments |
Optional arguments must be listed last: def add(x, y=3):
return x + y
add(2)
# => 5
Even though Python's collections and objects are generally mutable, default parameters e.g. |
Optional arguments can be listed at any position, as long as they do not conflict with an existing declaration: defmodule MyModule do
def add(x, y \\ 3) do
x + y
end
end
MyModule.add(2)
# => 5
or defmodule MyModule do
def add(x \\ 10, y) do
x + y
end
end
MyModule.add(2)
# => 12
With both examples above, Elixir defines two separate functions, |
|
Anonymous functions |
square = lambda n: n ** 2
square(10)
# => 100
|
square = fn n -> n * n end
square.(10)
# => 100
|
Why are there two kinds of functions in Elixir? |
print with newline |
print(obj) |
IO.puts(obj) |
|
print w/o newline |
print(obj, end='')
|
IO.write(obj) |
|
repr |
repr(obj)
|
inspect(obj)
|
|
print(repr(obj))
|
IO.inspect(obj) , which also returns obj after printing.
|
||
Pipe operator |
No pipe operator, unless you want to use macropy. In many cases, of course, the returned object has a relevant method for further processing: " hello world ".strip().upper()
# => 'HELLO WORLD'
|
The " hello world " |> String.trim |> String.upcase
# => "HELLO WORLD"
which is equivalent to: String.upcase(String.trim(" hello world "))
# => "HELLO WORLD"
|
|
Exponent |
** exponentiation operator works on both int s/long s and float s
|
No exponentiation operator; :math.pow exists but always returns a float.
|
An integer exponentiation function for Elixir. |
Mapping |
list(n * n for n in [1, 2, 3, 4])
# => [1, 4, 9, 16]
list(map(lambda n: n * n, [1, 2, 3, 4]))
# => [1, 4, 9, 16]
|
for n <- [1, 2, 3, 4], do: n * n
# => [1, 4, 9, 16]
Enum.map([1, 2, 3, 4], fn n -> n * n end)
# => [1, 4, 9, 16]
|
Elixir comprehensions. |
Mapping with a named function |
list(map(str.upper, ["hello", "world"]))
# => ['HELLO', 'WORLD']
|
The function must be captured with the & operator: ["hello", "world"] |> Enum.map(&String.upcase/1)
# => ["HELLO", "WORLD"]
|
|
assert |
assert x == 3, "not 3"
|
Elixir doesn't have an import ExUnit.Assertions, only: [assert: 1, assert: 2]
assert x == 3, "not 3"
You can also "assert" with pattern matching or by pinning a variable with x = 2
^x = 3
# => ** (MatchError) no match of
# => right hand side value: 3
{:ok, val} = {:fail, nil}
# => ** (MatchError) no match of
# => right hand side value: {:fail, nil}
|
|
String operations | |||
strip |
' text \n'.strip()
# => 'text'
|
" text \n" |> String.trim
# => "text"
|
Elixir trim * functions operate on graphemes and strip all Unicode whitespace.
|
rstrip |
' text \n'.rstrip()
# => ' text'
|
" text \n" |> String.trim_trailing
# => " text"
|
|
lstrip |
' text \n'.lstrip()
# => 'text \n'
|
" text \n" |> String.trim_leading
# => "text \n"
|
|
split |
' hello world '.split(' ')
# => ['', 'hello', 'world', '']
|
" hello world " |> String.split(" ")
# => ["", "hello", "world", ""]
|
|
split max=N |
'hello world again'.split(' ', 1) # max splits
# => ['hello', 'world again']
|
"hello world there" |> String.split(" ", parts: 2)
# => ["hello", "world there"]
|
|
upper |
'hello world'.upper()
# => 'HELLO WORLD'
|
"hello world" |> String.upcase
# => "HELLO WORLD"
|
|
lower |
'HELLO WORLD'.lower()
# => 'hello world'
|
"HELLO WORLD" |> String.downcase
# => "hello world"
|
|
Replace all occurences |
'hello world'.replace("l", "x")
# => 'hexxo worxd'
|
"hello world" |> String.replace("l", "x")
# => "hexxo worxd"
|
|
Replace first match |
'hello world'.replace("l", "x", 1)
# => 'hexlo world'
|
"hello world" |> String.replace("l", "x", global: false)
# => "hexlo world"
|
|
Repeat / duplicate |
'*' * 20
# => '********************'
|
"*" |> String.duplicate(20)
# => "********************"
|
|
Count graphemes | Unsupported |
"\u0067\u0308" |> String.length
# => 1
# Note: g̈
|
O(N) in Elixir |
Count codepoints |
len(u"\u0067\u0308")
# => 2
|
"\u0067\u0308" |> String.to_charlist |> length
# => 2
|
O(1) in Python, O(N) in Elixir |
Count bytes |
len(u"\u0067\u0308".encode('utf-8'))
# => 3
|
"\u0067\u0308" |> byte_size
# => 3
|
O(N) in Python with encoding step, O(1) in Elixir |
needle in haystack |
"ello" in "hello world"
# => True
|
"hello world" |> String.contains?("ello")
# => true
|
|
startswith |
'hello world'.startswith('hello')
# => True
|
"hello world" |> String.starts_with?("hello")
# => true
|
|
endswith |
'hello world'.endswith('hello')
# => False
|
"hello world" |> String.ends_with?("hello")
# => false
|