Result Types and Addictive Utility Functions

Robert Cooper
6 min readMar 20, 2018

--

You’ve Been Using the Result Type All Along

Many functional languages have the concept of a Result type. It is a data structure that represents the outcome of a function that could return either a value, or an error. A classic example of this would be the output from a simple HTTP call. The HTTP call either returns the body of the response, or in the case of an error, an error code. While Elixir doesn’t explicitly provide a result type, it does use convention to represent both cases of a Result type as tuples: {:ok, result} and {:error, reason}. The use of these two tuples is widespread and can be found all over the standard library. For example, from the docs for File.read:

File.read("hello.txt")
#=> {:ok, "World"}

File.read("invalid.txt")
#=> {:error, :enoent}

The Problem

These tuples are fantastic, they clearly convey the concept of a Result type, but the problem occurs once you try to do other Elixir-y things with them. Elixir developers love using the pipe operator, and why wouldn’t they? It is a concise way to capture the flow and transformations that an input value goes through on it’s way through your program. Take the following for example:

"World"
|> String.upcase
|> String.slice(1..3)
|> String.duplicate(2)
#=> "ORLORL"

However, the problem with piping the Result tuples through a pipeline quickly becomes apparent — many functions only want the value contained in the {:ok, value} tuple, not the whole tuple itself. And what about the error conditions? Most functions really don't want an error passed to them. Now, this isn't a new problem. Most functional languages include utility functions to help in these situations, but Elixir, strangely, does not. Instead, one common solution is to pattern match on the result type and run different blocks based on the outcome.

def transform_world(str) do
str
|> String.upcase
|> String.slice(1..3)
|> String.duplicate(2)
end

case File.read("hello.txt") do
{:ok, value} -> {:ok, transform_world(value)}
error -> error
end

#=> {:ok, "ORLORL"}

While this isn’t that bad of a solution, especially in this short example, we can do better. I find that as complexity increases; moving between values and result tuples, and pattern matching on those values throughout the code base can get tedious and it tends to cloud the normally concise nature of an elixir project.

Working with result tuples becomes even harder if you want to pipe data through a few functions that may return result tuples themselves:

def api_call1(payload) do
case payload do
{:ok, value} -> Http.get("/api1", value)
error -> error
end
end

def to_payload2(resp1) do
case resp1 do
{:ok, value} -> {:ok, resp1_to_payload2(value)}
error -> error
end
end

def api_call2(payload2) do
case payload2 do
{:ok, value} -> Http.get("/api2", value)
error -> error
end
end

{:ok, 3}
|> api_call1
|> to_payload2
|> api_call2

Now this… this is some pretty stinky code. Each function has to implement the same pattern matching block. Not very DRY. Writing code this way was very common, and in Elixir version 1.2 the with special form was added. Extolling it's virtues you can rewrite the previous code as:

def api_call1(payload) do
Http.get("/api1", value)
end

def to_payload2(resp1) do
resp1_to_payload2(value)
end

def api_call2(payload2) do
Http.get("/api2", value)
end

with {:ok, resp1} <- api_call1({:ok, 3}),
{:ok, payload2} <- to_payload2(resp1),
do: api_call2(payload2)

Hey, that’s much better! The definitions for api_call1 and api_call2 are way simpler now, and to_payload2, in this case, can just get removed and replaced with resp1_to_payload2 (I left it in for clarity). But... I don't know. I guess I'm picky. I really like the pipe operator. The with statement isn't as easy to grok at a glance, and in this case it's just taking the value from one call and sticking it another. This is what the pipe operator is for, but sadly it's not worth the extra code required in the pipeline functions.

This Might be a Better Way

People can argue about whether a function is missing or not from a languages standard library. It happens in every language. But javascript, which I mostly use for my day job, is just flat out missing a standard library. As a result it’s on you to cobble together all the helper and utility functions you want from third-party libraries. One such library, which has been the inspiration behind an Elixir library I created, is called Folktale.js. Folktale provides functions for creating, transforming, and pattern matching Result types (as well as a few other ones). When I switched over to Elixir for my side projects, I realized that Elixir almost had all of those things built in. Except for transforming. So I created a library called Moonsugar to explore the possibility of improving on the above elixir solutions.

Let’s go back over the first example, and see if we can do better with some helper functions from Moonsugar:

# Without Moonsugar
def transform_world(str) do
str
|> String.upcase
|> String.slice(1..3)
|> String.duplicate(2)
end

case File.read("hello.txt") do
{:ok, value} -> {:ok, transform_world(value)}
error -> error
end
#=> {:ok, "ORLORL"}
# With Moonsugar
alias Moonsugar.Result as: MR

File.read("hello.txt")
|> MR.map(&String.upcase/1)
|> MR.map(&(String.slice(&1, 1..3)))
|> MR.map(&(String.duplicate(&1, 2))
#=> {:ok, "ORLORL"}

Here, we use Moonsugars’ function map which applies a function to the value inside of a {:ok, value} tuple. If, however, the map function is supplied with a {:error, reason} tuple, the function is not applied, and the error is passed through:

File.read("no_file_exists_here.txt")
|> MR.map(&String.upcase/1)
|> MR.map(&(String.slice(&1, 1..3)))
|> MR.map(&(String.duplicate(&1, 2))
#=> {:error, "Can not find file"}

Is this method of handling the problem superior? The answer is: I don’t know, maybe? I like that you can pipe functions together without worrying about handling the error condition. But on the other hand, the pipeline has become a tad less readable. All-in-all, I think this is a win.

On to the next solution, the API calls:

# Without Moonsugar
def api_call1(payload) do
Http.get("/api1", value)
end

def to_payload2(resp1) do
resp1_to_payload2(value)
end

def api_call2(payload2) do
Http.get("/api2", value)
end

with {:ok, resp1} <- api_call1(payload1),
{:ok, payload2} <- to_payload2(resp1),
do: api_call2(payload2)
# With Moonsugar
alias Moonsugar.Result as: MR

def api_call1(payload) do
Http.get("/api1", value)
end

def to_payload2(resp1) do
resp1_to_payload2(value)
end

def api_call2(payload2) do
Http.get("/api2", value)
end

payload1
|> MR.chain(&api_call1/1)
|> MR.chain(&to_payload2/1)
|> MR.chain(&api_call2/1)

Here, we use the function chain which is just like map from the previous example, but instead of taking a function that returns a value, it takes a function that returns a result tuple. This lets you... chain together functions that may or may not fail.

So, yay! We got our pipeline back. Is this a better solution? Definitely probably. I never really liked the with. When I was first learning Elixir, I always forgot the commas and it always seemed strange to me that the syntax was so different. I think using the chain function makes the flow of data clearer.

Tons More Stuff

Maybe

There are a few other types that are similar to Result that are used commonly in functional programming. One is the maybe type, which can be represented in Elixir as either {:just, value} or :nothing. This is commonly used to replace values that might be nil. Using a maybe type instead, however, lets you take advantage of several Moonsugar utility functions as well as making it clear what values could contain no value. A couple examples from the library docs:

Maybe.map({:just, 3}, fn(x) -> x * 2 end)
#=> {:just, 6}

Maybe.map(:nothing, fn(x) -> x * 2 end)
#=> :nothing
Maybe.get_with_default({:just, 3}, 0)
#=> 3

Maybe.get_with_default(:nothing, 0)
#=> 0

Validation

The validation type, is another structure that Moonsugar is designed around. In Elixir, it can be represented as either {:success, value} or {:failure, reasons}. This type is very similar to the Result type, but the validation type is designed to represent values that are a collection of failures. For example, a Result type will short-circuit at the first point of failure, while a Validation type is designed to keep going. This is extremely useful for data validation such as in user forms and other inputs. For example, if a user tries to create a password with valid characters, but not a valid length or a capital letter:

user_password
|> MV.concat(&valid_length/1)
|> MV.concat(&valid_chars/1)
|> MV.concat(&has_one_cap/1)
#=> {:failure, ["Not long enough", "Not enough capital letters"]}

Is Moonsugar Worth It?

This is the million dollar question. Adding a library always adds some overhead. You are saying to whoever has to pick up your code “learn this too”. Do you think a library of this style provides enough of a benefit? I’ve been rewriting another one of my libraries with it, and so far I think it has added simplicity and readability to my code base, but I’m biased. Check out the docs on hex and let me know what you think.

--

--