tarao / record4s

Extensible records for Scala
https://tarao.orezdnu.org/record4s/
Apache License 2.0
63 stars 4 forks source link

Does not work in inline functions #88

Open tschuchortdev opened 5 months ago

tschuchortdev commented 5 months ago

It seems to me that record4s does not work correctly neither in inline nor transparent inline functions (the difference is important since they are inlined at different times iirc). Probably this is an issue with how the type checking of transparent functions works, so not strictly a bug in this library, but perhaps you can find a better workaround or escalate the issue to the Scala compiler. In any case, I hope that my report will at least serve as documentation for other users.

import com.github.tarao.record4s.%

%(name = "hello") + (bla = 123)

def foo() = {
  %(name = "hello") + (bla = 123)
}

inline def bar() = {
  %(name = "hello") + (bla = 123)
}

transparent inline def bob() = {
  %(name = "hello") + (bla = 123)
}

inline def foobar() = {
  (
    %(name = "hello").asInstanceOf[% { val name: String }]
      + (bla = 123)
  ).asInstanceOf[% { val name: String; val bla: Int }]
}

foobar()

Output:

val res0: com.github.tarao.record4s.%{val name: String; val bla: Int} = %(name = hello, bla = 123)

def foo(): com.github.tarao.record4s.%{val name: String; val bla: Int}

-- [E008] Not Found Error: -----------------------------------------------------
2 |  %(name = "hello") + (bla = 123)
  |  ^^^^^^^^^^^^^^^^^^^
  |value + is not a member of Any, but could be made available as an extension method.
  |
  |One of the following imports might make progress towards fixing the problem:
  |
  |  import math.Fractional.Implicits.infixFractionalOps
  |  import math.Integral.Implicits.infixIntegralOps
  |  import math.Numeric.Implicits.infixNumericOps
  |
1 error found

-- [E008] Not Found Error: -----------------------------------------------------
2 |  %(name = "hello") + (bla = 123)
  |  ^^^^^^^^^^^^^^^^^^^
  |value + is not a member of Any, but could be made available as an extension method.
  |
  |One of the following imports might make progress towards fixing the problem:
  |
  |  import math.Fractional.Implicits.infixFractionalOps
  |  import math.Integral.Implicits.infixIntegralOps
  |  import math.Numeric.Implicits.infixNumericOps
  |
1 error found

def foobar(): com.github.tarao.record4s.%{val name: String; val bla: Int}

val res1: com.github.tarao.record4s.%{val name: String; val bla: Int} = %(name = hello, bla = 123)

PS: Let me just say that this is an amazing library that doesn't have nearly as many stars as it deserves. It is not only surprisingly usable, but also serves as a good example of whitebox macro techniques 👍

tarao commented 4 months ago

Aha, I didn't know this behavior but it seems that this is the semantics of inlining.

See this: https://github.com/scala/scala3/issues/12754

I agree that I should mention this in the documentation.

Possible workarounds (work only for transparent inline methods)

1. Define the record outside the method

      val r = %(name = "hello")

      transparent inline def bob() = {
        r + (bla = 123)
      }

      val r1 = bob()
      r1.name
      r1.bla

2. Define the record as an argument

      transparent inline def bob[R <: %](r: R = %(name = "hello")) = {
        r + (bla = 123)
      }

      val r1 = bob()
      r1.name
      r1.bla

Note: this is a restricted version of generic record concatenation.