1

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?

anqit
  • 780
  • 3
  • 12

2 Answers2

2

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Clearly I need to sharpen my understanding of implicits. I thought by adding `(using c: Cast[A => A, B => B])` to `MakeTransform`'s `apply` method, that would be enough to make sure calls to `apply` had the appropriate implicit available at the call site. But you are right that this is not how the paper has restrained `inc`. Thank you for the continued help and debugging advice. – anqit Oct 21 '22 at 17:55
  • So playing with this a bit, while this works nicely for `Salary`, it appears `inc` cannot be applied to other argument types without specifying them: `inc(.1)(true)` doesn't compile, but `inc[Boolean](.1)(true)` behaves as expected. I think I need to somehow delay the resolution of `A` until the function returned by `inc` is actually called, will try to play around a bit to figure that out. – anqit Oct 21 '22 at 18:39
2

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.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Hm... No, wait, looking at what Dmytro did there, I might have missed something, because I didn't read it to the end. To extend it for recursions, one probably actually needs that the result of `mkT` is still parameterized by `B` instead of supplying it together with `A` in `mkT[A, B]`. – Andrey Tyukin Oct 21 '22 at 17:32
  • Right, I think the idea is to remain polymorphic in `B` as long as possible. But this is still cool, I'm curious why you went the route of making `Cast` a case class and hang on to `ev`? – anqit Oct 21 '22 at 17:48
  • That's why I structured `MakeTransform`/`mkT` in this way, so that `B` would remain unresolved until `apply` is called, I guess influenced by `shapeless`' polymorphic functions. – anqit Oct 21 '22 at 17:57
  • 1
    @anqit _"why [...] hang on to `ev`"_: I retained the `ev`, because with `Cast[A, B]` instead of `Cast[A => A, B => B]`, I needed two directions: from `A => B` and then back from `B => A`, so I needed `ev.flip`. Both the paper and Dmytro's solution bypass this issue by letting the compiler figure out everything at once while providing `Cast[A => A, B => B]`, which, admittedly, seems more elegant. I didn't read the paper in its entirety, but rather tried to fix the code from your question on my own, with limited success, as it seems. – Andrey Tyukin Oct 21 '22 at 18:08