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:
PartialFunction
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)
Methods
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)
Functors
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.
Monad Transformers
As hcoopz says below (and I've just realized that this would have saved me from writing a ton of unnecessary code), the term "lift" also has a meaning within Monad Transformers. Recall that a monad transformers are a way of "stacking" monads on top of each other (monads do not compose).
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]
Note any collection that extends PartialFunction[Int, A]
(as pointed out by oxbow_lakes) may be lifted; thus for instance
Seq(1,2,3).lift
Int => Option[Int] = <function1>
which turns a partial function into a total function where values not defined in the collection are mapped onto None
,
Seq(1,2,3).lift(2)
Option[Int] = Some(3)
Seq(1,2,3).lift(22)
Option[Int] = None
Moreover,
Seq(1,2,3).lift(2).getOrElse(-1)
Int = 3
Seq(1,2,3).lift(22).getOrElse(-1)
Int = -1
This shows a neat approach to avoid index out of bounds exceptions.
Another usage of lifting that I've come across in papers (not necessarily Scala-related ones) is overloading a function from f: A -> B
with f: List[A] -> List[B]
(or sets, multisets, ...). This is often used to simplify formalisations because it then doesn't matter whether f
is applied to an individual element or to multiple elements.
This kind of overloading is often done declaratively, e.g.,
f: List[A] -> List[B]
f(xs) = f(xs(1)), f(xs(2)), ..., f(xs(n))
or
f: Set[A] -> Set[B]
f(xs) = \bigcup_{i = 1}^n f(xs(i))
or imperatively, e.g.,
f: List[A] -> List[B]
f(xs) = xs map f
There is also unlifting, which is the inverse process to lifting.
If lifting is defined as
turning a partial function PartialFunction[A, B] into a total function A => Option[B]
then unlifting is
turning a total function A => Option[B] into a partial function PartialFunction[A, B]
Scala standard library defines Function.unlift
as
def unlift[T, R](f: (T) ⇒ Option[R]): PartialFunction[T, R]
For example, play-json library provides unlift to help with construction of JSON serialisers:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Location(lat: Double, long: Double)
implicit val locationWrites: Writes[Location] = (
(JsPath \ "lat").write[Double] and
(JsPath \ "long").write[Double]
)(unlift(Location.unapply))
Success story sharing
MonadTrans
instanceT
forM
and aMonad
instance forN
, thenT.liftM
can be used to lift a value of typeN[A]
to a value of typeM[N, A]
.liftM
for that, but didn't manage understand how to do that properly. Guys, you're rock!f
be an instance, notres0
?