As you most likely already know, Clojure is a reincarnation of Lisp on the JVM. It is a functional, dynamic language that promotes functions as the primary unit and as such has some interesting functions for deal with, well, functions. apply
is one of those methods. At first glance apply
is a tad confusing and the documentation doesn't help a whole lot either...
([f args] [f x args] [f x y args] [f x y z args] [f a b c d & args]) Applies fn f to the argument list formed by prepending intervening arguments to args.
For some background, being a dynamic language Clojure allows a type of method overloading based on function arity (the number of arguments). If you've ever used Erlang or Elixir this should be a familiar concept. The first line of the documentation specifies that the first argument is always f
which is a function to be applied. The last argument args
is also specified to be the thing which applied. So what are x
, y
, z
, etc.? Trying apply out in the console might make it clearer.
Clojure collections have a conj
(conjoin) method which behaves similar to Scala's :+
.
1
Seq(1, 2, 3) :+ 4 // => Seq(1, 2, 3 4)
1
(conj [1 2 3] 4) ;; => [1 2 3 4]
We can use the conj
method to understand apply
a bit better...
1
2
(conj [1 2 3] [4 5 6]) ;; => [1 2 3 [4 5 6]]
(apply conj [1 2 3] [4 5 6]) ;; => [1 2 3 4 5 6]
In this use of apply
, conj
is the f
argument (the function being applied), [1 2 3]
is the x
argument, and [4 5 6]
are the args being used for the application of conj
. This shows us what is happening: apply
takes a function and applies it with the given arguments for every argument of the list of args at the end. Without apply
we got nested lists, where as with apply
we got one list. That tells us apply
has a kind of accumulation or fold-like effect.
This clearly can be reproduced in Scala.
As with most things Scala it's helpful to start with a type signature. In this case it's worth considering the type signature of conj
so that we can imagine how apply
might use it.
1
def conj[A](seq: Seq[A], a: A): Seq[A] = seq :+ a
One thing to notice here, and this is relevant to our implementation of apply
, the first argument type is the same as the return type. We know we want to take in a function. To limit scope initially we'll only worry about Function2
and try to reproduce specifically the conj
example in Scala.
1
def apply[A,B,C](f: (A,B) => C, x: A, args: B*): C = ???
This is a start. It lets us take in a function f
which has two arguments, one which we will capture as x: A
, and a second which we will get a list of as args: B*
. However this is missing our rule, that the first argument (x
) must also be the same as the return type.
1
def apply[A,B](f: (A,B) => A, x: A, args: B*): A = ???
This now let's us chain the result of each application together. We could implement this recursively or we could use an iterator. In this case foldLeft
would allow us to start with the initial value x
and reduce the arguments down to a final return value of A
.
1
2
def apply[A,B](f: (A,B) => A, x: A, args: B*): A =
args.foldLeft(x) { case (acc, b) => f(acc,b) }
So as you can see, Clojure's apply
is really just a different way to invoke what Scala calls foldLeft
.
In use in Scala this becomes a bit tricky because we can't just pass in conj
because Scala won't treat it as a function by default...
1
2
3
4
5
scala> apply(conj, Seq(1, 2, 3), 4, 5, 6)
<console>:10: error: type mismatch;
found : (Seq[Nothing], Nothing) => Seq[Nothing]
required: (Seq[Int], Int) => Seq[Int]
apply(conj, Seq(1, 2, 3), 4, 5, 6)
The _
is required after the function to signal that we're talking about the function conj
not attempting to call it. Also, you'll notice that Scala's type inference is detecting conj
as (Seq[Nothing], Nothing) => Seq[Nothing]
. Apparently the type inference can't figure out we're dealing with Int
's so that will need to be specified as well.
1
2
apply((conj[Int] _), Seq(1, 2, 3), 4, 5, 6)
res2: Seq[Int] = List(1, 2, 3, 4, 5, 6)
While this might not translate directly over to Scala as being something useful in it's current form, the concept is interesting. The idiomatic way of writing this in Scala would be to create a new partial function and just use the foldLeft
off the list of arguments.
1
Seq(4, 5, 6).foldLeft(Seq(1, 2, 3))(_ :+ _)