Should Elixir functions be curry-able?

I’m (very) new to Elixir and have been poking around to get the feel of the language. Although currying isn’t built in, it is something functional folks do. So I ran across the definition of foldl in the List module (https://hexdocs.pm/elixir/List.html#foldl/3). Why wouldn’t this be defined as fun -> acc -> `a list ? The defintion now lst -> acc -> fun prevents the ‘normal’ currying of foldl (curry foldl and the function, then apply to the initial accumulator and list).

Is there some design pattern associated with Elixir that I should be aware of that motivates the current definition?

1 Like

Welcome to the forum!

FYI:


TL;DNR:

  • The first argument position is targeted by Kernel.|>/2 - this effectively puts the first argument position in the role that is typically filled by the last argument position in other (curried) FP languages.
  • The last argument position often accepts a keyword list of optional values (defaulting to an empty list) - syntax sugar makes the enclosing (list) square brackets optional - the function pretends to be variadic.
4 Likes

I believe that to use currying successfully, the module (like List) would at least need to be re-wrapped. For instance, :list.foldl’s argument order doesn’t lend it self naturally to currying. Typically, foldl’s signaure is something like ('a->'b–>'a) -> 'a -> 'b list -> 'a With this signature, if you had a function like add(a,b) you could use currying to create a new function add_list as follows: (foldl add 0) This isn’t possible with the current library as (for instance) the ‘folding function’ is the last argument in foldl - so you can’t ‘curry’ past it as one might like to do. I understand why the library is as it is. But maybe we should have curry-able interfaces as well?

I think piping comes from Unix - it’s been there ‘forever’

Erlang:

This adheres to our general principle that we do not provide built-in mechanisms; we provide primitives with which mechanisms can be built.

Erlang/Elixir: Beyond Functional Programming with Elixir and Erlang

i.e. The BEAM is primarily a Concurrent Programming platform - Functional Programming was adopted for pragmatic reasons - not as an end in itself.

also:

I tend to think of it not so much as a language with concurrency but more of an operating system with a language.


iex(1)> defmodule Demo do
...(1)>  
...(1)>   def make_foldl(acc, fun) do
...(1)>     fn (list) when is_list(list) ->
...(1)>       List.foldl(list, acc, fun)
...(1)>     end
...(1)>   end
...(1)> 
...(1)> end
{:module, Demo,
 <<70, 79, 82, 49, 0, 0, 5, 48, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 190,
   0, 0, 0, 18, 11, 69, 108, 105, 120, 105, 114, 46, 68, 101, 109, 111, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:make_foldl, 2}}
iex(2)> data = Enum.to_list(1..10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex(3)> add_list = Demo.make_foldl(0, &Kernel.+/2)
#Function<0.107962701/1 in Demo.make_foldl/2>
iex(4)> result = data |> add_list.()
55
iex(5)> IO.puts("#{inspect(result)}")
55
:ok
iex(6)> 

Partial application can be accomplished with closures and it is partial application that is the workhorse (though it seems that currying had a more effective PR-agent).

Currying is an aesthetic nicety - lack of it may make things a bit more verbose but it is hardly a showstopper.

Also concision is often used as an excuse to forego proper naming. Lambdas/Anonymous functions leave the reader to parse the code to determine what is being done in order to divine why it is being done - when a simple (variable/function) name could effectively and painlessly inform the reader about the why.

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (Martin Fowler, Refactoring)

1 Like

Elixir’s function call ordering is extremely pleasing in it’s consistency. Typically if a module defines a datatype its operation will land on the first position. For enumerables, the lambda argument order matches the metafunction order. For example, with enum.reduce it’s enumerable,accumulator and the inner lamba is enumerated, accumulator.

What you’ve provided here is what I was referring to as ‘wrapping’. I think currying has its roots in formal language development and is a direct ‘effect’ of the lambda calculus. If you are trying to do provably correct coding with something like Coq, it has advantages. But, as you’ve shown, there are probably many syntactical equivalences. I think my take away here is that Elixir, as you say, is an OS with a language that has borrowed from functional languages. I think the ‘when in Rome’ dictum makes a lot of sense and what I’ll do with Elixir going forward.

I personally don’t see the need of currying if a language supports partial application well.

Elixir supports partial application either via a wrapping anonymous function like:

fn a, b -> something(1, a, 2, b) end

Or inline with &:

&something(1, &1, 2, &2)

Though the thing of separate environments between functions and values means you have to call it with .(...) instead of just (...), which is a bit less user friendly for partial application.

The main reason why currying is not available in Erlang/Elixir is fact that this these languages allow overriding functions, or differently, there can be few different functions with the same name but different arity. So for sake of this argument assume that Enum.sort/2 function takes sorting function as first argument to allow currying in form of:

sort_by_foo = Enum.sort(fn a, b -> a.foo >= b.foo end)

How would you detect that this is not in fact call to the Enum.sort/1? Languages with built in currying (like Haskell) do not allow multiple functions with different arguments count.

What you’ve provided here is what I was referring to as ‘wrapping’.

If the parameters are fixed at compile-time, you can use macros to generate static functions (or just plain inline code) at compile-time. Sometimes the resulting code may not be as readable.

I think currying has its roots in formal language development and is a direct ‘effect’ of the lambda calculus.

Haskell book, p.10:

Each lambda can only bind one parameter and can only accept one argument. Functions that require multiple arguments have multiple, nested heads. When you apply it once and eliminate the first (leftmost) head, the next one is applied and so on. This formulation was originally discovered by Moses Schönfinkel in the 1920s but was later rediscovered and named after Haskell Curry and is commonly called currying.

So currying was originally a workaround in lambda calculus on a self-imposed constraint to emulate functions with an arity higher than 1.

5 Likes