Tuesday, February 8, 2022

Functional Programming in Elixir with Witchcraft

While Elixir is a functional programming language, it is different from most of the other popular functional languages like Haskell, Scala, OCaml, and F#.

Elixir pragmatically handles concurrent systems with high fault tolerance. In other words, Elixir is an FP language because this naturally fits it, and not for its own sake. So, porting idioms blindly from Haskell to Elixir can lead to undesired results.

At the same time, Elixir users should more frequently visit the whole wonderful universe of functors, monads, and other curiosities. Good libraries have been introduced for operating with algebraic structures.

In this article, I want to introduce you to a library called Witchcraft and show how you can use it to emulate Haskell-style programming in Elixir.

What is Witchcraft?

Witchcraft is a library written by Brooklyn Zelenka that provides Elixir programmers with predefined abstractions for writing Haskell-like code. Witchcraft takes into account the eccentricities of Elixir, such as the frequent use of the pipe operator.

In other words, Witchcraft is a library for writing Haskell 'fan fiction' in Elixir.

It connects with a few other libraries. All in all, the whole Witchcraft complex consists of four parts:

  • Quark provides some functional building blocks, including flipping arguments of a function in place, a macro for currying functions, and other commonly used things.
  • TypeClass provides a good interface for generating type classes and their instances.
  • Witchcraft implements common algebraic and categorical abstractions, such as monads, functors, etc.
  • Algae implements a domain-specific language (DSL) for creating composite data types like you would in Haskell, and also contains commonly-used composite data types.

Now, let’s try working with the library.

String Validation and Processing with Witchcraft

We will go through a credit card validation exercise.

Given a string that should contain a credit card number, we need to make sure that:

  • it contains 16 digits
  • it has at least two different digits
  • the final digit is even
  • the sum of the digits is more than 16

We will do this exercise with Either, one of the predefined data types that the Witchcraft complex offers.

Set Up Witchcraft

First, add the necessary libraries to your mix.exs file and run mix deps.get.

  defp deps do
    [
      {:witchcraft, "~> 1.0"},
      {:algae, "~> 1.2"},
    ]
  end

Parsing the String

First, make a new module and import Witchcraft at the top of the module.

defmodule Card do
  use Witchcraft
 
end

Then, write a function to parse the string into a list of digits and remove all non-digit items.

def get_digits(cardnumber) do
  cardnumber
  |> String.split("", trim: true)
  |> Enum.map(fn x -> Integer.parse(x) end)
  |> Enum.filter(fn x -> x != :error end)
  |> Enum.map(fn {int, _rest} -> int end
end

In the code above, we split the card number into characters and map Integer.parse (safe integer conversion) over them. Integer.parse returns either {int, rest_of_binary} or :error. We filter out the errors and unpack the tuples with the last two functions.

iex(1)> Card.get_digits("3456-3233-5689-4445")
[3, 4, 5, 6, 3, 2, 3, 3, 5, 6, 8, 9, 4, 4, 4, 5]

After that, we can write our first validation function with Either.

Quick Intro to the Either Data Type

Either is a type that contains one of two different options: Left or Right. Each of these wraps another type. Left stores failures, and Right stores successes.

-- Either data type as defined in Haskell
data Either a b = Left a | Right b

Usually, we return it from computations that can fail when you want to know what kind of error made them fail.

When working with Witchcraft, you can use the predefined Algae.Either to create new instances of the Either data type. We can do that by using one of two structs: %Algae.Either.Left{left: value} or %Algae.Either.Right{right: value}.

Most likely, you've already run into it somewhere. In other languages, it’s sometimes called Result (in Rust, for example). It’s also structurally identical to the result tuple frequently used in Elixir.

{:ok, result} -> {:right, result} -> %Algae.Either.Right{right: result}
{:error, reason} -> {:left, reason} -> %Algae.Either.Left{left: reason}

In fact, we could technically use the result tuple for our exercise. I've constructed it this way on purpose, so you can explore the machinery of Witchcraft while staying on more or less stable ground.

Example of Either

Let’s look at an example of Either.

We have parsed a list of digits from the given string, and now we want to know whether there are 16 items in the list. If we took the easy way out, we could write something like this:

def sixteen_digits(cardnumber), do: Enum.count(cardnumber) == 16

Unfortunately, the function above is not very composable: the return value loses the card number, so we won’t be able to pipe the result to do further computations on it.

Therefore, we want the result to be a more complicated structure such as Either.

  • If there are 16 digits in the list, we want to return an Either.Right with the list.
  • If there is a different number of digits, we want to return an Either.Left with :not_16_digits.

And here’s the function to do that:

def sixteen_digits(cardnumber) do
  case Enum.count(cardnumber) do
    16 -> %Algae.Either.Right{right: cardnumber}
    _ -> %Algae.Either.Left{left: :not_16_digits}
  end
end

Let’s try it out in iex:

iex(1)> right_number = Card.get_digits("3456-3233---5689-4445")
[3, 4, 5, 6, 3, 2, 3, 3, 5, 6, 8, 9, 4, 4, 4, 5]
iex(2)> Card.sixteen_digits(right_number)
%Algae.Either.Right{right: [3, 4, 5, 6, 3, 2, 3, 3, 5, 6, 8, 9, 4, 4, 4, 5]}
iex(3)> wrong_number = Card.get_digits("444")
[4, 4, 4]
iex(4)> Card.sixteen_digits(wrong_number)
%Algae.Either.Left{left: :not_16_digits}

Other Validation Functions

In the same way, we can make a validation rule for each requirement.

I suggest you try doing it yourself first since they will be similar to the first example.

To remind you, here are the conditions we need to check:

  • The number has at least two different digits.
  • The final digit of the number is even.
  • The sum of the digits is more than 16.

Each of the conditions should have its own function. Each of the functions should take a list of numbers and return:

  • %Algae.Either.Right{} with the list inside if the condition is fulfilled
  • %Algae.Either.Left{} with the reason inside if the condition is not fulfilled

If you are ready to proceed, here are the functions that I will use further on in this article:

def two_unique_digits(cardnumber) do
  unique =
    cardnumber
    |> Enum.uniq()
    |> Enum.count()
 
  case unique >= 2 do
    true -> %Algae.Either.Right{right: cardnumber}
    _ -> %Algae.Either.Left{left: :not_2_unique_digits}
  end
end
 
def final_even(cardnumber) do
  last_digit = Enum.at(cardnumber, -1)
 
  case rem(last_digit, 2) do
    0 -> %Algae.Either.Right{right: cardnumber}
    _ -> %Algae.Either.Left{left: :last_not_even}
  end
end
 
def sum_greater_than_16(cardnumber) do
  case Enum.sum(cardnumber) >= 16 do
    true -> %Algae.Either.Right{right: cardnumber}
    _ -> %Algae.Either.Left{left: :sum_smallr_than_16}
  end
end

Connecting the Functions

But how can we join these functions together? Each of them takes a list but returns a struct. We can’t pipe them into each other because their types don’t match up.

To chain them, we need something that knows how to unwrap Algae.Either and apply other functions to what's inside, and Witchcraft has just the thing we need.

definst Witchcraft.Chain, for: Algae.Either.Left do
  def chain(left, _), do: left
end
 
definst Witchcraft.Chain, for: Algae.Either.Right do
  def chain(%Right{right: data}, link), do: link.(data)
end

From Algae.Either.

chain, which can also be written in Witchcraft as >>> (or bind), is the famous bind operator from Haskell: >>=. Let’s understand what it does.

As defined for Either, it will take a value — either Left or Right — and a function from a regular value to Either.

In the case of Left, it will ignore the function and pass the value further. In the case of Right, it will apply the function to the value inside Right.

In our code, we can conveniently use >>> so that it looks pipe-y.

def parse_number(cardnumber) do
  cardnumber
  |> get_digits()
  |> sixteen_digits()
    >>> fn x -> two_unique_digits(x) end
    >>> fn x -> final_even(x) end
    >>> fn x -> sum_greater_than_16(x) end
end

Unfortunately, the capture syntax (&two_unique_digits/1) doesn’t work here. (The macro will expand the code to nested captures, which Elixir doesn’t allow you to do via &.)

Combining the Either data type and its defined chain function lets us build up a chain of functions. Even though they take a list and return an Either, the functions can be chained to arrive at a result. The whole chain will return the list in case of success and the first encountered error in the case of failure.

We can try it out in iex.

iex(1)> Card.parse_number("4444-444-222-1")
%Algae.Either.Left{left: :not_16_digits}
iex(2)> Card.parse_number("4444-4444-4444-4444")
%Algae.Either.Left{left: :not_2_unique_digits}
iex(3)> Card.parse_number("4444-4444-4444-4435")
%Algae.Either.Left{left: :last_not_even}
iex(4)> Card.parse_number("1020-0000-0000-0000")
%Algae.Either.Left{left: :sum_smaller_than_16}
iex(5)> Card.parse_number("4545-3232-5423-6788")
%Algae.Either.Right{right: [4, 5, 4, 5, 3, 2, 3, 2, 5, 4, 2, 3, 6, 7, 8, 8]}

From this point onward, you can claim to have written monadic code in Elixir.

Tinkering With the Codebase

Here are some minor changes we can make to the code so it looks nicer. These are not essential to your understanding but can give you more practice with the code example in question.

First off, Witchcraft provides a very handy ~> operator that does the same as piping into Enum.map.

We can use it in our get_digits function.

def get_digits(cardnumber) do
  cardnumber
  |> String.split("", trim: true)
  ~> fn x -> Integer.parse(x) end
  |> Enum.filter(fn x -> x != :error end)
  ~> fn {int, _rest} -> int end
end

After that, empty lists will cause some run-time errors with the Enum.at that we used, and having empty inputs probably isn’t part of the plan, so we can reject empty strings right at the start.

def get_digits(cardnumber) do
  digits =
    cardnumber
    |> String.split("", trim: true)
    ~> fn x -> Integer.parse(x) end
    |> Enum.filter(fn x -> x != :error end)
    ~> fn {int, _rest} -> int end
 
  case digits do
    [] -> %Algae.Either.Left{left: :empty_input}
    _ -> %Algae.Either.Right{right: digits}
  end
end

And finally, there is a macro that you can use in parse_number to simulate a more Haskell-like way of writing a sequence of actions in a certain context. This style is frequently called the do-notation.

def parse_number(cardnumber) do
 
  monad %Algae.Either.Right{} do
    digits <- get_digits(cardnumber)
    a <- sixteen_digits(digits)
    b <- two_unique_digits(a)
    c <- final_even(b)
    d <- sum_greater_than_16(c)
    return(d)
  end
end

In the code sample above, we provide the kind of container that we will operate with — %Algae.Either.Right{}. Then we can do actions inside it, and the monad macro will automatically chain our functions. In the end, it will return whatever we put in the return statement (but wrapped in the container).

It's very powerful, but also very confusing at first. If it is hard to understand what’s happening here, I encourage you to try out monad [] first since it is very similar to list comprehensions. In fact, if you think of this macro as generalized list comprehensions, you won't err too much.

Either and the Result Tuple in Elixir

Either is very similar to the result tuple in Elixir, and I’ve done that on purpose to keep you on somewhat familiar ground.

In fact, you can implement the same thing without using Witchcraft.

defmodule Card2 do
 
  def get_digits(cardnumber) do
    digits =
      cardnumber
      |> String.split("", trim: true)
      |> Enum.map(fn x -> Integer.parse(x) end)
      |> Enum.filter(fn x -> x != :error end)
      |> Enum.map(fn {int, _rest} -> int end)
  end
 
  def sixteen_digits(cardnumber) do
    case Enum.count(cardnumber) do
      16 -> {:ok, cardnumber}
      _ -> {:error, :not_16_digits}
    end
  end
 
  def two_unique_digits(cardnumber) do
    unique =
      cardnumber
      |> Enum.uniq()
      |> Enum.count()
 
    case unique >= 2 do
      true -> {:ok, cardnumber}
      _ -> {:error, :not_2_unique_digits}
    end
  end
 
  def final_even(cardnumber) do
    last_digit = Enum.at(cardnumber, -1)
 
    case rem(last_digit, 2) do
      0 -> {:ok, cardnumber}
      _ -> {:error, :last_not_even}
    end
  end
 
  def sum_greater_than_16(cardnumber) do
    case Enum.sum(cardnumber) >= 16 do
      true -> {:ok, cardnumber}
      _ -> {:error, :sum_smaller_than_16}
    end
  end
 
  def parse_number(cardnumber) do
    digits = get_digits(cardnumber)
 
    sixteen_digits(digits)
    |> chain(&two_unique_digits/1)
    |> chain(&final_even/1)
    |> chain(&sum_greater_than_16/1)
  end
 
  defp chain({:ok, result}, f), do: f.(result)
  defp chain({:error, _error} = result, _f), do: result
end

Here, we could also have used a with statement, which handles tagged result tuples in a monadic way.

def parse_number_with(cardnumber) do
    with digits <- get_digits(cardnumber),
         {:ok, a} <- sixteen_digits(digits),
         {:ok, b} <- two_unique_digits(a),
         {:ok, c} <- final_even(b),
         {:ok, d} <- sum_greater_than_16(c)
  do
    {:ok, d}
  else
      err -> err
  end
end

But the benefit of Witchcraft is that you get a specific data type with a specific chain function and a large set of types, type classes, and functions, all of which work well with each other to support a particular programming style.

Why Use Witchcraft to Write Haskell-style Elixir?

There are quite a few benefits to using Witchcraft when writing Elixir:

  • Powerful abstractions - for example, we have seen the monad macro, which generalizes over the for and with statements in Elixir. While for and with statements are monadic, they are basically made to handle just a few types (lists or tuples) in a certain way. This does cover a large part of your everyday needs, but there are other wonderful things you could create by applying the same pattern differently once you see it. For example, Haskell's wiki lists nine standard monads.
  • Pre-set patterns of code shared between typed functional programming languages. If you know these patterns, you can basically go to any typed FP language, check the syntax for them, and continue working with essentially no productivity loss. It's the opposite of macros: it works the same way everywhere :)
  • More elegant code - for example, a monadic way of handling tuples lets us escape from writing many ugly nested case statements. While Elixir's with statement already solves this particular problem, there are plenty of other places to apply these tools.

Unfortunately, Elixir isn’t really built for writing code like this. Therefore, using Witchcraft can also result in:

  • A certain amount of unnecessary ceremony
  • Performance penalties
  • Angry stares from people that read your code

In general, the most significant benefit of trying out this programming style in Elixir is to train your programmer brain. Even though your code might not use Witchcraft or exactly follow these patterns as you write Elixir, it is good to know what exists out there.

Summing Up and Further Witchcraft Resources

In this post, we looked at how you can write Haskell 'fan fiction' in Elixir using Witchcraft with a credit card validation code example.

Hopefully, you've seen that Witchcraft contains a ton of stuff, enough to satisfy your curiosity about this kind of programming and more.

But, while it is a good tool for teaching FP concepts, there aren't a lot of tutorials available. Brooklyn Zelenka did a talk about Witchcraft, but there isn't much else out there. If you would like to learn more about this programming style right now, your best bet is to start exploring a language like Haskell, F#, or OCaml (the latter two also have pipes).

Alternatively, you can explore typed BEAM by trying languages like Gleam or Hamler.

Happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!



from Hacker News https://ift.tt/ya6zWtv

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.