Sometimes when I read articles in the Scala ecosystem I read the term "lifting" / "lifted". Unfortunately, it is not explained what that exactly means. I did some research, and it seems that lifting has something to do with functional values or something like that, but I was not able to find a text that explains what lifting actually is about in a beginner friendly way.
There is additional confusion through the Lift framework which has lifting in its name, but it doesn't help answer the question.
What is "lifting" in Scala?
There are a few usages:
Remember a PartialFunction[A, B]
is a function defined for some subset of the domain A
(as specified by the isDefinedAt
method). You can "lift" a PartialFunction[A, B]
into a Function[A, Option[B]]
. That is, a function defined over the whole of A
but whose values are of type Option[B]
This is done by the explicit invocation of the method lift
on PartialFunction
.
scala> val pf: PartialFunction[Int, Boolean] = { case i if i > 0 => i % 2 == 0}
pf: PartialFunction[Int,Boolean] = <function1>
scala> pf.lift
res1: Int => Option[Boolean] = <function1>
scala> res1(-1)
res2: Option[Boolean] = None
scala> res1(1)
res3: Option[Boolean] = Some(false)
You can "lift" a method invocation into a function. This is called eta-expansion (thanks to @Ben James for this). So for example:
scala> def times2(i: Int) = i * 2
times2: (i: Int)Int
We lift a method into a function by applying the underscore
scala> val f = times2 _
f: Int => Int = <function1>
scala> f(4)
res0: Int = 8
Note the fundamental difference between methods and functions. res0
is an instance (i.e. it is a value) of the (function) type (Int => Int)
A functor (as defined by scalaz) is some "container" (I use the term extremely loosely), F
such that, if we have an F[A]
and a function A => B
, then we can get our hands on an F[B]
(think, for example, F = List
and the map
method)
We can encode this property as follows:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
This is isomorphic to being able to "lift" the function A => B
into the domain of the functor. That is:
def lift[F[_]: Functor, A, B](f: A => B): F[A] => F[B]
That is, if F
is a functor, and we have a function A => B
, we have a function F[A] => F[B]
. You might try and implement the lift
method - it's pretty trivial.
So for example, suppose you have a function which returns an IO[Stream[A]]
. This can be converted to the monad transformer StreamT[IO, A]
. Now you may wish to "lift" some other value an IO[B]
perhaps to that it is also a StreamT
. You could either write this:
StreamT.fromStream(iob map (b => Stream(b)))
Or this:
iob.liftM[StreamT]
this begs the question: why do I want to convert an IO[B]
into a StreamT[IO, B]
?. The answer would be "to take advantage of composition possibilities". Let's say you have a function f: (A, B) => C
lazy val f: (A, B) => C = ???
val cs =
for {
a <- as //as is a StreamT[IO, A]
b <- bs.liftM[StreamT] //bs was just an IO[B]
}
yield f(a, b)
cs.toStream //is a Stream[IO[C]], cs was a StreamT[IO, C]