Contextual

simple and typesafe interpolated strings, checked at compile-time

Brian Clapper, @brianclapper
bmc@ardentex.com

What is Contextual?

  • Allows you to define your own string interpolators
  • ... that are checked at compile time

What is a "string interpolator"?

You've seen them before. Some are built into Scala:

Simple string interp:

        
          val age = 10
          val name = "Jenna"
          println(s"My name is $name, and I am $age years old.")
        
      

printf-style interp (checked at compile time!):

      
        println(f"My name is $name%20s, and I am $age%2d years old.")
      
    

Other examples

  • Apache Spark's DataFrame API provides a "$" interpolator as a convenient column reference: $"name", $"salary"
  • The Slick API has a SQL interpolator: sql"SELECT * FROM ..."

You can write your own

As noted at http://docs.scala-lang.org/overviews/core/string-interp.html , whenever the compiler sees


id"something"

it looks for an id() method on StringContext. If such a method is found, the compiler parses the string into prefixes and arguments, instantiating a StringContext with the prefixes and passing the arguments into the id() method.

You can write your own

What the hell does that mean?

Suppose you code this:

    
      json"{ name: $name, id:$id }"
    
  

The compiler converts that to:

    
      new StringContext("{name: ", " id: ", "}").json(name, id)
    
  

Let's take a look at an example

Time for some code...

How do you get compile-time evaluation?

The previous example failed at runtime. How do you implement a string interpolator that fails at compile time, the way the "f" interpolator does?

Short answer: Compiler macros.

Macros can be quite complicated. They expose the compiler's AST, and the macro writer is often required to manipulate the AST and perform type-level programming.

There's an easier way.

It's called Contextual.

Contextual hides all the macro fiddling, providing a simpler (though not entirely simplistic) API that allows you to specify the compile-time and runtime behavior of your interpolator.

How does it work?

Basic approach:

  • Add the necessary imports.
  • Create an object that subclasses Interpolator.
  • Implement the contextualize() method to provide any compile-time checks you want.
  • Implement the evaluate() method to provide the runtime behavior.
  • Create an implicit class to add the appropriate interpolator method to the Scala StringContext class.

A Contextual "regex" interpolator

Let's reimplement our "regex" interpolator in Contextual. (Time for more code.)

What about substitution?

Let's look at an "sh" interpolator that:

  • allows embedded quotes
  • supports compile-time syntax checks
  • allows substitution (with some restrictions)

We'll also look at a class that removes that final restriction.

(Back to IntelliJ.)

Conclusion

This API isn't exactly simple. But if you want compile-time interpolator checks, it's far simpler than the alternative.

There's a Gitter channel devoted to Contextual, where you can ask questions.

Jon Pretty is a very nice fellow. Don't be afraid to ask questions. Also, don't be afraid to suggest updates.

Questions?