Continuing on from a previous question of mine, I am attempting to implement Scrap Your Boilerplate in scala 3 and am running into an issue now with the mkT
function described in the paper. Given the following definition of cast
:
trait Cast[A, B]:
def apply(a: A): Option[B]
object Cast:
given cSome[A, B](using t: A =:= B): Cast[A, B] with
def apply(a: A) = Some(t(a))
given cNone[A, B](using t: NotGiven[A =:= B]): Cast[A, B] with
def apply(a: A) = None
def cast[A, B](a: A)(using c: Cast[A, B]): Option[B] = c(a)
I have tried to make mkT
as follows:
class MakeTransform[A] (val f: A => A) {
def apply[B](b: B)(using c: Cast[A => A, B => B]): B = c(f) match {
case Some(fb) => fb(b)
case _ => b
}
}
def mkT[A](f: A => A): MakeTransform[A] = MakeTransform(f)
And this seems to work fine with the boolean example:
def not(a: Boolean): Boolean = !a
mkT(not)(true) // false, function is clearly called on input value
mkT(not)('a') // 'a'
However, when I try it with the company model objects, I can only get it to function as expected when I provide an explicit type call and the parameter matches that type. So given the following Salary
definition:
sealed trait Salary
case class S(amt: Float) extends Salary
def incS(amt: Float): Salary => Salary = {
case S(a) => S(a * (1 amt))
}
val ralf: Employee = E(P("Ralf", "Amsterdam"), S(8000))
I attempt to raise a Salary
:
inc(.1)(S(8000)) // S(8000) <= no change
Unless, however, I am explicit with the type:
inc(.1)[Salary](S(8000)) // S(8800.0)
But when I do that, I can only pass objects of the specified type as input:
inc(.1)[Salary](ralf) // does not compile
which obviously defeats the purpose.
My thought was, that because MakeTransform
's apply
method takes a type parameter, that the input type would be inferred by the value passed to it, but that doesn't seem to always be the case. Even more baffling to me is the inconsistent behavior between the Boolean
and Salary
examples. Any ideas why? Also, while debugging things like this, is there a way to see what types are being inferred? The debugger shows the runtime type of the variables, but it would be helpful if there was a way to see what type parameters are at runtime.
UPDATE: new thought, does this have to do with the fact that S <: Salary
and not S =:= Salary
?
CodePudding user response:
You seem to again miss an implicit parameter (constraint in Haskell terms)
inc :: Typeable a => Float -> a -> a
-- ^^^^^^^^^^
inc k = mkT (incS k)
Confer
def inc[A](amt: Float): A => A = mkT(incS(amt))(_)
inc(.1)(S(8000)) // S(8000.0) -- not changed
with
def inc[A](amt: Float)(using c: Cast[Salary => Salary, A => A]): A => A = mkT(incS(amt))(_)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
inc(.1)(S(8000)) // S(8800.0) -- changed
The whole code
https://scastie.scala-lang.org/DmytroMitin/v82LGbOtRieGmJX7gCb99A/1
Regarding debugging you can switch on
scalacOptions = Seq("-Xprint:typer", "-Xprint-types")
in build.sbt
.
CodePudding user response:
Your mkT
looks quite different from what's in the paper. Here is my take at it:
import util.NotGiven
case class Cast[A, B](ev: Option[A =:= B])
object Cast:
given cSome[A, B](using t: A =:= B): Cast[A, B] = Cast(Some(t))
given cNone[A, B](using t: NotGiven[A =:= B]): Cast[A, B] = Cast(None)
def cast[A, B](a: A)(using c: Cast[A, B]): Option[B] = c.ev.map(e => e(a))
def mkT[A, B](f: B => B)(a: A)(using c: Cast[A, B]): A =
c.ev match
case Some(aToB) => aToB.flip(f(aToB(a)))
case None => a
def not(a: Boolean): Boolean = !a
println(mkT(not)(true)) // false
println(mkT(not)('a')) // 'a'
sealed trait Salary
case class S(amt: Float) extends Salary
def incS(amt: Float): S => S = {
case S(a) => S(a * (1 amt))
}
def inc[A](k: Float)(a: A)(using c: Cast[A, S]): A = mkT(incS(k))(a)
println(inc(.1)(S(8000))) // increased to `S(8800.0)`
println(inc(.1)('a')) // left as-is
It works just fine when you change the type of incS
from Salary => Salary
to S => S
, because in your case, S
is a subtype of Salary
that's not equal to Salary
.