1

Consider this code:

def f1(a: Int, b: Int) = a + b

def f2(a: Option[Int], b: Option[Int]): Int = (a, b) match {
  case (Some(x), Some(y)) => x + y
  case _ => throw new IllegalArgumentException
}

println(f1(10, 20))
println(f2(Some(10), Some(20)))

I heard you can "lift" f1 to be like f2. As a beginner I have the following question:

What is lifting and why it used? In terms of implementation, how can I "lift" f1?

Any explanation is greatly appreciated as it is a bit tricky to find why I would "lift" something

finite_diffidence
  • 893
  • 2
  • 11
  • 20
  • 3
    Note that `f2` isn't really a good function. If it can only operates with **Somes** it shouldn't have asked for **Options**. It would be better if it would return another **Option** instead of throwing an exception. – Luis Miguel Mejía Suárez Jun 24 '20 at 12:48
  • Thanks @LuisMiguelMejíaSuárez, I tried to write a demo function just for this que, but for my real work I will keep what you said in mind – finite_diffidence Jun 24 '20 at 13:06
  • You would lift something to reuse your pure function inside different contexts. You lift a single argument function with a functor and an n-ary function with a composition of applicative functors. You don't need monads for this. –  Jun 24 '20 at 13:46

2 Answers2

3

Why: when you have a function with signature like f1 and want to "call it" on Option[Int]s (or List[Int]s, etc)

How: you could write it directly:

def lift2option[A, B, C](f: (A, B) => C): (Option[A], Option[B]) => Option[C] = ???

I'm leaving it undefined because you should try writing it yourself; your definition of f2 should be a good starting point. Note that I made it return Option[Int] instead of Int. Later I can edit and give the answer if you want.

And then instead of defining f2 as separate function, you do:

val f2 = lift2option(f1 _)
println(f2(Some(10), Some(20)))

Of course, the point is that now for any function with a signature like f1 you can get an f2 equivalent.

It can be further generalized to work not only for Option, but you'll want to look into that later.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Thanks @Alexey, that makes a lot more sense to me now. I kept seeing defintions of Functors and Monads (which I aint learnt yet) but wanted a more basic explanation – finite_diffidence Jun 24 '20 at 13:07
  • 4
    Yes, those are what you need to generalize as mentioned in the last sentence :) To be more precise, functors let you lift functions of 1 argument, and applicative functors (which are between functors and monads) lift functions of 2 or more arguments. – Alexey Romanov Jun 24 '20 at 13:10
2

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 NullPointerExceptions 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 Nones:

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 maps and flatMaps. 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 Options.

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
}
user
  • 7,435
  • 3
  • 14
  • 44
  • Functor/Applicative embodies the idea of lifting a pure function. What do you lift in your examples? Where is the code reuse, which is usually obtained by lifting? –  Jun 24 '20 at 19:56
  • @bob I've lifted `f1`. I've put an example at the end for adding 2 Options, but I can put an example for adding multiple Options if you like – user Jun 24 '20 at 19:59
  • @bob I've updated my answer to include an example of adding 3 `Option[Int]` objects together without lifting `f1` – user Jun 24 '20 at 20:06