Scala macros

Travis Brown
@travisbrown

Don't use macros!

Don't use macros!

The Scala language is already too complex

The Scala compiler is already too slow

You'll have to rewrite for 2.11 and 2.12

They don't work with Pants*

*Because of the need for separate compilation; you can define macros and publish them, but can't use macros you've defined without publishing.

So why does this course exist?

You learn a lot about the guts of Scala writing macros

Programs that write programs are fun

In some cases they're worth the danger

Macros are driving language evolution

Full disclosure

My personal problem

Full disclosure

I've been using since (almost) the beginning

I've contributed macro code to dozens of projects

I'm trying really hard not to oversell here

What are macros?

Rules for rewriting code


#define RADTODEG(x) ((x) * 57.29578)
					

Macros in Scala

Introduced in 2.10 by Eugene Burmako

New syntax and one new keyword

Some features require plugin in 2.10

Macro flavors

  1. def macros (ship with scalac)
    • blackbox
    • whitebox
  2. macro annotations (via plugin in 2.10)
  3. type macros (no longer supported)

What are macros good for?

Reducing boilerplate

Improving performance

More compile-time safety

Smarter test reporting

Extending the language

Reducing boilerplate:
generic derivation


case class User(id: Long, name: String, email: String)

body.as[User]         // Read JSON representation of user
body.as[Long => User] // Read id-less JSON user
body.as[User => User] // Read partial JSON user
					

Reducing boilerplate:
type providers


val dct = PrefixGenerator.fromSchema[Rdf]("/dctype.rdf")
val dc = PrefixGenerator.fromSchema[Rdf]("/dcterms.rdf")

val frankensteinNotebookB = url.a(dct.Text)
  -- dc.title ->- "Frankenstein Draft Notebook B"
  -- dc.creator ->- URI(
    "https://en.wikipedia.org/wiki/Mary_Shelley"
  )
					

Simplifying code:
false data dependencies


for {
  author <- getAuthor(postId)
  tags   <- getTags(postId)
  bio    <- getAuthorInfo(author)
  page   <- createPage(author, tags, bio)
} yield page
					

Desugared


getAuthor(postId).flatMap { author =>
  getTags(postId).flatMap { tags =>
    getAuthorInfo(author).flatMap { bio =>
      createPage(author, tags, bio)
    }
  }
}
					

Optimized


getAuthor(postId).join(getTags(postId)).flatMap {
  case (author, tags) =>
    getAuthorInfo(author).flatMap { bio =>
      createPage(author, tags, bio)
    }
  }
}
					

Improving performance:
fast loops


import spire.syntax.cfor._

cfor(0)(_ < 10, _ + 1) { i =>
  println(i)
}
					

More compile-time safety:
string interpolation


// works fine
def foo(name: String, id: Int) = f"$name: $id%08d"

// doesn't compile
def bar(name: String, id: Int) = f"$id: $name%08d"
					

Smarter test reporting:
Diagrammed assertions


scala> assert("hello".startsWith("h") && "goodbye".endsWith("y"))
org.scalatest.exceptions.TestFailedException: 

assert("hello".startsWith("h") && "goodbye".endsWith("y"))
       |       |          |    |  |         |        |
       "hello" true       "h"  |  "goodbye" false    "y"
                               false
					

Smarter tests:
Testing for non-compilation


import shapeless.test.illTyped

illTyped("""
  class Foo(i: Int)

  val foo = new Foo("a")
""")
					

Extending the language

What do macros look like?


import language.experimental.macros, reflect.macros.Context

def tupleFillImpl[A: c.WeakTypeTag](c: Context)
  (n: c.Expr[Int])(a: c.Expr[A]) = {
  import c.universe._

  val howMany = n.tree match {
    case Literal(Constant(i: Int)) => i
    case _ => c.abort(
      c.enclosingPosition,
      "Must provide a literal integer!"
    )
  }

  c.Expr[Any](q"""(..${List.fill(howMany)(a)})""")
}
					

Macro definitions

No new syntax in this part: all library-level

You can run this example in a 2.10 or 2.11 REPL

What do macros look like?


scala> def tupleFill[A](n: Int)(a: A) = macro tupleFillImpl[A]
defined term macro tupleFill: [A](n: Int)(a: A)Any

scala> tupleFill(4)("hey")
res0: (String, String, String, String) = (hey,hey,hey,hey)
					

Macro methods and usage

One new keyword in the macro method

Note the statically inferred return type

(Also note the failed syntax highlighting)

Navigating the minefield

Increasing degrees of danger

  1. Manual AST construction
  2. Whitebox def macros
  3. Macro annotations
  4. enclosing*

Manual AST construction

The macro reflection API is changing rapidly

Quasiquotes are intended to be more stable

Box color

Whitebox: can't provide a type signature before expansion

Blackbox: type signature is required (and conformed to)

Macro annotations

Extremely powerful: can introduce new top-level classes

Much more experimental than def macros

enclosingUnit, etc.

Extremely powerful: look at code that's not an argument

Already deprecated in 2.11

Future-proofing your macros

blackbox def macros + quasiquotes + no enclosing*

Macros at Twitter

Generic derivation in Algebird


import com.twitter.algebird._
import com.twitter.algebird.macros.caseclass._
import com.twitter.algebird.macros.ArbitraryCaseClassMacro._

case class Foo(s: String, i: Int, d: Double)

implicitly[Monoid[Foo]].zero
arbitrary[Foo].arbitrary.sample
					

Other resources