Tuples are Different to Function Arguments in Scala
In Scala Tuples and function arguments look similar but they can’t be used interchangeably.
A Tuple of two Ints can be defined as:
> val t2 = (2, 4)
scala: (Int, Int) = (2,4) t2
Given a simple sum method that takes in two Ints:
def sum(n1: Int, n2: Int) = n1 + n2
you might think that you could simply pass in your tuple t2 to invoke the sum method:
> sum(t2)
scala<console>:15: error: not enough arguments for method sum: (n1: Int, n2: Int)Int.
.
Unspecified value parameter n2sum(t2)
Unfortunately this does not work as you can’t simply replace an argument list with a similarly-sized tuple. t2 is taken as the first argument n1, hence the error indicating that n2 has not been supplied.
One way to get this to work is to do the following:
> sum _ tupled t2
scala: Int = 6 res0
Let’s break that incantation down step-by-step to make it more digestible.
- Convert the sum method into a Function:
> val f1 = sum _
scala: (Int, Int) => Int = $$Lambda$1447/998900406@31452c9 f1
- Convert the function into its tupled variant:
> val f2 = f1.tupled
scala: ((Int, Int)) => Int = scala.Function2$$Lambda$227/234698513@3f891cfe f2
Tupling the sum function is merely going from (Int, Int) => Int
to ((Int, Int)) => Int
. Notice the extra parenthesis around the arguments.
- Apply the tupled function to the tupled input t2:
> f2(t2)
scala: Int = 6 res21
Looking back that does look very difficult but it’s not very intuitive.
Using Underscores with Currying
I had a similar problem recently where I had a contains method defined as:
def contains[A](values: List[A], value: A, pred: A => A => Boolean): Boolean = {
.exists(pred(value))
values}
And a List l1 defined as:
val l1 = List(1, 2, 3)
I tried to call the contains method using underscores for the values of the pred parameter and got the following error:
> contains[Int](l1, 3, _ == _)
scala<console>:17: error: missing parameter type for expanded function ((x$1: <error>, x$2) => x$1.$eq$eq(x$2))
[Int](l1, 3, _ == _)
contains^
<console>:17: error: missing parameter type for expanded function ((x$1: <error>, x$2: <error>) => x$1.$eq$eq(x$2))
[Int](l1, 3, _ == _) contains
You can use underscores to represent positional arguments in an argument list where you don’t need to bind it to a name. So why did this fail?
I can get the contains method to work with:
> contains[Int](l1, 3, x => y => x == y)
scala: Boolean = true res24
Conversely, why did this work?
Another interesting variant is if I change the definition of contains to contains2 that takes in an uncurried pred function:
def contains2[A](values: List[A], value: A, pred: (A, A) => Boolean): Boolean = {
.exists(pred(value, _))
values}
I can invoke it with the underscore syntax:
> contains2[Int](l1, 3, _ == _)
scala: Boolean = true res59
One of the main reasons for using a curried version of pred was that I could partially apply it with the exists method on List without having to use underscores to convert the method to a function. I can still achieve the same result by currying pred where it is applied:
def contains3[A](values: List[A], value: A, pred: (A, A) => Boolean): Boolean = {
.exists(pred.curried(value))
values}
The reason I couldn’t use underscores to represent the parameters of the contains method is that the curried function pred, represents two argument lists; One that takes an A
and returns another function that takes another A
and returns a Boolean:
(A) => (A => Boolean)
Underscores can only used to represent positional arguments of a single argument list, since we have two in the curried variation of pred in contains we can’t use it.
Changing the shape of the Input Function
If I define a uncurried function isEqual as:
def isEqual[A](a1: A, a2: A): Boolean = a1 == a2
I can call contains2 as:
> contains2[Int](l1, 3, isEqual)
scala: Boolean = true res32
If I define an isEqual2 as:
def isEqual2[A]: A => A => Boolean = a1 => a2 => a1 == a2
I can call contains as:
> contains[Int](l1, 3, isEqual2)
scala: Boolean = true res33
But if I try to call contains2 with isEqual2 we get:
> contains2[Int](l1, 3, isEqual2[Int])
scala<console>:18: error: type mismatch;
: Int => (Int => Boolean)
found : (Int, Int) => Boolean
required[Int](l1, 3, isEqual2[Int]) contains2
And we can fix that by uncurrying isEqual2:
> contains2(l1, 3, Function.uncurried(isEqual2))
scala: Boolean = true res65
If we define isEqual3 with a Tuple2 as:
def isEqual3[A]: Tuple2[A, A] => Boolean = t => t._1 == t._2
And we try to invoke contains2 with isEqual3 we get:
> contains2(l1, 3, isEqual3[Int])
scala<console>:15: error: type mismatch;
: ((Int, Int)) => Boolean
found : (?, ?) => Boolean
requiredcontains2(l1, 3, isEqual3[Int])
And we can easily fix that by untupling the parameters to isEqual3:
> contains2(l1, 3, Function.untupled(isEqual3))
scala: Boolean = true res69
Case Class Constructors
And one last example invoking a constructor of a case class:
> case class Person(name: String, age: Int)
scalaclass Person
defined
> val nameAge = ("Katz", 20)
scala: (String, Int) = (Katz,20)
nameAge
> val pc = Person.apply _
scala: (String, Int) => Person = $$Lambda$1565/1849401610@5417f849 pc
If I try to invoke pc with nameAge I get an error as expected:
> pc nameAge
scala<console>:13: error: value nameAge is not a member of (String, Int) => Person
pc nameAge
And we can solve that by tupling the constructor:
> pc tupled nameAge
scala: Person = Person(Katz,20) res21
Or more succinctly:
> Person.tupled(nameAge)
scala: Person = Person(Katz,20) res22
Hopefully this has given you some insight into the various ways to invoke functions that takes tuples, curried arguments or uncurried variants.
Some references: