Lifting

Below is a compileable Scala source file. If you read it from top to bottom, it may help with some insights regarding applicative functors. It was partially inspired by Eric’s rendition of The Essence of the Iterator Pattern.

trait Lift[F[_]] {
  // Spot the pattern in these type signatures
  // of increasing arity.
 
  def lift0[A]:
    A => F[A]
 
  def lift1[A, B]:
    (A => B) => (F[A] => F[B])
 
  def lift2[A, B, C]:
    (A => B => C) => (F[A] => F[B] => F[C])
 
  def lift3[A, B, C, D]:
    (A => B => C => D) => (F[A] => F[B] => F[C] => F[D])
 
  // ... and so on
 
  // The relationship between lift<N> and lift<N-1>
  // can be given by a function,
 
  def ap[A, B]:
    F[A => B] => (F[A] => F[B])
}
 
trait LiftImpl[F[_]] extends Lift[F] {
  // Each lift function uses
  // the previous lift function and ap.
 
  def lift1[A, B]:
    (A => B) => (F[A] => F[B])
    = ap compose lift0
 
  def lift2[A, B, C]:
    (A => B => C) => (F[A] => F[B] => F[C])
    = f => ap compose lift1(f)
 
  def lift3[A, B, C, D]:
    (A => B => C => D) => (F[A] => F[B] => F[C] => F[D])
    = f => a => ap compose lift2(f)(a)
}
 
// Notes
// * lift0 is often called: unit, return, pure, point, η
// * lift1 is often called: fmap, map, ∘
// * lift<N> is often called: liftA<N>, liftM<N>

All that is left to do is to implement the LiftImpl trait! You can do this by implementing the ap and lift0 functions.

Examples of implementations that I know will work out if you try to implement them:

  • class ListLift extends LiftImpl[List]
  • class OptionLift extends LiftImpl[Option]
  • class EitherLift[R] extends LiftImpl[({type λ[α] = Either[R, α]})#λ]
  • class Function1Lift[R] extends LiftImpl[({type λ[α] = R => α})#λ]
  • Those last couple are a bit funky, but a lot of that is syntax noise rather than anything too complicated. Fill out the body of those classes!

    12 Responses to “Lifting”

    1. Charles O'Farrell Says:

      I’m finding it tempting to just use the predefined map/flatMap, but I realise that invalidates the whole exercise. :)

    2. Tony Morris Says:

      Cheater! It’s not a big deal if you use flatMap/map to implement ap. You’ll just be repeating the function body otherwise, so as long as you understand that, you don’t lose anything.

    3. Paulo Says:

      I’m having some trouble with the third one. Possibly I’m not understanding the type correctly, but shouldn’t the following typecheck:

      val el = new EitherLift[Char]
      val f1 = (x: Int) => x + 2
      el.lift1(f1)(Right(13)) // Ok, gives the expected result Right(15)
      el.lift1(f1)(Left(’a')) // Fails to typecheck

    4. Erik Says:

      Thanks Tony! This is short and very sweet, just the way I like it. Really enjoyed this one too, btw: http://blog.tmorris.net/understanding-practical-api-design-static-typing-and-functional-programming.

    5. Tony Morris Says:

      Hey Paulo, You’ll have to type-annotate your Left(’a') value.

    6. Paulo Says:

      Thank you! I did try to annotate with Either[Char, Int] before, but it wasn’t working. My fault though, I had 2.7 installed instead of 2.9, and it seems I can drop the type-annotation entirely.

    7. Kenbot Says:

      So it looks like most of these can be replaced with a class MonadLift[M[_]: scalaz.Monad] extends LiftImpl[M].

      I’m not sure if this can be applied to the Function1 example though. Is it possible?

    8. Channing Walton Says:

      Hi Tony,
      you mentioned that using map/flatmap is cheating. Any chance of a hint how to implement ap without flatmap/map?

      Thanks

      Channing

    9. Tony Morris Says:

      Hi Kenbot, yes a monad gives rise to all of the operations, however, a monad is not necessary. You may weak the interface to an applicative functor, which has some desirable properties that monads do not.

      Channing, you’d just reimplement the bodies of flatMap/map in your definition of ap. If you’re comfortable with knowing how it all works, just use flatMap/map.

    10. Germán Says:

      I tried ListLift and OptionLift, for which I tend to use for comprehensions. But these in turn are based on flatMap/map, right? If there was no flatMap/map, I guess you’d go for imperative iteration and pattern matching. Am I off the mark?

    11. Tony Morris Says:

      Germán, I’d use the next best thing available. Pattern-matching is a last resort, but in the absence of specialised combinators I might just have to resort that low in abstraction.

    12. λ Tony’s blog λ » Blog Archive » Lifting (Haskell addendum) Says:

      [...] follow-on from Lifting. A port of the Scala code to Haskell follows. class Lift f where lift0 :: a -> f a   lift1 [...]

    Leave a Reply