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` declaration: 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. `y=dict()` are evaluated only when the function is defined, not on every function call. |
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, `MyModule.add/1` and `MyModule.add/2`. |
|
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 `|>` pipe operator works like Clojure's `->` thread-first macro: " 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 `assert` in Kernel, but you can use the one in ExUnit.Assertions: 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 `^`. The downside is that may get a less-informative error on failures: 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 |