Skip to content

Syntax for method types or tree constructors useful in meta-programming #5563

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

Closed
liufengyun opened this issue Dec 3, 2018 · 7 comments
Closed
Assignees

Comments

@liufengyun
Copy link
Contributor

liufengyun commented Dec 3, 2018

I'm writing a ScalaTest macro, where I find it helpful to have syntax for method types.

The use case is to transform the source code:

assert(5 === 6)

which is desugared as:

assert(
  AssertionsSpec.this.convertToEqualizer[scala.Int](5).===(6)(scalactic.Equality.default[scala.Int])
)

and macro expanded to something like the follows:

{
    val lhs = 5
    val rhs = 6
    val result = AssertionsSpec.this.convertToEqualizer[scala.Int](lhs).===(rhs)(scalactic.Equality.default[scala.Int])
    val _bool = Bool.binaryMacroBool(lhs,  "===", rhs, result)
    Assertions.assertionsHelper.macroAssert(_bool, "", pos)
}

The natural thing to do in the implementation is to seal the following tree as a whole:

AssertionsSpec.this.convertToEqualizer[scala.Int]

The ideal implementation is something like the following:

val fun = fn.seal[(Any)Equalizer[_]]
val left = lhs.seal[Any]
val right = rhs.seal[Any]
val equality = eq.seal[Equality[_]]

'{
  val _left   = ~left
  val _right  = ~right
  val _result = ~fun(_left).===(_right)(~equality)
  val _bool = Bool.binaryMacroBool(_left, ~op.toExpr, _right, _result, ~prettifier)
  Assertions.assertionsHelper.macroAssert(_bool, ~clue, ~pos)
}

Note that in the implemetation, we used the syntax for method type: (Any)Equalizer[_].

Or, we can provide tree constructors (Related #5438):

// How to do overloading resolution for `===`?
def result(l: Expr[Any], r: Expr[Any]) = 
  fn.appliedTo(l.unseal).select("===").appliedTo(r.unseal).appliedTo(eq.unseal).seal[Boolean]
val left = lhs.seal[Any]
val right = rhs.seal[Any]

'{
  val _left   = ~left
  val _right  = ~right
  val _result = ~result(_left, _right)
  val _bool = Bool.binaryMacroBool(_left, ~op.toExpr, _right, _result, ~prettifier)
  Assertions.assertionsHelper.macroAssert(_bool, ~clue, ~pos)
}

However, the constructor-approach is inferior: in the quote-approach overloading resolution is a given for free, while in the constructor-approach it will be a burden on macro authors or the constructors have to do overloading resolution. AFAIK, the latter is not in the scope of #5438, @nicolasstucki mentioned adapation should be avoided in constructors.

@smarter
Copy link
Member

smarter commented Dec 3, 2018

I don't think we should introduce a special syntax like this that can only be used in one context, it's going to be very confusing to users. What about writing seal[Any => Equalizer[_]] instead? The implementation can then use method types internally if needed

@liufengyun
Copy link
Contributor Author

seal[Any => Equalizer[_]] will cause seal to fail, and it causes ambiguity about whether it's a method type or function type.

I agree introducing new syntax should be avoided. What about introducing an intrinsic type constructor:

type MethodType[T]

Where MethodType[Int => Int] means (Int)Int?

@nicolasstucki
Copy link
Contributor

nicolasstucki commented Dec 3, 2018

I belive this would be best encoded with quote extractors (which we do not have yet). It would look something like

  case '{ (~eq).apply(~left, ~right) } if isEqEqEq(eq) => // where eq is of type (Expr[T1], Expr[T2]) => Expr[Boolean]
   '{ 
      val _left   = ~left
      val _right  = ~right
      val _result = ~eq(_left, _right)
      val _bool = Bool.binaryMacroBool(_left, ~op.toExpr, _right, _result, ~prettifier)
      Assertions.assertionsHelper.macroAssert(_bool, ~clue, ~pos)
   }    

@liufengyun
Copy link
Contributor Author

@nicolasstucki The pattern match for the following:

AssertionsSpec.this.convertToEqualizer[scala.Int](5).===(6)(scalactic.Equality.default[scala.Int])

is more complex:

case '{ (~fun)(~left).===(~right)(~equality) } } ==>

I see on easy way to give semantics to the code above.

@liufengyun
Copy link
Contributor Author

Fixed in #5603 , to be confirmed in ScalaTest.

@liufengyun
Copy link
Contributor Author

#5603 helps when we know the signature of the method statically.

In the following case, we don't know the signature statically:

// definition
def convertToEqualizer[T](left: T): Equalizer[T]

// usage
this.convertToEqualizer[Option[String]]

Then it's difficult to explicitly give a sensible type below:

val fun = fn.seal[???]

It seems in such cases, constructors are more convenient.

@liufengyun
Copy link
Contributor Author

Close this, discussions for constructors are in #5567 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants