scalaimplicitscrap-your-boilerplatesyb

SYB `mkT` function in Scala


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?


Solution

  • 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.