Skip to content

Remove implicit conversion from liftable types to Expr[T] #4129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

nicolasstucki
Copy link
Contributor

@nicolasstucki nicolasstucki commented Mar 16, 2018

Use import scala.quotes._ and x.toExpr notation instead.

Use `import scala.quotes.Liftable._` and `x.toExpr` notation instead.
To use `import scala.quotes._` to import `toExpr` extension
Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reason for dropping the conversion? I'm asking because MetaML supports lifting without conversions out of the box.

@nicolasstucki
Copy link
Contributor Author

In general, it has proven to be awkward to have these implicit transformations. By looking at the code it is not always simple to know what is lifted and what is not. Which seems to make it harder to see the different stages in the code. If we require an explicit toExpr it is easy to see and understand which values have cross-stage persistence.

@OlivierBlanvillain might have some additional reasons.
@smarter and @biboudis also agreed that this was a better approach.

@odersky
Copy link
Contributor

odersky commented Mar 16, 2018

Maybe a case to introduce an @implicitNotFound annotation on implicit conversions?

@odersky
Copy link
Contributor

odersky commented Mar 17, 2018

I am not convinced yet that this is a good idea.

@nicolasstucki
Copy link
Contributor Author

nicolasstucki commented Mar 17, 2018

Currently, we can automatically lift with the implicit conversion

val x = 1
'{ ~(x: Expr[Int]) }

val y = 1
val y2: Expr[Int] = y
'{ ~y2 }

We can also explicitly lift using

val x = 1
'{ ~x.toExpr }

There is also the corner case where a value is lifted but not used in a splice

def positive(n: Int): Expr[Int] = if (n < 0) 0 else n
// or explicitly
def positive(n: Int): Expr[Int] = if (n < 0) '(0) else n.toExpr

But we cannot lift

val x = "a"
'{ x } // phase inconsistent
'{ ~x } // ~ is not a member of x 

This last version would be the only way to lift if this PR in merged. Which is shorter and clearer than using implicit conversion in most cases (see first code example and compare with second). I saw this while coding https://github.com/nicolasstucki/quoted-util, specifically this commit .

   implicit def Tuple22IsLiftable[T1 : Liftable : Type, T2 : Liftable : Type, T3 : Liftable : Type, T4 : Liftable : Type, T5 : Liftable : Type, T6 : Liftable : Type, T7 : Liftable : Type, T8 : Liftable : Type, T9 : Liftable : Type, T10 : Liftable : Type, T11 : Liftable : Type, T12 : Liftable : Type, T13 : Liftable : Type, T14 : Liftable : Type, T15 : Liftable : Type, T16 : Liftable : Type, T17 : Liftable : Type, T18 : Liftable : Type, T19 : Liftable : Type, T20 : Liftable : Type, T21 : Liftable : Type, T22 : Liftable : Type]: Liftable[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] = {
-    x => '{ (~(x._1: Expr[T1]), ~(x._2: Expr[T2]), ~(x._3: Expr[T3]), ~(x._4: Expr[T4]), ~(x._5: Expr[T5]), ~(x._6: Expr[T6]), ~(x._7: Expr[T7]), ~(x._8: Expr[T8]), ~(x._9: Expr[T9]), ~(x._10: Expr[T10]), ~(x._11: Expr[T11]), ~(x._12: Expr[T12]), ~(x._13: Expr[T13]), ~(x._14: Expr[T14]), ~(x._15: Expr[T15]), ~(x._16: Expr[T16]), ~(x._17: Expr[T17]), ~(x._18: Expr[T18]), ~(x._19: Expr[T19]), ~(x._20: Expr[T20]), ~(x._21:Expr[T21]), ~(x._22: Expr[T22])) }
+    x => '{ (~x._1.toExpr, ~x._2.toExpr, ~x._3.toExpr, ~x._4.toExpr, ~x._5.toExpr, ~x._6.toExpr, ~x._7.toExpr, ~x._8.toExpr, ~x._9.toExpr, ~x._10.toExpr, ~x._11.toExpr, ~x._12.toExpr, ~x._13.toExpr, ~x._14.toExpr, ~x._15.toExpr, ~x._16.toExpr, ~x._17.toExpr, ~x._18.toExpr, ~x._19.toExpr, ~x._20.toExpr, ~x._21.toExpr, ~x._22.toExpr) }
   }

@nicolasstucki
Copy link
Contributor Author

nicolasstucki commented Mar 17, 2018

With implicit conversions, we also can easily write buggy code without noticing. For example both of the following methods will typecheck, though there is a missing ' (easy to miss) which will make the code behave radically different. The first will print correctly when we run the quote, but the second one will print when the quote is created.

def printHelloAndReturnFoo: Expr[String] = '{
  println("hello")
  "foo"
}
def printHelloAndReturnBar: Expr[String] =  {
  println("hello")
  "bar"
}

This has already happened a few times to me.

@odersky
Copy link
Contributor

odersky commented Mar 17, 2018

I am still not convinced. None of these seem to be serious problems for MetaML. Beware of expert syndrome here! Your initial users will have only a faint idea what goes wrong. If a simple and obvious thing does not work because it misses a .toExpr, you know better, but they will feel first confused why their code does not work, and then insulted that the compiler is so pedantic about a thing that should work.

"Make simple things easy and complex things possible" -- we should take that seriously.

@nicolasstucki
Copy link
Contributor Author

Then we should support automatic lifting for the case

val x = "a"
'{ ... ~x ... }

This seams to be the most common case.

Which might become a bit tricky with types that already define ~ such as Int.

@OlivierBlanvillain
Copy link
Contributor

OlivierBlanvillain commented Mar 19, 2018

"Make simple things easy and complex things possible" -- we should take that seriously.

I don't think it makes things easier to blur the distinction between compilation time and execution time.

The biggest obstacle when first trying out staging is to understand what happens at what stage. I believe having implicit conversion involved makes it significantly harder to get started with multi stage programs.

Also, it's literally a one line addition in user code to revert what this PR does, simply define implicit def f[T: Liftable](x: T): Expr[T] = x.toExpr, whereas in the current state of things there is no way to disable this implicit.

None of these seem to be serious problems for MetaML.

That's because they don't have any automatic lifting it in metaocaml, everything has to be precisely annotated:

let x : int code = .<1>.
-- compile ok
let x : int code = 1
-- metaocaml test.ml
File "./test.ml", line 2, characters 21-22:
Error: This expression has type int but an expression was expected of type
         int code
BER MetaOCaml toplevel, version N 104

Compilation exited abnormally with code 2 at Mon Mar 19 11:57:10

@nicolasstucki nicolasstucki merged commit 7be9199 into scala:master Mar 19, 2018
@Blaisorblade Blaisorblade deleted the remove-implicit-conversion-toExpr branch March 19, 2018 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants