287

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?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user573215
  • 4,679
  • 5
  • 22
  • 25

4 Answers4

321

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]
starball
  • 20,030
  • 7
  • 43
  • 238
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • 7
    Delving further into **scalaz**, _lifting_ also comes up in relation to _monad transformers_. If I have a `MonadTrans` instance `T` for `M` and a `Monad` instance for `N`, then `T.liftM` can be used to _lift_ a value of type `N[A]` to a value of type `M[N, A]`. – 846846846 Jul 31 '13 at 09:24
  • Perfect! Just one more reason to say: Scala - the best. Which could be lifted to Martin Odersky & Co - the best. I even would to use `liftM` for that, but didn't manage understand how to do that properly. Guys, you're rock! – Dmitry Bespalov Feb 13 '15 at 19:51
  • 3
    In **Methods** section _...res0 is an instance (i.e. it is a value) of the (function) type (Int => Int)..._ Shouldn't `f` be an instance, not `res0`? – srzhio Jan 08 '18 at 21:13
27

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.

elm
  • 20,117
  • 14
  • 67
  • 113
21

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
Malte Schwerhoff
  • 12,684
  • 4
  • 41
  • 71
6

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))
Mario Galic
  • 47,285
  • 6
  • 56
  • 98