1. Introduction

Lifting in Scala refers to different things in different cases, depending on where it’s applied. It’s important to note that there isn’t a general definition.

In this article, we’re going to look at the meaning and usage of lifting in a multitude of cases and explore the advantages it brings.

2. Partial Functions to Functions

Partial Functions are functions applicable to a subdomain of values. However, there might be cases where we want to extend their domain. Lifting, in this case, allows the extension of the function’s domain.

Let’s write a function that prints out the square root of a positive Double. We are going to use this partial function to start with:

val squareRoot: PartialFunction[Double, Double] = {
  case x if x >= 0 => Math.sqrt(x)
}

Keeping the function domain unchanged would force us to check if the function is defined for the value we are calculating the square root for:

def getSqrtRootMessagePartialFunction(x: Double) = {
  if (squareRoot.isDefinedAt(x)) {
    s"Square root of $x is ${squareRoot(x)}"
  } else {
    s"Cannot calculate square root for $x"
  }
}

We can write the same code in a more idiomatic way by extending the domain of our partial function using the lift method:

def getSqrtRootMessageTotalFunction(x: Double) = {
  squareRoot.lift(x).map(result => s"Square root of ${x} is ${result}")
    .getOrElse(s"Cannot calculate square root for $x")
}

Lifting the squareRoot function will extend its domain to the whole Double. From a PartialFunction[Double, Double] it will become a Function[Double, Option[Double]].

Another useful application of lifting to partial functions is to avoid “index out of bound” exceptions:

Seq("one", "two", "three").lift(1) // Some("two")

Seq("one", "two", "three").lift(7) // None

The lifted Seq domain will be extended from String to Option[String]. When accessing a value corresponding to an index out of bounds, the lifted Seq will return a None, rather than an exception.

3. Methods To Functions

There are cases where it is useful to have an easy way of transforming methods into functions. This is another case of lifting. For example, if we have the following methods:

def add5(x: Int) = x + 5
def isEven(x: Int) = x % 2 == 0

We can compose the two methods like so:

isEven(add5(3))

However, if we want to compose them in a functional way, we need a way to transform them into functions. In Scala, we can lift them using the _ symbol:

val funcAdd5 = add5 _
val funcIsEven = isEven _

Now we can easily compose them in a more functional way:

(funcAdd5 andThen funcIsEven)(3)

4. Pure Functions to Effectful Functions: Functors

In a Functor[F], as defined in the Cats documentation, the effect F allows the lifting of a pure function:

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]

  def lift[A, B](f: A => B): F[A] => F[B] =
    fa => map(fa)(f)
}

The operation of taking a function from A => B and putting it into a Functor context, transforming it in a function of F[A] => F[B], is called lifting. We can leverage Functor composition when working with nested data types.

Let’s write a function which calculates the length of the elements of a List[Option[String]]:

def listOptionLength(l: List[Option[String]]): List[Option[Int]] =
  Functor[List].compose[Option].map(l)(_.length)

With Functor composition, we can easily achieve it without the need for unwrapping the different nested data types.

5. Monad to Monad Transformers

Monad transformers are a way of easily combine monads together. Let’s see how the concept of lifting applies to monad transformers.

First, let’s say we have that two Futures:

val sayHello: Future[Option[String]] = Future.successful(Some("Say hello to"))
val firstname: Future[String] = Future.successful("Fabio")

The two Futures we are dealing with have two different domains. This means that when dealing with the results, we have to handle them in an inconsistent way:

def getGreetingsBasic() = {
  val maybeHello: Future[String] = for {
    hello <- sayHello
    name  <- firstname
  // $hello is an Option and need to be unwrapped to get the result
  // $name is already a String
  } yield s"${hello.get} $name"

  Await.result(maybeHello, 1 second)
}

Using monad transformers, when lifting firstName to OptionT, we can have a more consistent way of handling our Futures:

def getGreetingsMonadTranformer() = {
  val maybeHello: OptionT[Future, String] = for {
    hello <- OptionT(sayHello)
    name  <- OptionT.liftF(firstname)
  } yield s"$hello $name"

  val result: Future[Option[String]] = maybeHello.value

  Await.result(result, 1 second)
}

6. Conclusion

In this tutorial, we have seen the concept of lifting implies a transformation of the domain. It is really helpful to remove some of the boilerplate code and write our code in a more idiomatic and composable way. This allows us to concentrate on the business logic of our code rather than on the plumbing of the different parts building it.

The full source code for this article is available over on GitHub.

Comments are closed on this article!