54

To Pipe or Not to Pipe

 6 years ago
source link: https://www.tuicool.com/articles/hit/amyAB3m
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

The material in this post is based on one of our recent talks at Code BEAM Stockholm. It elaborates on what we covered there on this topic. Though this is a very small part of what it means to program in Elixir, it is something that we stumble across often, so it warrants a post. Here’s our second instalment in the #ElixirOverload series.

There is a time and place for one of Elixir’s most loved pieces of notation: the pipe . It is not always best. There are cases where it can really hinder readability while there are other cases where it can very much aid it. What do these look like?

We all know a variant introduction to the pipe. Perhaps an expression on a collection with successive work performed by the various Enum functions like the map/2, filter/2, and reduce/3.

Take a look at the Elixir script below:

require Integer

0..8
|> Enum.filter(&Integer.is_even/1)
|> Enum.map(&Integer.to_string/1)
|> IO.inspect()

This has some important aspects to it that are too easy to miss. The first being that there are clear subjects to the expression, the initial Range 0..8, and the intermediate collections we are working through. The other being that there are many successive transformations we perform. Together, a clear subject and successive transformations, are one recipe for a good pipe use-case.

The pipe also brings home referential transparency in a way that traditional notation simply cannot. In the below, the values in the comments can be substituted for the the pipeline in the source code up to that point, without changing the behaviour of the program:

require Integer

0..8                               ## [0, 1, 2, 3, 4, 5, 6, 7, 8]
|> Enum.filter(&Integer.is_even/1) ## [0, 2, 4, 6, 8]
|> Enum.map(&Integer.to_string/1)  ## ["0", "2", "4", "6", "8"]
|> IO.inspect()

The other case where we see the pipe in its full utility is with a structure. This is where we might liken it to method chaining in so-called OO languages. It might be most familiar to us through the Plug.Conn structure. This time the expression involves an abstract data-type (the Plug.Conn) with functions that transform and return an updated %Plug.Conn{} rather than the Enum higher-order functions. We still perform successive transformation on the structure.

conn
|> Plug.Conn.put_resp_content_type("text/plain")
|> Plug.Conn.send_resp(200, "Hello!")

That leaves us with the question of when a traditional notation is best. The answer is when there is no clear subject to an expression, few transformations we want to perform, or the transformations in question juggle intermediate data. Expressions that lend themselves to a traditional notation in some cases include mathematical formulae, reading/writing files, and socket I/O. The formula √|x| + 5·x^3 is one example:

:math.sqrt(Kernel.abs(x)) + 5 * :math.pow(x, 3)

In arithmetic the rules of precedence are well known and expected. Thus, that is all we need to read and understand whatever it is that we are calculating. This is true for all sorts of operator expressions including the common arithmetic, boolean, and list operators. A pipe in these cases would only be awkward and hinder readability. The same goes for the following example:

alias :gen_tcp, as: TCP

    {:ok, socket} = TCP.connect(host, port, [:binary, packet: 0] ++ passive())
    :ok = TCP.send(socket, data)
    {:ok, response} = TCP.recv(socket, 0)
    TCP.close(socket)

Of course there is a middle ground where the choice between a pipe and traditional notation makes little difference if at all. It is common in this case that we are piping values of different types through successive transformations. If we want to write a program vertically, as opposed to horizontally, then a pipe is the only good choice to avoid superfluous variables if we do not want to nest function application.

Compare and contrast the following two definitions:

def total(x) do
  x
  |> Keyword.values()
  |> Enum.sum()
end

def total(x) do
  Enum.sum(Keyword.values(x))
end

96 = total(type: 16, length: 16, value: 64)

To conclude, the pipe has great expressive power. It very much aids readability when used appropriately. However there are cases where its use does the opposite. It is likely that traditional notation is more expressive in these cases. Of course, this is a subjective matter, but some thought on the subject can help all of us make the most of Elixir’s syntactic constructs.

I would like to thank Panayiotis Yiasemides, Rafał Studnicki, Claudio Ortolina, Szymon Mentel, Arkadiusz Gil, Radosław Szymczyszyn, Agnieszka Gawda, Zofia Polkowska, and Nasreen Abu-Hunaina for help in writing this post.

If you liked this post then you might also like the other three in our #ElixirOverload series which coincide with our Elixir Architecture sessions. There is the Alchemy 101 series too: we’ve written about howfault tolerance can’t be taken for granted, Mix and Distillery configuration , and the particulars ofmodule attributes. Of course you can find us on social media, reach out at [email protected] , or comment below.

Go back to the blog

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK