Search code examples
scalashapelesscurrying

Scala + Shapeless abstract over curried function


I'm trying to figure out how to abstract over a curried function. I've can abstract over an uncurried function via:

def liftAU[F, P <: Product, L <: HList, R, A[_]](f: F)
(implicit
 fp: FnToProduct.Aux[F, L => R],
 gen: Generic.Aux[P, L],
 ap: Applicative[A]
): A[P] => A[R] = p => p.map(gen.to).map(f.toProduct)

This will take a function like (Int, Int) => Int and turn it into something like Option[(Int, Int)] => Option[Int]. And it works for any arity of function.

I want to create the curried version which will take a function like Int => Int => Int and convert it to Option[Int] => Option[Int] => Option[Int].

It should also work for any arity of curried function.

Since FnToProduct only works on the first parameter list, it's not helpful here, I've also tried to write some recursive definitions at the typelevel, but I'm having issues defining the types.

Not really sure if its possible, but would love to know if others have tried anything like this.


Solution

  • Dmytro's answer doesn't actually work for me unless I change the instance names in one of the objects, and even then it doesn't work for a function like Int => Int => Int => Int, and I find working with Poly values really annoying, so instead of debugging the previous answer, I'm just going to write my own.

    You can actually write this operation pretty nicely using a 100% Shapeless-free type class:

    import cats.Applicative
    
    trait LiftCurried[F[_], I, O] {
      type Out
      def apply(f: F[I => O]): F[I] => Out
    }
    
    object LiftCurried extends LowPriorityLiftCurried {
      implicit def liftCurried1[F[_]: Applicative, I, I2, O2](implicit
        lc: LiftCurried[F, I2, O2]
      ): Aux[F, I, I2 => O2, F[I2] => lc.Out] = new LiftCurried[F, I, I2 => O2] {
        type Out = F[I2] => lc.Out
        def apply(f: F[I => I2 => O2]): F[I] => F[I2] => lc.Out =
          (Applicative[F].ap(f) _).andThen(lc(_))
      }
    }
    
    trait LowPriorityLiftCurried {
      type Aux[F[_], I, O, Out0] = LiftCurried[F, I, O] { type Out = Out0 }
    
      implicit def liftCurried0[F[_]: Applicative, I, O]: Aux[F, I, O, F[O]] =
        new LiftCurried[F, I, O] {
          type Out = F[O]
          def apply(f: F[I => O]): F[I] => F[O] = Applicative[F].ap(f) _
        }
    }
    

    It's probably possible to make that a little cleaner but I find it reasonable readable as it is.

    You might want to have something concrete like this:

    def liftCurriedIntoOption[I, O](f: I => O)(implicit
      lc: LiftCurried[Option, I, O]
    ): Option[I] => lc.Out = lc(Some(f))
    

    And then we can demonstrate that it works with some functions like this:

    val f: Int => Int => Int = x => y => x + y
    val g: Int => Int => Int => Int = x => y => z => x + y * z
    val h: Int => Int => Int => String => String = x => y => z => _ * (x + y * z)
    

    And then:

    scala> import cats.instances.option._
    import cats.instances.option._
    
    scala> val ff = liftCurriedIntoOption(f)
    ff: Option[Int] => (Option[Int] => Option[Int]) = scala.Function1$$Lambda$1744/350671260@73d06630
    
    scala> val gg = liftCurriedIntoOption(g)
    gg: Option[Int] => (Option[Int] => (Option[Int] => Option[Int])) = scala.Function1$$Lambda$1744/350671260@2bb9b82c
    
    scala> val hh = liftCurriedIntoOption(h)
    hh: Option[Int] => (Option[Int] => (Option[Int] => (Option[String] => Option[String]))) = scala.Function1$$Lambda$1744/350671260@45eec9c6
    

    We can also apply it a couple more times just for the hell of it:

    scala> val hhhh = liftCurriedIntoOption(liftCurriedIntoOption(hh))
    hhh: Option[Option[Option[Int]]] => (Option[Option[Option[Int]]] => (Option[Option[Option[Int]]] => (Option[Option[Option[String]]] => Option[Option[Option[String]]]))) = scala.Function1$$Lambda$1744/350671260@592593bd
    

    So the types look okay, and for the values…

    scala> ff(Some(1))(Some(2))
    res0: Option[Int] = Some(3)
    
    scala> ff(Some(1))(None)
    res1: Option[Int] = None
    
    scala> hh(Some(1))(None)(None)(None)
    res2: Option[String] = None
    
    scala> hh(Some(1))(Some(2))(Some(3))(Some("a"))
    res3: Option[String] = Some(aaaaaaa)
    

    …which I think is what you were aiming for.