Skip to content

Latest commit

 

History

History
448 lines (320 loc) · 11.1 KB

index.adoc

File metadata and controls

448 lines (320 loc) · 11.1 KB

Scala 2 vs Scala 3 macros

Mateusz Kubuszok

Note

Preparations: - open Chimney project (compiling!, 0.8.2) - open Pipez (compiling!, 0.5.1) - open terminal - 1 tab for examples, 1 for Chimney, 1 for Pipez - arrange them to easily jump from one to another - put speakers notes on laptop

About me

Note

Every presentation should start with some excuse, why you even are here.

Agenda

  • What is a macro method?

  • Expressions and types

  • Symbols

  • Examples showing similarities and differences when solving the same small problems

  • I will focus on examples that I saw while maintaining my libraries

  • I will NOT focus on macro annotations

Note

We will demonstrate these concepts using Scala CLI scripts.

What is macro method?

We can intuitively think that macro method is a code generator pretending to be some object’s method.

Note

We’ll explain what we mean by that in the next few examples. We’ll show how macros differ in behavior from normal method.

Let’s see an example of a very simple macro

  1. Calling the impl method is the only thing we are allowed to do.

  2. Expr[A] is the AST of code that represents the value of type A.

  3. Why Scala 2 call it blackbox will be explained later.

  4. Scala 3 has "global" expressions while Scala 2 use path-dependent types for them.

Note

We’re start with the simplest code that returns a constant value (01_simple_macro).

I can mention that expression is basically anything that computes a value.

Can the AST generation be in a different place than macro method?

  1. Impl doesn’t have to be in the same definition as unquoting - it doesn’t even have to be in the same package!

  2. Impl has to be defined in a preceeding compilation unit to call site (macro referring to it can be in the same as call site).

Note

Then we’re going to look at the example above but with classes (02_macro_with_classes).

This is the first trope that we shouldn’t treat macros as methods - they don’t even have to be define in the same place as what unquotes them.

Let’s call some method in the macro

Note

Now let’s call some method in the same object as the macro (03_calling_methods).

We are ancouraged to use full qualified name in Scala 2 as it doesn’t understand context in quasiquotes.

If we used private method, compilation would fail since external code cannot access private methods (04_calling_private).

This is the first example showing why I consider macro method to be a codegen rather than a method.

Let’s try printing some parameters

Note

At first let’s print some parameters to see how printing works (05_print_param).

Then let’s try to print this (06_print_this).

Scala 2 require the same names and positions of parametrs in macro and in called impl definition.

Scala 2 require path-dependent type for WeakTypeTag as well, Scala 3 does not require it (like with Expr).

Typed representations exists to be passed around (as params/returns/implicits), untyped can be actually worked with.

Scala 2 contains a special value for what was before macro, and Scala 3 requires us to pass it explicitly.

!

Scala 2

Scala 3

(c: scala.reflect.macros.blackbox.Context)

(using quotes: scala.quoted.Quotes)

c.WeakTypeTag[A] (or c.TypeTag[A])

scala.quoted.Type[A]

c.Type

quotes.reflect.TypeRepr

c.Expr[A]

scala.quoted.Expr[A]

c.universe.Tree

quotes.reflect.Tree

c.prefix

no counterpart

Note

Scala 2 require the same names and positions of parametrs in macro and in called impl definition.

Scala 2 require path-dependent type for WeakTypeTag as well, Scala 3 does not require it (like with Expr).

Typed representations exists to be passed around (as params/returns/implicits), untyped can be actually worked with.

Scala 2 contains a special value for what was before macro, and Scala 3 requires us to pass it explicitly.

!

Scala 2

Scala 3

weakTypeOf[A]: c.Type

TypeRepr.of[A]: TypeRepr

c.WeakTypeTag[A](tpe: c.Type)

(tpe: TypeRepr).asType.asInstanceOf[Type[A]]

expr.tree

expr.asTerm

c.Expr[A](tree)

tree.asExprOf[A]

Note

WeakTypeTag can only store proper types.

quoted.Type has AnyKind so it can also store type constructor.

asExprOf takes implicit Type.

!

Scala 2

Scala 3

show(expr) or showCode(expr)

expr.asTerm.show or expr.asTerm.show(using Printer.TreeCode)

no counterpart

expr.asTerm.show(using Printer.TreeAnsiCode)

showRaw(expr)

expr.asTerm.show(using Printer.TreeStrucrture)

weakTypeOf[A].toString

TypeRepr.of[A].show or TypeRepr.of[A].show(using Printer.TypeReprCode)

no counterpart

TypeRepr.of[A].show(using Printer.TypeReprAnsiCode)

showRaw(weakTypeOf[A])

TypeRepr.of[A].show(using Printer.TypeReprStructure)

Note

At first let’s print some parameters to see how printing works (05_print_param).

Then let’s try to print this (06_print_this).

Scala 2 always carries around what was "before" dot macro method name, Scala 3 requires explicit passing of this.

Scala 2 require path-dependent type for WeakTypeTag as well, Scala 3 does not require it (like with Expr).

Scala 2 require the same names and positions of parametrs in macro and in called impl definition.

Scala 2 contains a special value for what was before macro, and Scala 3 requires us to pass it explicitly.

!

Scala 2

Scala 3

c.enclosingPosition

Position.ofMacroExpansion

c.echo(pos, msg) or c.echo(msg)

report.info(msg, pos) or report.info(msg) or report.info(msg, expr)

c.warn(pos, msg)

report.warning(msg, pos) or report.warning(msg) or report.warning(msg, expr)

c.error(pos, msg)

report.error(msg, pos) or report.error(msg) or report.error(msg, expr)

c.abort(pos, msg)

report.errorAndAbort(msg, pos) or report.errorAndAbort(msg) or report.errorAndAbort(msg, expr)

Note

Show example of 07_reporting.

Explain why println is not a good idea.

Analyzing types

Symbol - a reference to definition (type, class, val, var, method, parameter, binding…​).

!

Scala 2

Scala 3

(tpe: c.Type).typeSymbol

(repr: TypeRepr).typeSymbol

sym.isType / sym.isClass / sym.isModule / sym.isTerm

sym.isType / sym.isClassDef / --- / sym.isTerm

sym.asType, sym.asClass, sym.asModule, sym.asTerm

only 1 kind of Symbol

sym.asClass.primaryConstructor

sym.primaryConstructor

NoSymbol

Symbol.noSymbol

(tpe: c.Type).decls

sym.declaredFields / sym.declaredMethods

(tpe: c.Type).members

sym.fieldMembers / sym.methodMembers

Note

Let’s try to see what information we can obtain from the type (08_analyzing_type).

  1. Scala 3 has no isModule - we need to check that something has Flag.Modules

  2. Scala 2 name it isClass and Scala 3 isClassDef

  3. When class nas no constructor it has a special NoSymbol value

  4. Scala 2 has members (all definitions, inherited or declared) and decls (only definitions defined in the type) in Type, Scala 3 separated fields from methods and store them in Symbol

I can explain that Symbol is basically anything which can have a name or handle to be referred to.

!

typeParams (Scala 2)

paramLists (Scala 2)

paramSymss (Scala 3)

def method: Unit

List()

List()

List()

def method(): Unit

List()

List(List())

List(List())

def method(a: Int, b: String): Unit

List()

List(List(value a, value b))

List(List(val a, val b))

def method(a: Int)(b:String):Unit

List()

List(List(value a), List(value b))

List(List(val a), List(val b))

def method[A]: Unit

List(type A)

List()

List(List(type A))

def method[A](a: A): Unit

List(type A)

List(value a)

List(List(type A), List(val b))

extension [A](a: A) def method[b](b: B): Unit

List(List(type A), List(val a), List(type B), List(val b))

Note

Mention SIP-47 Clause Interleaving.

Building expressions

Example:

  • take a type of a case class/sealed trait

  • try to create List with a value of this type

  • for case class create a value if all params has default value

  • for sealed, create all children that can be created (case objects lub case classes like above)

Note

Show example (10_example).

Show that while it looks ok, it doesn’t support all cases.

Skeletons in the closet

Note

Examples:

  1. Scala 2’s companion object issue

    • TODO: failing test

    • Chimney, Scala 2, ProductTypesPlatform.scala:224

  2. Scala 2’s knownDirectChildren and incremental compiler

    • TODO: failing test

  3. Weird bugs

    • TODO: failing test

    • Pipez, Scala 2, Macros.scala:115

  4. Scala 3’s typeSignatureIn

    • Chimney, Scala 2, TypesPlatform.scala:24

    • Chimney, Scala 3, TypesPlatform.scala:29

  5. Scala 3’s public

    • ???

  6. Scala 3’s fresh name

    • ???

Also mention that: 1. default values in case class have different names (apply vs <init>) 2. parameterless case is not case object 3. @BeanProperty difference

Other differences

Scala 2

Scala 3

def method = macro methodImpl

def methodImpl(c.blackbox.Context): c.Expr[…​]

inline def method = ${ methodImpla }

def methodImpl(using Quotes): Expr[…​]

def method = macro methodImpl

def methodImpl(c.whitebox.Context): c.Expr[…​]

transparent inline def method = ${ methodImpla }

def methodImpl(using Quotes): Expr[…​]

"macro bundle"

no counterpart

Note

Show whitebox macros and transparent inline defs.

Show macro bundles on Scala 2, and what Scala 3 has.

Summary

  • basic concepts - typed and untyped expressions and types, AST, Symbols - are the same

  • Scala 2 APIs have more utilities, Scala 3 had more consistent utilities

  • both implementations have enough features to build upon them

  • both implementations have rather basic documentation

  • examples and slides available on my GitHub (GitHub.com/MateuszKubuszok)

Questions?

Thank You!