Drinking more Elixir
I’ve been reading & writing more Elixir; a few things caught my attention.
Bitstring comprehensions aren’t so bad
I originally thought you had to write bitstrings/binaries in Elixir like so:
iex(1)> << 6::size(4), 15::size(4), 6::size(4), 11::size(4) >>
"ok"
That’s one way, but that’s equivalent to:
iex(1)> <<6::4, 15::4, 6::4, 11::4>>
"ok"
There are lots of options you can put in the <<..>>
syntax. <<..>>
is just a macro. You can parse floats directly! So don’t go searching like me for float_to_binary
or binary_of_float
:
iex(1)> <<3.141592653589793::float>>
<<64, 9, 33, 251, 84, 68, 45, 24>>
big-integer != big integers
There’s a gotcha I ran into. Can you guess what this will evaluate to?
iex(1)> <<1024::big-integer>>
???
It’s not <<4, 0>>
. It’s <<0>>
. As it turns out, the big-integer
type means big-endian (and it’s the default), not big integer.
Big-endian:
iex(1)> <<1024::big-integer-size(16)>>
<<4, 0>>
Little-endian:
iex(2)> <<1024::little-integer-size(16)>>
<<0, 4>>
If you want the binary of a big integer, you can either parse the Erlang external term format (which is subject to change every 2 releases!), or you can count the number of bytes (say, n_bytes
) by taking the log256 of the integer and parse it like any fixed-width binary as <<big_integer::size(8*n_bytes)>>
.
Sizes & units
Instead of writing size(16)
or size(8*n_bytes)
, you can guarantee you’re generating a binary (size = 8k) by specifying how many bits a -size(1)
takes up. This way, you’d never accidentally output a 17-bit bitstring when you wanted a binary.
iex(1)> <<1024::big-integer-size(2)-unit(8)>>
<<4, 0>>
List comprehensions
The list/bitstring comprehension syntax in Erlang was kind of separate from the rest of the language. In Elixir, it’s a lot like everything else:
Eshell V7.2.1 (abort with ^G)
1> List = [1, 2, 3, 4, 5].
[1,2,3,4,5]
2> [ Elt * 2 || Elt <- List, Elt rem 2 == 0 ].
[4,8]
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> list = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex(2)> for elt <- list, rem(elt, 2) == 0, do: elt * 2
[4, 8]
Bitstring comprehensions look a bit weird at first, but it’s still a lot like Erlang.
Eshell V7.2.1 (abort with ^G)
1> % A list of integers generated by a bitstring
1> [ 1-Bit || <<Bit:1>> <= <<240>> ].
[0,0,0,0,1,1,1,1]
2> % A list of bitstrings generated by a bitstring
2> [ <<(1-Bit):1>> || <<Bit:1>> <= <<240>> ].
[<<0:1>>,
<<0:1>>,
<<0:1>>,
<<0:1>>,
<<1:1>>,
<<1:1>>,
<<1:1>>,
<<1:1>>]
3> % A bitstring of bitstrings generated by a bitstring
3> << <<(1-Bit):1>> || <<Bit:1>> <= <<240>> >>.
<<15>>
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> # A list of integers generated by a bitstring
iex(2)> for << bit::size(1) <- <<240>> >>, do: 1-bit
[0, 0, 0, 0, 1, 1, 1, 1]
iex(3)> # A list of bitstrings generated by a bitstring
iex(4)> for << bit::size(1) <- <<240>> >>, do: <<(1-bit)::size(1)>>
[<<0::size(1)>>, <<0::size(1)>>, <<0::size(1)>>, <<0::size(1)>>, <<1::size(1)>>,
<<1::size(1)>>, <<1::size(1)>>, <<1::size(1)>>]
iex(5)> # A bitstring of bitstrings generated by a bitstring
iex(6)> for << bit::size(1) <- <<240>> >>, into: <<>>, do: <<(1-bit)::size(1)>>
<<15>>
Up to this point, I prefer the Erlang syntax. But what you gain is the into:
keyword: with it you can generate not just lists and bitstrings but also maps, keyword lists, whatever.
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> # A list comprehension (the into: [] isn't necessary)
iex(2)> for w <- ["alpaca","cat"], into: [], do: String.length w
[6, 3]
iex(3)> # A bitstring comprehension
iex(4)> for w <- ["alpaca","cat"], into: <<>>, do: <<String.length w>>
<<6, 3>>
iex(5)> # A klist comprehension
iex(6)> for w <- ["alpaca","cat"], into: Keyword.new, do: {w, String.length w}
[{"alpaca", 6}, {"cat", 3}]
iex(7)> # A map comprehension
iex(8)> for w <- ["alpaca","cat"], into: Map.new, do: {w, String.length w}
%{"alpaca" => 6, "cat" => 3}
Brilliant! This works for anything that implements Collectable.into/1
, including hash sets, files, IO streams, functions(?), & anything you write yourself. I guess you could write red-black tree comprehensions or binomial heap comprehensions, too; you’re not limited by what’s built into the language.
More on that …, do: … syntax
There are two main ways to write do
blocks. The Ruby-esque syntax is good for multiple lines:
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo do
...(1)> def bar(baz) do
...(1)> baz * 10
...(1)> end
...(1)> end
{:module, Foo, ...}
iex(2)> Foo.bar(12)
120
There’s also the one line version (which you usually wouldn’t use like this, for modules):
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo, do: def bar(baz), do: baz * 10
{:module, Foo, ...}
iex(2)> Foo.bar(12)
120
But remember the bracket-less keyword lists? It’s certainly suspiciously familiar… they’re just missing the (optional) parentheses.
Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Foo, do: def bar(baz), [{:do, baz * 10}]
{:module, Foo, ...}
iex(2)> Foo.bar(12)
120
iex(3)> defmodule Foo, [{:do, ( def bar(baz), [{:do, baz * 10}] )}]
iex:3: warning: redefining module Foo
{:module, Foo, ...}
iex(4)> Foo.bar(12)
120
iex(5)> defmodule( Foo, [{ :do, def( bar(baz), [{:do, baz * 10}] ) }] )
{:module, Foo, ...}
iex(6)> Foo.bar(12)
120
Well that’s certainly interesting. It’s a lot easier to read the version without the extra braces & parens, but it’s the same thing.
Since the bar(baz)
part isn’t evaluatable (it’s not valid code..), you can tell that def
& defmodule
have got to be macros.
You can’t write down every float
Erlang doesn’t support every valid IEEE-754 floating-point value. At least, I haven’t found a way to generate values for +Infinity
, -Infinity
, or NaN
.
You can build/destructure floats just fine:
iex(1)> float64 = fn (sign, exponent, mantissa) ->
...(1)> <<sign::1, exponent::11, mantissa::52>>
...(1)> end
#Function<18.54118792/3 in :erl_eval.expr/5>
iex(2)> one_point_five = float64.(0, 1023, 0x8000000000000)
<<63, 248, 0, 0, 0, 0, 0, 0>>
iex(3)> <<x::float>> = one_point_five
<<63, 248, 0, 0, 0, 0, 0, 0>>
iex(4)> x
1.5
But you can’t build ±Infinity
. According to the spec, they are written
- sign = 0 for positive infinity, 1 for negative infinity
- biased exponent = all 1 bits
- fraction = all 0 bits
iex(5)> positive_infinity = float64.(0, 2047, 0)
<<127, 240, 0, 0, 0, 0, 0, 0>>
iex(6)> <<x::float>> = positive_infinity
** (MatchError) no match of right hand side value: <<127, 240, 0, 0, 0, 0, 0, 0>>
iex(6)> negative_infinity = float64.(1, 2047, 0)
<<255, 240, 0, 0, 0, 0, 0, 0>>
iex(7)> <<x::float>> = negative_infinity
** (MatchError) no match of right hand side value: <<255, 240, 0, 0, 0, 0, 0, 0>>
Oh well. You can’t build a NaN
, either! Again, I’ll quote Wikipedia:
- sign = either 0 or 1
- biased exponent = all 1 bits
- fraction = anything except all 0 bits (since all 0 bits represents infinity)
iex(7)> not_a_number = float64.(0, 2047, 1)
<<127, 240, 0, 0, 0, 0, 0, 1>>
iex(8)> <<x::float>> = not_a_number
** (MatchError) no match of right hand side value: <<127, 240, 0, 0, 0, 0, 0, 1>>
So that’s a bit of a bummer. I’m not sure what it would take to get these values added to the Erlang runtime. Honestly, I’m not sure why they’re not supported already.
Looking at the Erlang external term format
If you run :erlang.term_to_binary
, it’ll serialize any term (including floats) to a binary. Regarding floats:
A float is stored as 8 bytes in big-endian IEEE format.
All binaries-of-terms (of the current version) start with <<131>>
, and the tag for NEW_FLOAT_EXT
is <<70>>
. So, the question is: can you create an Erlang term with the bytes of ±Infinity
?
iex(11)> erlang_one_point_five = <<131, 70>> <> one_point_five
<<131, 70, 63, 248, 0, 0, 0, 0, 0, 0>>
iex(12)> :erlang.binary_to_term erlang_one_point_five
1.5
It certainly works for normal floats.
iex(13)> erlang_positive_infinity = <<131, 70>> <> positive_infinity
<<131, 70, 127, 240, 0, 0, 0, 0, 0, 0>>
iex(14)> :erlang.binary_to_term erlang_positive_infinity
** (ArgumentError) argument error
:erlang.binary_to_term(<<131, 70, 127, 240, 0, 0, 0, 0, 0, 0>>)
But not for all floats. I can’t remember using a language that didn’t have values like NaN
. On the other hand, I’ve gone a pretty long time without needing it, so maybe it’s not as important as I’d think. I probably have used languages without them, but I never knew. I find this.. somewhat bizarre, honestly. I’m sure Joe Armstrong has a story about it.
Atoms are not floats
Don’t be tricked by how Elixir interprets atoms. The following might look like floats:
iex(1)> Infinity
Infinity
iex(2)> NaN
NaN
But they’re not!
iex(3)> is_float(Infinity)
false
iex(4)> is_float(NaN)
false
This is because NaN
and Infinity
are just atoms like any other module:
iex(5)> Infinity == :"Elixir.Infinity"
true
iex(6)> NaN == :"Elixir.NaN"
true
iex(7)> NaNaNaNaNaNaNaNaBatman == :"Elixir.NaNaNaNaNaNaNaNaBatman"
true
Macros are great! but a little verbose
Elixir has really good macro support. The ASTs it generates are not bad at all.
iex(1)> quote do Enum.map %{3 => 10}, fn {k, v} -> {k*2, v/2} end end
{ {:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
[{:%{}, [], [{3, 10}]},
{:fn, [],
[{:->, [],
[[{ {:k, [], Elixir}, {:v, [], Elixir}}],
{ {:*, [context: Elixir, import: Kernel], [{:k, [], Elixir}, 2]},
{:/, [context: Elixir, import: Kernel], [{:v, [], Elixir}, 2]}}]}]}]}
That’s not mind-bending. Maybe a bit unintuitive.
My initial reaction, though, is where’s the backquoting? I’ll quote an example from Andrea Leopardi’s blog (which I’m not criticizing, it’s a good example), just to show what I mean:
iex(1)> defmodule SimpleMacro do
...(1)> defmacro plus(x, y) do
...(1)> quote do: unquote(x) + unquote(y)
...(1)> end
...(1)> end
{:module, SimpleMacro, ..}
iex(2)> quote do 3 + 5 end |> Macro.expand(__ENV__) |> Macro.to_string
"3 + 5"
A very simple macro, perhaps the simplest. Still, it’s a bit of a drag to see past all the quote
s and unquote
s, when I’m really trying to visualize this:
* (defmacro plus (x y) `(+ ,x ,y))
PLUS
* (macroexpand-1 '(plus 3 5))
(+ 3 5)
I miss those commas. I’ll get used to it. (:
Happy hacking!