You know by now that you can create strings and regular-expression literals using sigils:
| string = ~s{now is the time} |
| regex = ~r{..h..} |
Have you ever wished you could extend these sigils to add your own specific literal types? You can.
When you write a sigil such as ~s{...}, Elixir converts it into a call to the function sigil_s. It passes the function two values. The first is the string between the delimiters. The second is a list containing any lowercase letters that immediately follow the closing delimiter. (This second parameter is used to pick up any options you pass to a regex literal, such as ~r/cat/if.)
Here’s the implementation of a sigil ~l that takes a multiline string and returns a list containing each line as a separate string. We know that ~l… is converted into a call to sigil_l, so we just write a simple function in the LineSigil module.
| defmodule LineSigil do |
| @doc """ |
| Implement the `~l` sigil, which takes a string containing |
| multiple lines and returns a list of those lines. |
| ## Example usage |
| |
| iex> import LineSigil |
| nil |
| iex> ~l\""" |
| ...> one |
| ...> two |
| ...> three |
| ...> \""" |
| ["one","two","three"] |
| """ |
| def sigil_l(lines, _opts) do |
| lines |> String.trim_trailing |> String.split("\n") |
| end |
| end |
We can play with this in a separate module:
| defmodule Example do |
| import LineSigil |
| |
| def lines do |
| ~l""" |
| line 1 |
| line 2 |
| and another line in #{__MODULE__} |
| """ |
| end |
| end |
| |
| IO.inspect Example.lines |
This produces ["line 1","line 2","and another line in Elixir.Example"].
Because we import the sigil_l function inside the example module, the ~l sigil is lexically scoped to this module. Note also that Elixir performs interpolation before passing the string to our method. That’s because we used a lowercase l. If our sigil were ~L{…} and the function were renamed sigil_L, no interpolation would be performed.
The predefined sigil functions are sigil_C, sigil_c, sigil_R, sigil_r, sigil_S, sigil_s, sigil_W, and sigil_w. If you want to override one of these, you’ll need to explicitly import the Kernel module and use an except clause to exclude it.
In this example, we used the heredoc syntax ("""). This passes our function a multiline string with leading spaces removed. Sigil options are not supported with heredocs, so we’ll switch to a regular literal syntax to play with them.
Let’s write a sigil that enables us to specify color constants. If we say ~c{red}, we’ll get 0xff0000, the RGB representation. We’ll also support the option h to return an HSB value, so ~c{red}h will be {0,100,100}.
Here’s the code:
| defmodule ColorSigil do |
| |
| @color_map [ |
| rgb: [ red: 0xff0000, green: 0x00ff00, blue: 0x0000ff, # ... |
| ], |
| hsb: [ red: {0,100,100}, green: {120,100,100}, blue: {240,100,100} |
| ] |
| ] |
| |
| |
| def sigil_c(color_name, []), do: _c(color_name, :rgb) |
| def sigil_c(color_name, 'r'), do: _c(color_name, :rgb) |
| def sigil_c(color_name, 'h'), do: _c(color_name, :hsb) |
| |
| defp _c(color_name, color_space) do |
| @color_map[color_space][String.to_atom(color_name)] |
| end |
| |
| defmacro __using__(_opts) do |
| quote do |
| import Kernel, except: [sigil_c: 2] |
| import unquote(__MODULE__), only: [sigil_c: 2] |
| end |
| end |
| end |
| |
| defmodule Example do |
| use ColorSigil |
| |
| def rgb, do: IO.inspect ~c{red} |
| def hsb, do: IO.inspect ~c{red}h |
| end |
| |
| Example.rgb #=> 16711680 (== 0xff0000) |
| Example.hsb #=> {0,100,100} |
The three clauses for the sigil_c function let us select the colorspace to use based on the option passed. As the single-quoted string ’r’ is actually represented by the list [?r], we can use the string literal to pattern-match the options parameter.
Because I’m overriding a built-in sigil, I decided to implement a __using__ macro that automatically removes the Kernel version and adds our own (but only in the lexical scope that calls use on our module).
The fact that we can write our own sigils is liberating. But misuse could lead to some pretty impenetrable code.