Writing Your Own Sigils

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.

odds/line_sigil.exs
 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:

odds/line_sigil.exs
 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.

Picking Up the Options

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:

odds/color.exs
 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.