Home > Enterprise >  In Scala, how to summon a polymorphic function applicable to an input type, without knowing the outp
In Scala, how to summon a polymorphic function applicable to an input type, without knowing the outp

Time:09-17

Since Scala 2.12 (or is it 2.13, can' be sure), the compiler can infer latent type arguments across multiple methods:

    def commutative[
        A,
        B
    ]: ((A, B) => (B, A)) = {???} // implementing omitted

val a = (1 -> "a")
val b = commutative.apply(a)

The last line successfully inferred A = Int, B = String, unfortunately, this requires an instance a: (Int, String) to be given.

Now I'd like to twist this API for a bit and define the following function:

def findApplicable[T](fn: Any => Any)

Such that findApplicable[(Int, String)](commutative) automatically generate the correct function specialised for A = Int, B = String. Is there a way to do it within the language's capability? Or I'll have to upgrade to scala 3 to do this?

UPDATE 1 it should be noted that the output of commutative can be any type, not necessarily a Function2, e.g. I've tried the following definition:

trait SummonedFn[-I,  O] extends (I => O) {

    final def summon[II <: I]: this.type = this
}

Then redefine commutative to use it:

    def commutative[
        A,
        B
    ]: SummonedFn[(A, B), (B, A)] = {???} // implementing omitted

val b = commutative.summon[(Int, String)]

Oops, doesn't work, type parameters doesn't get equal treatment like value parameters

CodePudding user response:

If at some point some call-site knows the types of arguments (they aren't actually Any => Any) it is doable using type classes:

trait Commutative[In, Out] {
  def swap(in: In): Out
}

object Commutative {

  def swap[In, Out](in: In)(implicit c: Commutative[In, Out]): Out =
    c.swap(in)

  implicit def tuple2[A, B]: Commutative[(A, B), (B, A)] =
    in => in.swap
}

At call site:

def use[In, Out](ins: List[In])(implicit c: Commutative[In, Out]): List[Out] =
  ins.map(Commutative.swap(_))

However, this way you have to pass both In as well as Out as type parameters. If there are multiple possible Outs for a single In type, then there is not much you can do.

But if you want to have Input type => Output type implication, you can use dependent types:

trait Commutative[In] {
  type Out
  def swap(in: In): Out
}

object Commutative {

  // help us transform dependent types back into generics
  type Aux[In, Out0] = Commutative[In] { type Out = Out0 }

  def swap[In](in: In)(implicit c: Commutative[In]): c.Out =
    c.swap(in)

  implicit def tuple2[A, B]: Commutative.Aux[(A, B), (B, A)] =
    in => in.swap
}

Call site:

// This code is similar to the original code, but when the compiler
// will be looking for In it will automatically figure Out.
def use[In, Out](ins: List[In])(implicit c: Commutative.Aux[In, Out]): List[Out] =
  ins.map(Commutative.swap(_))

// Alternatively, without Aux pattern:
def use2[In](ins: List[In])(implicit c: Commutative[In]): List[c.Out] =
  ins.map(Commutative.swap(_))

def printMapped(list: List[(Int, String)]): Unit =
  println(list)

// The call site that knows the input and provides implicit
// will also know the exact Out type. 
printMapped(use(List("a" -> 1, "b" -> 2)))
printMapped(use2(List("a" -> 1, "b" -> 2)))

That's how you can solve the issue when you know the exact input type. If you don't know it... then you cannot use compiler (neither in Scala 2 nor in Scala 3) to generate this behavior as you have to implement this functionality using some runtime reflection, e.g. checking types using isInstanceOf, casting to some assumed types and then running predefined behavior etc.

CodePudding user response:

I'm not sure I understand the question 100%, but it seems like you want to do some kind of advanced partial type application. Usually you can achieve such an API by introducing an intermediary class. And to preserve as much type information as possible you can use a method with a dependent return type.

class FindApplicablePartial[A] {
  def apply[B](fn: A => B): fn.type = fn
}
def findApplicable[A] = new FindApplicablePartial[A]
scala> def result = findApplicable[(Int, String)](commutative)
def result: SummonedFn[(Int, String),(String, Int)]

And actually in this case since findApplicable itself doesn't care about type B (i.e. B doesn't have a context bound or other uses), you don't even need the intermediary class, but can use a wildcard/existential type instead:

def findApplicable[A](fn: A => _): fn.type = fn

This works just as well.

  • Related