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.