Result Types and Addictive Utility Functions

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"
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"}
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
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)

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.

# 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"}
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"}
# 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)

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.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store