Home > Mobile >  SYB `mkT` function in Scala
SYB `mkT` function in Scala

Time:10-22

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.

  • Related