Motivation

To begin our exploration into Elixir, let us start with a quaint problem: hashing. Given the first name, last name and birth date, our simple algorithm will concatenate them with a hyphen, duplicate thrice it for redundancy and finally shuffle by reversing it once. Using the standard library:

hasher = fn (first_name, last_name, birth_date) ->
  String.reverse(
    String.duplicate(
      Enum.join(
        [first_name, last_name, birth_date],
        "-"),
      3))
end

hasher.("Clara", "Oswald","09/01/2012")
# "2102/10/90-dlawsO-aralC2102/10/90-dlawsO-aralC2102/10/90-dlawsO-aralC"

The hashing function, hasher, is correct despite the lack of entropy or randomness; however, the function itself is not aesthetically pleasing despite the indentation or style. From an editing standpoint, it is hard to change the middle function, String.duplicate, if it changes arity nor is it easy to add a new function in the sequence without mangling parenthesis specially without structured editing capabilities such as paredit or smartparens in your editor. From a reading standpoint, finding the innermost function and working up is cumbersome specially while finding the parameters of each function in the chain. In short, this form or style has its issues despite the standard method in chaining functions.

Pipe Operator

In functional languages such as Haskell or Elixir boast of builtin function composition. In the case of Elixir, we have the pipe operator, |>, which is compactly defined by the documentation as:

This operator introduces the expression on the left-hand side as the first argument to the function call on the right-hand side.

Reflecting it on our motivating problem with some modification:

value = ["Amy", "Pond", "09/01/2012"]
joined_value = Enum.join(value, "-")
duplicated_value = String.duplicate(joined_value, 3)
shuffled_value = String.reverse(duplicated_value)
final_value = shuffled_value

final_value

This operator allows us expressively write it as:

["Amy", "Pond", "09/01/2012"]
|> Enum.join("-")
|> String.duplicate(3)
|> String.reverse()

Since the operator is a macro, it allows us to omit the first argument of each function and assume the results are passed as the first in each chain as promised. Going back on the issues of the original form, it is easy to add and remove functions in the chain as it is simple as adding or removing lines in this new form. Also, reading it from top to bottom is easier to understand at a glance instead of scanning in the middle. This chaining operator is a welcome addition to the coding toolbox and is encouraged by the style guide.

As an example, I want to replace all "/" by "&" before duplicating it. This is quickly done:

["Amy", "Pond", "09/01/2012"]
|> Enum.join("-")
|> String.replace("/", "&") # Or can be added before the Enum.join
|> String.duplicate(3)
|> String.reverse()

Object Oriented Style

This chaining form is simply not a syntactic sugar, it is the functional mindset of composing functions together. By having this operator builtin, it tilts the language as more functional than object oriented. Contrast this with method chaining with object oriented languages such as Java or Python, our example problem in JavaScript would look like this:

String.prototype.duplicate = function (times) {
    return (new Array(times + 1)).join(this);
};

["Amy", "Pond", "09/01/2012"]
    .join("-")
    .replace(/-/g, "&")
    .duplicate(3)
    .reverse();

Notice that the snippet above works because of object prototyping; however, what if that wasn't allowed? The problem with method chaining is that the object returned should conveniently have the method or function to continue the pipeline. If the method is missing, the chain is broken. To demonstrate:

duplicate = function (text times) {
    return (new Array(times + 1)).join(text);
};

duplicate(
    ["Amy", "Pond", "09/01/2012"]
        .join("-")
        .replace(/-/g, "&")
    , 3)
    .reverse();

The intention to transform data with a series of methods or functions exist although lacking. In a way, the pipe operator is a bit more powerful and generic that does not need object methods to chain. Fascinating still, the pipe operator acts like the dot syntax of invoking methods from objects. Like so in Elixir and JavaScript:

"Hello World"
|> String.upcase()
"Hello World"
    .toUpperCase();

For those coming from the object oriented paradigm, these similarities indicate that the pipe operator is a bridge to the functional paradigm and style meant for Elixir. Not a comparison between paradigms but an anchor and operator to better understand and learn. Learning the pipe operator is more than just a syntax sugar but also the data transformation paradigm.

(Optional) Threading Macros

This is an optional section. No need to understand the code or know Emacs Lisp, just the form and possibility.

Elixir does not hold monopoly over the pipe operator. Languages with macro functionality such as Clojure and Racket have threading macros. Although not a definition, threading macros are a step higher than the pipe operator. An issue with the pipe operator is that it can only pass the result as the first argument, what if you wanted to pass it as the last argument or anywhere in the middle? This is troublesome where function design or decisions don't align in the first argument.

The pipe operator is one of the threading macros. As a demonstration, I use Emacs Lisp with dash.el threading macros. The pipe operator or thread first in this library is represented by -> and applying it to our motivating problem:

(require 'dash)

(defun string-duplicate (text times)
  (string-join
   (-map (lambda (_i) text)
           (number-sequence 1 times))
   ""))

(defun string-replace (text from to) ;; Written to fit the pipe operator
  (replace-regexp-in-string from to text))

;;; Without pipe macro
(string-reverse
 (string-duplicate
  (string-replace
   (string-join
    '("Amy" "Pond" "09/01/2012")
    "-")
   "/"
   "&")
  3))

;;; With pipe macro
(-> '("Amy" "Pond" "09/01/2012")
    (string-join "-")
    (string-replace "/" "&")
    (string-duplicate 3)
    (string-reverse))

Seems similar enough but do note that I had to write the duplication function when a standard string utility exist. This library is s.el and we will use its s-repeat function. To apply:

(require 's)

(s-repeat 3 "Me!")               ;;; Me!Me!Me!
;; (string-duplicate "Me!" 3)    ;;; Notice the argument placement

(s-replace "/" "&" "09/01/2012") ;;; 09&01&2012

However, note that the arguments of s-repeat and string-duplicate are reversed thus I cannot simply plug it in with the pipe operator macro. The design decision for s is that the target string argument is the last argument whereas the native libraries have it as the first. To demonstrate the design decision with s, we use the thread last macro, ->>:

(require 's)

;;; Without the thread last macro
(s-reverse
 (s-repeat
  3
  (s-replace
   "/"
   "&"
   (s-join
    "-"
    '("Amy" "Pond" "09/01/2012")))))

;;; With the thread last macro
;;; Will not work with ->
(->> '("Amy" "Pond" "09/01/2012")
     (s-join "-")
     (s-replace "/" "&")
     (s-repeat 3)
     (s-reverse))

Although they look almost the same, fitting functions arguments for the piping macros are tricky. Using unary or single argument functions make it look painless. When functions require two or more arguments, threading them harmoniously involve some juggling or wrapper functions.

With the most generic threading macro, -->, we can choose how the arguments are placed by specifying the position with the symbol it. Another demonstration to show the possibilities of joining different designs:

;;; Notice where =it= is placed
(--> '("Amy" "Pond" "09/01/2012")
     (string-join it "-")
     (s-replace "/" "&" it)
     (s-repeat 3 it)
     (s-reverse it))

Once you have used the pipe operator, the ability to choose how to pipe is quite welcome. For Elixir, we can use the PipeTo library where the syntax is quite familiar in application:

import PipeTo

# Now use ~> and specify position through _
["Amy", "Pond", "09/01/2012"]
~> Enum.join(_, "-")
~> String.replace(_, "/", "&")
~> String.duplicate(_, 3)
~> String.reverse(_)

The example is somewhat contrived since Elixir functions are designed to work with the pipe operator. This suggest that your function design do the same to be consistent and avoid dependencies; however, when the need arises, threading macros allow for more generic and fluent chaining that the pipe operator can afford.

For why the other threading macros are not included by default, the discussion can be found here.