Implementing functions returning match-based dependent types in scala 3

Viewed 47

I'm experimenting with describing computations using functional composition in Scala 3. The goal is to be able to write something like this:

def doThat(v: Val[String]): Val[String] =
  v.maybeMap((s) => if s.length < 3 then () else s.capitalize) // fn1
    .map(_.substring(0, 3)) // fn2

The idea is that Val has semantics similar to Option, so fn2 would never be called for arguments shorter than 3 characters because fn1 would return Unit. Note that I don't just use Option instead of Val here because I'm interested in capturing the structure of the computation, not the result of a computation with a given argument, so Val is a placeholder for a value, not an actual value.

The goal is to be able to invoke doThat like this:

enum ValType:
  case Just, Option

class ValImpl[I](val t: ValType, val fn: ((Any) => Any) | Unit, val input: Val[Any] | Unit) extends Val[I]:
  // snipped

v = doThat(ValImpl(ValType.Just, (), ()))

and get a result that looks like ValImpl(Just, fn2, ValImpl(Option, fn1, ValImpl(Just, (), ())))

I defined Val the following way:

trait Val[I]:
  def map[R](fn: (Val.Unwrap[I]) => R): Val.SomeOf[R]
  def maybeMap[R](fn: (Val.Unwrap[I]) => R | Unit): Val.OptionOf[R]

object Val:
  type SomeOf[I] = Val[I]

  type OptionOf[I] = I match
    case Option[i] => I
    case _ => Val[Option[I]]

  type Unwrap[I] = I match
    case Option[i] => i
    case _ => I

The rationale for using dependent types for parameters of map and maybeMap is that if they are invoked on Val[Option[I]], the fn would still be written with I as an argument. Further, maybeMap returns dependent type to avoid returning Val[Option[Option[I]] if invoked on Val[Option[I]].

To this point it all compiles. However, now I am trying to implement ValImpl like this:

case class ValImpl[I](val t: ValType, val fn: ((Any) => Any) | Unit, val input: Val[Any] | Unit) extends Val[I]:
  // helps to make the code more readable
  private def eraseType[P, R](fn: (P) => R) = fn.asInstanceOf[(Any) => Any]
  private def eraseType[P](v: Val[P]) = v.asInstanceOf[Val[Any]]

  def map[R](fn: (Val.Unwrap[I]) => R) = this match
    case _ : ValImpl[Option[I]] => ValImpl(ValType.Option, eraseType(fn), eraseType(this))
    case _ : ValImpl[I] => ValImpl(ValType.Just, eraseType(fn), eraseType(this))

  def maybeMap[R](fn: Unwrap[I] => R | Unit) = this match
    case _ : ValImpl[Option[I]] => ValImpl[Option[R]](ValType.Option, eraseType(fn), eraseType(this))
    case _ : ValImpl[I] => ValImpl[Option[R]](ValType.Option, eraseType(fn), eraseType(this))

I am getting the following compilation errors in method ValImpl.maybeMap:

Test.scala:39:54
Found:    test.ValImpl[Option[R]]
Required: test.Val.OptionOf[R]
    case _ : ValImpl[Option[I]] => ValImpl[Option[R]](ValType.Option, eraseType(fn), eraseType(this))

Test.scala:40:46
Found:    test.ValImpl[Option[R]]
Required: test.Val.OptionOf[R]
    case _ : ValImpl[I] => ValImpl[Option[R]](ValType.Option, eraseType(fn), eraseType(this))

Please can someone explain why the compiler struggles with these lines? I cannot understand why ValImpl[Option[R]] in this case is not also Val.OptionOf[R].

Any suggestions on how to fix this problem in the code are also appreciated.

1 Answers

I found a way past this once I realised that type calculations and the actual runtime behaviour exist independently. Even though the compiler tries to enforce alignment, it is not able to do so all the time, and at times you need to nudge it with .asInstanceOf[]. Perhaps it's completely misguided, but no matter what I tried I was not able to get it to realise that Val.OptionOf[I] is the same as thing as Val[Option[I]].

I also realised that matching by the type parameter in the implementations of map and maybeMap is pointless as that information would not be available at runtime.

So, the working code looks like the following:

case class ValImpl[I](val t: ValType, val fn: ((Any) => Any) | Unit, val input: Val[Any] | Unit) extends Val[I]:
  // helps to make the code more readable
  private def eraseType[P, R](fn: (P) => R) = fn.asInstanceOf[(Any) => Any]
  private def eraseType[P](v: Val[P]) = v.asInstanceOf[Val[Any]]

  def map[R](fn: (Val.Unwrap[I]) => R) =
    ValImpl(ValType.Option, eraseType(fn), eraseType(this)).asInstanceOf[Val.OptionOf[R]]

  def maybeMap[R](fn: Unwrap[I] => R | Unit) =
    ValImpl[Option[R]](ValType.Option, eraseType(fn), eraseType(this)).asInstanceOf[Val.OptionOf[R]]
Related