Dorian

@iacobson

Crypto Curry with Elixir

(Even More) Functional Elixir — 1

If you (like me) got into Elixir from an object-oriented language, then concepts like currying, function composing and pointfree functions may not be very familiar. Some of them do not apply by default to Elixir. But they are part of the general functional programming (FP) concepts and worth exploring.

I started with this free book recommended by a colleague: Professor Frisby’s Mostly Adequate Guide to Functional Programming. I would recommend it to anybody getting into functional programming. It uses JavaScript and a few libraries to exemplify the concepts. Yet, JS is not my main language, so I’m very curious to see how those apply in Elixir.

Anything those days is about cryptocurrencies. To keep up with the trends let’s build a small crypto price checker. Then we will refactor it using FP concepts.

Initial Implementation

The public function takes the amount in GBP as only argument. It outputs the number of crypto-coins you can buy with.

Example:

buy_crypto_with_gbp(2000) =>
[
BTC: 0.18983617051918897,
ETH: 1.9337893873932355,
LTC: 10.66955239648603
]

The role of the Support module is to communicate with the external APIs and return the exchange rates. You can find the implementation in the Github repo. It exposes 2 public functions:

# exchange rate between 2 currencies
exchange_rate("USD", "GBP") => 0.7463
# exchange rate between crypto and USD
list_crypto_usd() => [BTC: 14256.67, ETH: 1371.76, LTC: 262.51]

There are a few simple actions happening in the code above:

  • get a list of the 3 cryptocurrencies prices in USD
  • get an exchange rate between 2 currencies
  • convert the list of crypto prices from USD to GBP.
  • map the quantity for each crypto-coin

The challenge is to refactor the code above using the functional concepts of currying, composing and pointfree.

Curry, Compose, Pointfree

First of all, I must say that those concepts are not provided with Elixir out of the box. We could implement some of them on our own, but it’s safer and easier to make use of this amazing package: Quark. You may say that we can already compose functions in Elixir with the pipe |> operator. Stay with me, the composing we are going to use is slightly different. Add the Quark package in the mix file, and use Quark at the top of the module.

Currying

Is the ability to call a function that takes many arguments, with one argument at a time. Each time it will return a new function until the last argument is applied and the result is returned.

Quark defines currying with defcurry or defcurryp for private functions.

Let’s start by determining the exchange rage from USD to GBP. Add a wrapper around our Support.exchange_rate/2 function:

defcurry exchange_rate(from, to) do
Support.exchange_rate(from, to)
end

Now you can start to apply the arguments one by one to our new function. You can assign the partial result (which is a function) to a variable. Or it can become a new function definition:

# apply arguments
exchange_rate.("USD").("GBP") => 0.7463
# assign resulting function to a variable
from_usd = exchange_rate.("USD") => #Function<5.65603100/1 in CryptoCurry.exchange_rate/0>
from_usd.("GBP") => 0.7463
# new function definition
def exchange_rate_from_usd do
exchange_rate().("USD")
end
exchange_rate_from_usd.("GBP") => 0.7463

Next step is to apply the conversion above to the cryptocurrencies info we receive {BTC: 14256.67} . We use the same apply_conversion/2 function defined in the Initial implementation, but we just currying it:

defcurry apply_conversion(rate, {crypto, value_usd}) do
{crypto, value_usd * rate}
end

If we follow the same logic as above we can now do the following to the bitcoin value in GBP:

apply_conversion_gbp =
apply_conversion.(exchange_rate_from_usd.("GBP")) => #Function<7.34465968/1 in CryptoCurry.apply_conversion/0>
apply_conversion_gbp.({:BTC, 14265.67}) =>
{:BTC, 10646.469520999999}

Getting a bit confusing? This is where the next concept may come handy.

Composing

The Elixir pipe operator |> takes the output from the expression on its left side and passes it as the first argument to the function call on its right side.

The composing is very similar, only it passes a function as the argument to another function. It can be:

  • right to left — the mathematical way — represented with <|>
  • left to right — the flow way, as the Elixir pipe — represented with <~>

Let’s look at a small example outside our test application:

where rl is right to left, and lr is left to right

In this case:

bazz_rl.("a").("b") == bazz_lr.("a").("b")

Back to the crypto example, we are going to keep close to the FP book and use the mathematical way of composing. We want to define a function that will take the crypto data and convert it to GBP.

def apply_conversion_gbp do
(apply_conversion() <|> exchange_rate_from_usd()).("GBP")
end

apply_conversion will partially apply exchange_rate_from_usd as its first argument. The resulting function will be called with the "GBP" attribute. The result is, of course, another function that can be called with crypto data:

apply_conversion_gbp.({:BTC, 14265.67}) =>
{:BTC, 10646.469520999999}

We are almost there. Need a function that will return the quantity of coin that we can buy with the amount of GBP. We already have that function from the Initial implementation. Just need to curry it.

defcurryp get_quantity({crypto, value_gbp}, amount) do
{crypto, amount / value_gbp}
end

Mapping and composing

If you remember from the Initial example, we were iterating twice over the crypto data list. Once to apply the exchange rate and then to get the quantities:

Support.list_crypto_usd()
|> Enum.map(&apply_conversion(Support.exchange_rate("USD", "GBP"), &1))
|> Enum.map(&get_quantity(&1, amount))

This can be inefficient, but math helps us again here:

map(f2) <|> map(f1) == map(f2 <|> f1)

So we can compose the 2 functions and map them only once.

def convert_gbp_and_get_quantity do
get_quantity() <|> apply_conversion_gbp()
end
defcurry buy_crypto_with_gbp(amount) do
Enum.map(
Support.list_crypto_usd(),
&convert_gbp_and_get_quantity().(&1).(amount)
)
end

Our application is now in working state.

buy_crypto_with_gbp.(2000) =>
[
BTC: 0.19774571082071027,
ETH: 2.0169243958209693,
LTC: 11.090413196189877
]

Pointfree

This last step is a cosmetic change in our case. It was the only reason we curried the buy_crypto_with_gbp/1.

Pointfree is the ability to declare the function without mentioning the arguments. We use the defx Quark package to define a pointfree interface function for our application.

defx buy_crypto_with_gbp do
buy_crypto_with_gbp().()
end

Which we can then call with buy_crypto_with_gbp(2000).

Putting it all together

Quite a long journey, but we’ve made it. Implementing crypto converter using FP concepts.

Back to Elixir

Yet, keeping some of the learnings from the implementation above, we can refactor our Elixir code, without curry or need for additional libraries.

Conclusion

If I would need to choose between CryptoCurry and CryptoElixir implementations above, I would go for the last one. I find the Elixir native way more clear and explicit.

Currying and composing functions can DRY your code. Can help building specialised functions generic ones. Also, the code may look cleaner as function names are shorter, without so many arguments. But on longer term, or for new people in the project, it can be confusing.

Let’s take our example. I see that convert_gbp_and_get_quantity is called with 2 successive arguments. But I cannot be sure if that is just a partial application or not. I don’t know if it returns a final result or another function waiting for more arguments.

In the end, it is important to know that we have the option to use those concepts in Elixir as well. Especially if you have a functional background, you may miss them sometimes.

I hope the post gave you at least some basic idea about curry, composing and freepoint in Elixir.

I would be very curious to find your opinions on Elixir and those FP concepts. If you worked with them in other languages, do you need them in Elixir? If you didn’t, would you consider using them?

Topics of interest

More Related Stories