Search code examples
scalascala-3hlistpolymorphic-functions

Mapping over generic tuples with polymorphic functions


Scala 3 provides polymorphic functions and Tuples similar to shapeless HList:

scala> 1 *: "foo" *: Tuple()
val res0: (Int, String) = (1,foo)

scala> val f: ([T] => T => Option[T]) = [T] => (v: T) => Some(v)
val f: PolyFunction{apply: [T](x$1: T): Option[T]} = <function1>

scala> res0.map(f)
val res1: Option[Int] *: Option[String] *: EmptyTuple = (Some(1),Some(foo))

How could we reimplement the following shapeless example using Scala 3 functionality?

import poly._

object choose extends (Set ~> Option) {
  def apply[T](s : Set[T]) = s.headOption
}

scala> val sets = Set(1) :: Set("foo") :: HNil
sets: Set[Int] :: Set[String] :: HNil = Set(1) :: Set(foo) :: HNil

scala> val opts = sets map choose
opts: Option[Int] :: Option[String] :: HNil = Some(1) :: Some(foo) :: HNil

In other words, how could we make something like so compile

scala> val choose: ([T] => Set[T] => Option[T]) = [T] => (s: Set[T]) => s.headOption
val choose: PolyFunction{apply: [T](x$1: Set[T]): Option[T]} = <function1>

scala> val sets = Set(1) *: Set("foo") *: Tuple()
val sets: (Set[Int], Set[String]) = (Set(1),Set(foo))

scala> sets.map(choose)
1 |sets.map(choose)
  |         ^^^^^^
  |     Found:    (choose : PolyFunction{apply: [T](x$1: Set[T]): Option[T]})
  |     Required: PolyFunction{apply: [t](x$1: t): Any}

Solution

  • Shapeless map is quite a bit more magical than Scala 3 tuple map, the signature of latter being:

    def map[F[_]](f: [t] => (x$1: t) => F[t]): Map[Tuple, F]
    

    Map[Tuple, F] is a special match type that is basically tuple with every argument type wrapped in F[_]. The issue is that shape, t => F[t] that prevents too much fanciness.

    Except of course, F can be a match type itself:

    type Choose[T] = T match {
      case Set[a] => Option[a]
    }
    
    def choose[T](t: T): Choose[T] = t match
      case set: Set[a] => set.headOption
    
    // messy, but it works
    @main def run =
      val sets = Set(1) *: Set("foo") *: Tuple()
      println(sets.map([T] => (t: T) => choose(t)))
    

    There's currently an issue that compiler won't infer polymorphic function from methods. Match types aren't fully type-safe either, e.g. doing choose("string") will compile but throw a MatchError. I've also ran into problems with inference of a match type from polymorphic function value, hence the usage of def method.