JetDim - Handle `dim > 0` Check at Compile-time?

ยอดดู 47 ครั้ง
ข้ามไปที่ข้อความที่ยังไม่อ่านรายการแรก

Kevin Meredith

ยังไม่อ่าน,
27 เม.ย. 2558 11:54:1127/4/58
ถึง spire...@googlegroups.com
Looking at `JetDim.scala`'s JetDim:

case class JetDim(dimension: Int) {
  require(dimension > 0)
}

Could this check be handled at compile-time? If so, how would you do it?

I saw in the SD 2015 talk (https://www.parleys.com/tutorial/scalactic-way) that Bill Venners resolved a constraint (such as the above) at compile-time.

Note - I'm asking for my own learning. Once I finish the ScalaCheck work (on Jet now), I'd be interested to work on this if that's OK.

Thanks.

Erik Osheim

ยังไม่อ่าน,
27 เม.ย. 2558 12:32:0627/4/58
ถึง Kevin Meredith, spire...@googlegroups.com
On Mon, Apr 27, 2015 at 08:54:11AM -0700, Kevin Meredith wrote:
> Could this check be handled at compile-time? If so, how would you do it?

Hi Kevin,

In cases where the user instantiates JetDim with a constant value (for
example JetDim(3)), we could perform the check at compile-time.

In the general case (JetDim(x)) we cannot.

I think the right way to do this would be to create a macro which
expands into either the constructor call (if the constant is checked
and ok), a compile error (if the constant is checked and bad), or a
run-time check and constructor call (if the argument is not a
constant).

If the machinery were simple (or reusable) it would be nice to allow
these kind of compile-time checks in other places as well.

One thing to keep in mind is that the constructor itself probably
can't be a macro, so you'll need to write a factory constructor on the
JetDim companion object.

> Note - I'm asking for my own learning. Once I finish the ScalaCheck work
> (on Jet now), I'd be interested to work on this if that's OK.

You should definitely feel free to work on this.

I think it's important not to complicate the API too much, so we may
need to go through several iterations before we end up with something
that we can merge. But it's a great project to work on.

Thanks!

-- Erik

Tom Switzer

ยังไม่อ่าน,
27 เม.ย. 2558 12:41:1627/4/58
ถึง Erik Osheim, Kevin Meredith, Spire User List
An idea for a more general mechanism, is to kind of copy the Shapeless approach (https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/singletons.scala#L39). Basically, we could have a

final class PositiveWitness(val value: Int) extends AnyVal

Then add an implicit (macro) conversion from Int => PositiveWitness. It could do the compile-time-check-when-possible stuff. You could also go further (this is probably simpler, actually) and ONLY allow the implicit macro to work when we can guarantee the value is positive, and then force other people to use an explicit PositiveWitness constructor, like `JetDim(PositiveWitness(n))`, when they don't have the compile-time singleton type,and this'll just do the runtime check. We can then use this type anywhere we want the guarantees, like:

case class JetDim(dim: PositiveWitness) { ... }

Kevin Meredith

ยังไม่อ่าน,
28 เม.ย. 2558 14:30:5328/4/58
ถึง spire...@googlegroups.com, er...@plastic-idolatry.com, kevin.m....@gmail.com
So, to make sure that I understand correctly, here's the approach that scalatest/scalactic uses to verify that a PosInt only has values > 0.

Looking at the AST:

scala> import scala.reflect.runtime.{universe => u}
import scala.reflect.runtime.{universe=>u}

scala> val expr =  u reify { 100 }
expr: reflect.runtime.universe.Expr[Int] = Expr[Int(100)](100)

scala> u showRaw expr
res12: String = Expr(Literal(Constant(100)))


So, similarly to scalactic's approach, the check for JetDim's argument would be something like:

1. if a Constant gets passed to JetDim, then we can check if it's > 0 (use Context#abort if it's <= 0)
2. if it's not a Constant, then let it go and find out at runtime?

I'll review the link that you provided, Tom, for shapeless. 

I simply commented above since, although I haven't used macros, the scalactic code looks simple to me.

ข้อความถูกลบแล้ว

Kevin Meredith

ยังไม่อ่าน,
30 เม.ย. 2558 10:27:5530/4/58
ถึง spire...@googlegroups.com, er...@plastic-idolatry.com, kevin.m....@gmail.com
I wrote the macro, but I'm stuck on how this issue - http://stackoverflow.com/questions/29949563/using-macro-to-make-case-class

Kevin Meredith

ยังไม่อ่าน,
30 เม.ย. 2558 11:41:1930/4/58
ถึง spire...@googlegroups.com, kevin.m....@gmail.com, er...@plastic-idolatry.com
Thanks for your help, Tom.

Per this comment:

>Yeah, it would generally be useful elsewhere, so it'd make sense to have it live by itself so other methods/classes can use it.

this means that PositiveInt.scala would live in macros/src/main/scala/spire/macros? And JetDim would stay within core/src/main/scala/spire/math/Jet.scala?

With that attempt, I got this compile-time error:

[info] Compiling 1 Scala source to C:\Users\k\Workspace\spire\macros\target\scala-2.11\classes...
[error] C:\Users\k\Workspace\spire\macros\src\main\scala\spire\macros\PositiveInt.scala:8: not found: type PositiveInt
[error]   implicit def wrapConstantInt(n: Int): PositiveInt = macro verifyPositiveInt
[error]                                         ^
[error] C:\Users\k\Workspace\spire\macros\src\main\scala\spire\macros\PositiveInt.scala:10: not found: type PositiveInt
[error]   def verifyPositiveInt(c: Context)(n: c.Expr[Int]): c.Expr[PositiveInt] = {
[error]                                                             ^
[error] two errors found

Since the PositiveInt case class lives within the `core` SBT project, and it depends upon the `macros` project, I don't see how to fix this compile-time problem.

Perhaps, per your answer to my "Where to Put Macros" question, PositiveInt.scala should live within the `core` project?

Tom Switzer

ยังไม่อ่าน,
30 เม.ย. 2558 11:48:0330/4/58
ถึง Kevin Meredith, Spire User List, Erik Osheim
The PositiveInt, the case class, can live inside macros too, right? I'd just put them together. May be under a different package though... `spire.validation` or something?

Kevin Meredith

ยังไม่อ่าน,
30 เม.ย. 2558 12:29:4630/4/58
ถึง spire...@googlegroups.com, kevin.m....@gmail.com, er...@plastic-idolatry.com
Thanks, Tom.

I have a follow-up question from Erik's initial response:

>In cases where the user instantiates JetDim with a constant value (for 
>example JetDim(3)), we could perform the check at compile-time. 

I *think* that I understand the difference between a constant and non-constant value:

scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe
 
scala> universe showRaw res0
res1: String = Expr(Function(List(ValDef(Modifiers(PARAM), TermNam
nstant(10))))))
 
scala> universe showRaw reify { x: Int => x + 10 }
res2: String = Expr(Function(List(ValDef(Modifiers(PARAM), TermName("x"), Ident(scala.Int), EmptyTree)),
Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(10))))))
 
scala> universe showRaw reify { 100 }
res3: String = Expr(Literal(Constant(100)))

However, if constructing a new JetDim requires an Int argument, won't we always be able to check that it's > 0 at compile-time?

I assume not given Erik's answer. Could one of you please elaborate on non-constant?

Thank you,
Kevin

Alec Zorab

ยังไม่อ่าน,
30 เม.ย. 2558 12:45:0830/4/58
ถึง Kevin Meredith, spire...@googlegroups.com, er...@plastic-idolatry.com
def makeJetDim(dimString:String) : JetDim = JetDim(dimString.toInt)

Tom Switzer

ยังไม่อ่าน,
30 เม.ย. 2558 13:15:4030/4/58
ถึง Kevin Meredith, Spire User List, Erik Osheim
Yeah, the essential thing is to think when *macro expansion* (when the macro is replaced with the AST it generates) actually happens and what it could possibly know at that point in time. Macros are expanded at the call site where they are used. For example, in the simple case, we have:

JetDim(33)

The compiler sees 33, sees that JetDim takes a PositiveInt, so looks for an implicit conversion from 33 => PositiveInt. It finds an implicit macro, so it sticks that in the place:

JetDim(PositiveInt.wrapConstantInt(33))

This is the call site of the macro, so it then *expands* the macro at this point. Here, you can see that we are explicitly passing *33* into the macro, the macro is able to check, at compile time, that 33 > 0, and so all is great. It replaces the call site with:

JetDim(PositiveInt(33))

But think of a situation like this:

def buildTheThing(n: Int): JetDim = JetDim(n)

We are in a similar situation, where the compiler finds the implicit macro conversion from Int => PositiveInt, so it then expands the macro... *at this point* - inside of the def - exactly once. It's just changing the implementation of buildTheThing:

def buildTheThing(n: Int): JetDim = JetDim(PositiveInt.wrapConstantInt(n))

At this point in the call site, it does not have any information about the actual value of n - it could, literally, be anything since it comes from an argument to buildTheThing. So, when we call buildTheThing(33), there are no macros anymore - all the macro stuff happened when we defined buildTheThing, not when we call buildTheThing.

Does that make sense? I suspect there is a much more concise way to express the idea :(


Kevin Meredith

ยังไม่อ่าน,
30 เม.ย. 2558 16:05:3330/4/58
ถึง spire...@googlegroups.com, kevin.m....@gmail.com, er...@plastic-idolatry.com
Thanks, Alec and Tom.

I understood the first part of your answer, Tom, but not the second with `buildTheThing`.

scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe
 
// `Constant`
scala> universe showRaw reify { 100
res3: String = Expr(Literal(Constant(100)))
 
// not a `Constant`
scala> reify { "100".toInt }
res0: reflect.runtime.universe.Expr[Int] = Expr[Int](Predef.augmentString("100").toInt)

I'm guessing that, when n (from buildTheThing) gets passed into JetDim(...), its AST representation does not match Constant? Could you please show me an example or elaborate on what n breaks down into with respect to the AST?

Alec corrected my wrong assumption that, so long as the types match, then the PositiveInt macro can handle the check at compile-time.
ตอบทุกคน
ตอบกลับผู้สร้าง
ส่งต่อ
ข้อความใหม่ 0 รายการ