I just wanted to add that lifting a function can be used as an alternative to mapping over a functor. For example, if you had 2 Option[Int]
objects which you wanted to apply f1
to, you could do this:
val sum: Option[Int] = option1.flatMap { x => option2.map{ y => x + y } }
Note that the result is an Option[Int]
. As Alexey Romanov said, the return type of f2
should also be an Option
. The whole point of Option
is to let you do operations on a value without fear of NullPointerException
s or other errors because the value doesn't exist.
However, this mapping is a bit verbose, and it's annoying having to decide when you need to use flatMap
and map
. This is where lifting comes in handy.
Let's define f2
a little better to handle None
s:
def f2(a: Option[Int], b: Option[Int]): Option[Int] =
a match {
case Some(x) => b match {
case Some(y) => Some(x + y)
case None => None
}
case None => None
}
We can also define this in terms of f1
by replacing x + y
with f1(x + y)
def f2(a: Option[Int], b: Option[Int]): Option[Int] =
a match {
case Some(x) => b match {
case Some(y) => Some(f1(x, y))
case None => None
}
case None => None
}
Right now, f2
doesn't need to know anything about how to add numbers, it just uses f1
to add them. We could even make f1
a parameter of f2
, in fact.
def f2(f1: (Int, Int) => Int)(a: Option[Int], b: Option[Int]): Option[Int] =
a match {
case Some(x) => b match {
case Some(y) => Some(f1(x, y))
case None => None
}
case None => None
}
See what happened there? We just used f2
to "lift" f1
from (Int, Int) => Int
to (Option[Int], Option[Int]) => Option[Int]
. Let's rename it lift2
, actually. We could also make it more generic:
def lift2[A, B, C](f1: (A, B) => C)(a: Option[A], b: Option[B]): Option[C] =
a match {
case Some(x) => b match {
case Some(y) => Some(f1(x, y))
case None => None
}
case None => None
}
lift2
is now a function that takes a function of type (A, B) => C
(here, A
, B
, and C
are all Int
for f1
) and returns another function of type (Option[A], Option[B]) => Option[C]
. Now, we don't have to use those awkward nested map
s and flatMap
s. You can just do this:
val sum: Option[Int] = lift2(f1)(option1, option2)
You can also define lift3
, lift4
, etc., of course, but it's probably easier to define just a lift1
function and use currying to do the rest.
Of course, you can only lift
a function if you know how to take apart and put together the type that you are lifting to. For example, if Some
were an object with a private unapply
method, and it were impossible to pattern match on it, you wouldn't be able to lift f1
. The same would happen if the constructor for Some
were private and it were impossible for you to make new Option
s.
Edit: Here's how you can add multiple Option[Int]
objects together with f1
and the lift2
function.
val f2 = lift2(f1)
val optionSum = f2(f2(option1, option2), option3)
Without f2, it'd look something like this
val sum1 = option1 match {
case Some(x) => option2 match {
case Some(y) => Some(f1(x, y))
case None => None
}
case None => None
}
val finalSum = sum1 match {
case Some(x) => option3 match {
case Some(y) => Some(f1(x, y))
case None => None
}
case None => None
}