Monday, 20 November 2017

Practical Awesome Recursion - Ch 01: Fixpoints

This is chapter 1 in a series of blog posts about recursion schemes. This series uses Scala, and will focus on usage and applicability. It will be scarce in theory, and abundant in examples. The theory is valuable and fascinating, but I often find that knowing the theory alone is only half understanding. The other half of understanding comes from, and enables, practical application. I recommend you bounce back and forth between this series and theory. The internet is rich with blogs and videos on theory that explain it better than I would, so use those awesome resources.

Overview

The goal of this post is to prepare your data types so that you can abstrct over/away recursion, and be able to use all the generalisations that we'll explore in all future chapters in this series.

In order to prepare your data, there are three things to do:

  1. Make the recursive positions in your data type abstract
  2. Create a Functor for your data type
  3. Wraps your data type in Fix[_]

Step 1. Remove recursion from the data type

Add a type parameter to your data type that will be used to represent recursion (and other things as you'll see in future chapters). Everywhere that your type references itself, replace the type with the new abstract type parameter.

IntList

Example #1: let's say you have your own hand-written cons-list, specialised to Int; maybe you want to avoid boxing with List[Int]. It might look like this:

// Before
sealed trait IntList
final case class IntCons(head: Int, tail: IntList) extends IntList
case object IntNil extends IntList

This type references itself in IntCons#tail. Let's fix that...

//  After (v1)
sealed trait IntList[F]
final case class IntCons[F](head: Int, tail: F) extends IntList[F]
final case class IntNil[F]() extends IntList[F]

Ok so now it never references itself, but we've had to modify the non-recursive case, IntNil, from an object to a case class with zero params. I'd prefer IntNil to remain an object for better ergonomics and efficiency so let's use variance.

(Note: Many Scala FP'ers avoid type variance entirely because it's kind of broken in theory, can lead to bugs in higher-kinded contexts, screws up implicits sometimes and breaks type inference sometimes too. I advise: learn about it, understand the tradeoffs and decide on a case-by-case basis.)

//  After (v2)
sealed trait IntList[+F]
final case class IntCons[+F](head: Int, tail: F) extends IntList[F]
case object IntNil extends IntList[Nothing]

BinaryTree

How about a binary tree with an abstract type A in the nodes:

// Before
sealed trait BinaryTree[+A]
final case class Node[+A](left: BinaryTree[A], value: A, right: BinaryTree[A]) extends BinaryTree[A]
case object Leaf extends BinaryTree[Nothing]

Here we replace the left and right branches.

// After
sealed trait BinaryTree[+A, +F]
final case class Node[+A, +F](left: F, value: A, right: F) extends BinaryTree[A, F]
case object Leaf extends BinaryTree[Nothing, Nothing]

Don't worry about preserving the A in the branches -- don't try to make it an F[A] -- just a plain old *-kinded F is all you need. Step 3 will ensure that As persist in both branches' children.

JSON

JSON is recursive too.

JSON has arrays of JSON, it has objects with JSON values, those values can be arrays that contains even more objects with nested arrays and... you get the picture.

// Before
sealed trait Json
object Json {
  case object      Null                               extends Json
  final case class Bool(value: Boolean)               extends Json
  final case class Str (value: String)                extends Json
  final case class Num (value: Double)                extends Json
  final case class Arr (values: List[Json])           extends Json
  final case class Obj (fields: List[(String, Json)]) extends Json
}

Only replace the self references; preserve the outer type. List[Json] should be List[F], not F.

// After
sealed trait Json[+F]
object Json {
  case object      Null                              extends Json
  final case class Bool  (value: Boolean)            extends Json[Nothing]
  final case class Str   (value: String)             extends Json[Nothing]
  final case class Num   (value: Double)             extends Json[Nothing]
  final case class Arr[F](values: List[F])           extends Json[F]
  final case class Obj[F](fields: List[(String, F)]) extends Json[F]
}

Step 2. Create a Functor

In this step we create a Functor for our data types. Functor is a type class that exists in the FP lib of your choice, namely Scalaz or Cats. It looks like this:

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

It empowers the world outside your data structure to change one of its abstract types, and by necessity, all values of that type. It's just like calling .map on a list:

// Change the values
List(1, 2, 3).map(_ * 10)
  // yields
  List[Int](10, 20, 30)

// Change the type too
List(1, 2, 3).map(i => s"[$i]")
  // yields
  List[String]("[1]", "[2]", "[3]")

This functor is how the generic recursion abstractions can have access to, and control over the recursive spots in your data.

(Note: If you want to use monadic variants of morphisms later, you'll need to upgrade your Functor to a Traverse. I'll show that in a future post but just keep it in mind.)

Let's create instances for our examples above. It doesn't matter if you use Scalaz or Cats, only the imports need to change. The code itself is identical.

IntList

Let the compiler guide you, it will only accept one implementation:

implicit val functor: Functor[IntList] = new Functor[IntList] {
  override def map[A, B](fa: IntList[A])(f: A => B): IntList[B] = fa match {
    case IntCons(head, tail) => IntCons(head, f(tail))
    case IntNil              => IntNil
  }
}

Note: You could also use an implicit object but it can cause implicit resolution problems later. I can't remember why/when anymore, it's just become habit.

// Also possible but leads to implicit resolution problems
implicit object IntListFunctor extends Functor[IntList] {
  override def map[A, B](fa: IntList[A])(f: A => B): IntList[B] = fa match {
    case IntCons(head, tail) => IntCons(head, f(tail))
    case IntNil              => IntNil
  }
}

Another side-note, do explicitly annotate the type.

  1. If you don't the type will be a structural type which will slow down the compiler and mess with implicit resolution.
  2. Explicit annotation will be mandatory in Scala v3 anyway.
// Don't do this
implicit val functor = new Functor[IntList] {

BinaryTree

As intended, this next case introduces a bit more complexity. We want to provide the ability to transform the recursive positions which means we need to keep the value: A position abstract and stable.

You'll need to use kind-projector to get the nice BinaryTree[A, ?] syntax, instead of the monstrous, out-of-the-box ({ type L[X] = BinaryTree[A, X] })#L syntax. If types and terms weren't so different, it'd be BinaryTree[A, _] just like the underscore in List(1,2,3).map(_ * 100).

implicit def functor[A]: Functor[BinaryTree[A, ?]] = new Functor[BinaryTree[A, ?]] {
  override def map[B, C](fa: BinaryTree[A, B])(f: B => C): BinaryTree[A, C] = fa match {
    case Node(left, value, right) => Node(f(left), value, f(right))
    case Leaf                     => Leaf
  }
}

There's nothing special about the F type over the A. You could also write a functor over the A type, i.e. a Functor[BinaryTree[?, F]] instance.

JSON

Here our Fs are embedded in other values so we have to work a little harder to transform them, but not much harder. Lists have functors too so it's easy peasy: just call .map.

implicit val functor: Functor[Json] = new Functor[Json] {
  override def map[A, B](fa: Json[A])(f: A => B): Json[B] = fa match {
    case Null        => Null
    case j: Bool     => j
    case j: Str      => j
    case j: Num      => j
    case Arr(values) => Arr(values.map(f))
    case Obj(fields) => Obj(fields.map { case (k, v) => (k, f(v)) })
  }
}

Step 3. The Fixpoint

In step 1, we went from a recursive data structure to a non-recursive data structure. Our goal is to be able to abstract over/away recursion, not to vanquish it. How do we regain our recursion? After all, we still want the users of our amazing IntList library to be able to store more than one element!

We've going to wrap our types in a magical fixpoint type. Doing so will give us back our recursion.

Here's a definition of Fix:

case class Fix[F[_]](unfix: F[Fix[F]])

Confused? This isn't a theory series but get a pen and paper, plug in IntList and expand the alias step-by-step; it'll clear it up real quick.

Exciting side note: Stephen Compall and Tomas Mikula found a way to define Fix without boxing! See how, here. I measured a 20-30% improvement and copied the approach in my own recursion library. Very awesome stuff.

Anyway, wrap your data types in Fix[_] and you get your recursion back.

type RecursiveIntList       = Fix[IntList]
type RecursiveBinaryTree[A] = Fix[BinaryTree[A, ?]]
type RecursiveJson          = Fix[Json]

...which is a bit long-winded and unpleasant. Let's rename things.

The most-common convention I've seen is to append an F for "functor" to the data type and use the proper name in the alias.

sealed trait IntListF[+F]
type IntList = Fix[IntListF]

sealed trait BinaryTreeF[+A, +F]
type BinaryTree[A] = Fix[BinaryTreeF[A, ?]]

sealed trait JsonF[+F]
type Json = Fix[JsonF]

This actually works out great because then, in practice, I often create a new object with helpers that improve ergonomics and avoid Fix boilerplate when you need to manually create a structure, for example, unit test expectations, or using a parser that doesn't play nice with recursion schemes.

For example:

type IntList = Fix[IntListF]

object IntList {

  // Helpful cos Scala's type inference fails so often
  def apply(f: IntListF[IntList]): IntList =
    Fix(f)

  def nil: IntList =
    apply(IntNil)

  def cons(head: Int, tail: IntList): IntList  =
    apply(IntCons(head, tail))

  def fromList(is: Int*): IntList  =
    is.foldRight(nil)(cons)
}

Done!

That's it! We're done. It may seem like a lot of work but there's benefit coming that greatly outweighs the cost. The cost isn't huge anyway, just a bit of one-time-only boilerplate.

All source code available here: https://github.com/japgolly/learning/tree/public/recursion-blog/example/src/main/scala/japgolly/blog/recursion

If you like what I do and you'd like to support me, this series or any of my other projects, become a patron! It will make a big difference to me and help further more content. You can also follow me on Twitter for updates about the next post the series.

Monday, 5 June 2017

Dependently-Typed Functions

It's been a while since my last blog post. This time around I'm going to show how to do something in Scala that you might first think would be very straight-forward but unfortunately, at least in Scala ≤ 2.12.2, requires a bit of hoop-jumping: dependently-typed functions.

Consider the following data type:

sealed trait Field { type Value }
object Field {
  case object Name extends Field { type Value = String }
  case object Age  extends Field { type Value = Int }
}

With this data type, you have the following path-dependent types:

Name.Value == String
Age .Value == Int

val n = Name
n.Value == String

You also have the following type projections:

Name .type#Value = String
Age  .type#Value = Int
Field     #Value = Any

1. A Dependently-Typed Method

Lets say you want to create the following, rather innocent-looking function:

def emptyValue(f: Field): f.Value

Likely your first attempt will be to pattern-match:

def emptyValue(f: Field): f.Value =
  f match {
    case Field.Name => ""
    case Field.Age  => 0
  }

Unfortunately scalac doesn't support this and you instead get compilation errors:

<console>:15: error: type mismatch;
 found   : String("")
 required: f.Value
           case Field.Name => ""
                              ^
<console>:16: error: type mismatch;
 found   : Int(0)
 required: f.Value
           case Field.Age  => 0
                              ^

Ok, let's switch to a different representation:

type Aux[A] = Field { type Value = A }

def emptyValueHack[A](f: Aux[A]): A =
  f match {
    case Field.Name => ""
    case Field.Age  => 0
  }

def emptyValue(f: Field): f.Value =
  emptyValueHack[f.Value](f)

This does work. Great! END BLOG POST. Right? Wrong. There's another problem. What happens if we forget a case:

def emptyValueHack2[A](f: Aux[A]): A =
  f match {
    case Field.Name => ""
    // case Field.Age  => 0
  }

It's compiles successfully without any warnings. We've lost exhaustivity checking which is a runtime exception just waiting to happen. Oh noes. Now what? Consider: what are we doing when we do simple pattern-matching on each case? We're inspecting a value, choosing a corresponding function, and executing it. If each case has exactly one corresponding function then we have exhaustivity. That sounds like something we already have the means to do, without pattern-matching, in plain old Scala.

Ok, let do this ourselves, exactly one function per case and choose depending on the Field.

sealed trait Field {
  type Value
  def fold(n: Field.Name.type => Field.Name.Value,
           a: Field.Age .type => Field.Age .Value): Value
}

object Field {
  case object Name extends Field {
    override type Value = String
    override def fold(n: Field.Name.type => Field.Name.Value, a: Field.Age.type => Field.Age.Value): Value =
      n(this)
  }

  case object Age extends Field {
    override type Value = Int
    override def fold(n: Field.Name.type => Field.Name.Value, a: Field.Age.type => Field.Age.Value): Value =
      a(this)
  }
}

Take a good look at this. There are a few things that might seem odd. You're probably wondering why I'm passing statically-known singletons to arguments. Why not just:

def fold(name: => Field.Name.Value,
         age : => Field.Age .Value): Value

Three reasons:

  1. Cases aren't always objects. What if they're case classes like case class CustomField(label: String) extends Field. In such a case it will be important to pass the instance to the caller so that they have access to the additional/dynamic information in the label field.

  2. It embodies the proof that the appropriate argument case is required for each field case. The types make it clear and so long as you call the arg with this instead of Field.Name directly, then you get an extra bit of proof of correctness. Seeing as the workarounds described in this post introduce some tedium so too do they often introduce copy-pasting which can lead to accidental bugs like this:

// Spot the bug...
case object Name extends Field {
  override type Value = String
  override def fold(n: Field.Name.type => Field.Name.Value, a: Field.Age.type => Field.Age.Value): Value =
    n(Name)
}
case object Age extends Field {
  override type Value = Int
  override def fold(n: Field.Name.type => Field.Name.Value, a: Field.Age.type => Field.Age.Value): Value =
    n(Name)
}
  1. The caller has the same problem as above; if they're pattern-matching and want an downcast instance of Field in their case functions then this gives it to them so that they don't manually reference Field.Name and end up with potential bugs.

2. Reducing Duplication

Next, let's think about what happens if we add a new case like Field.Address; we'll have to update the fold signature in our current three places and then add a fourth in Field.Address. So much repetition! If you find yourself copy-pasting a cumbersome list of method arguments, consider it a good practice to encapsulate them all in a class/record. That way you can consistently pass around and declare a single type, and changes to its fields don't cause changes to method signatures. Let's do that for our fold method:

sealed trait Field {
  type Value
  def fold(f: Field.Fold): Value
}

object Field {
  case object Name extends Field {
    override type Value = String
    override def fold(f: Field.Fold) = f.name(this)
  }

  case object Age extends Field {
    override type Value = Int
    override def fold(f: Field.Fold) = f.age(this)
  }

  final case class Fold(name: Field.Name.type => Field.Name.Value,
                        age : Field.Age .type => Field.Age .Value)
}

Look at that fold method, nice and easy. We can add cases ALL DAY LONG without repetition. High five thineself.

While we're in the vicinity, let's add a little convenience method to the Fold class to really prove to ourselves that can accomplish our original goal, pay attention to signature:

final case class Fold(name: Field.Name.type => Field.Name.Value,
                      age : Field.Age .type => Field.Age .Value) {
  def apply(f: Field): f.Value =
    f.fold(this)                        
}

Now using the above, how can we rewrite our original emptyValue function?

def emptyValue(f: Field): f.Value =
  Field.Fold(
    name = _ => "",
    age  = _ => 0,
  )(f)

Goals: accomplished.

  • Concise
  • Exhaustive
  • Pattern-match in spirit

It's a little ugly but not a huge deal. Depending on your codebase and environment you can likely avoid the need for _ => bits without penalty which will make it a little nicer on the eyes.

3. A Dependently-Typed Function

Like most everything from OOP, methods are boring. You can't abstract over them, all you can do is call them. Functions allow you to do more because they themselves are values; you can pass them around which facilitates awesome collections methods like .map(A => B), .filter(A => Boolean), etc. and that's just the tip of the iceberg.

In the previous step we created a method whose output depends on its input value. How can we have the same in the form of a function? Function types are fixed, and the function type doesn't have a value to work with. Scala doesn't accept this kind of syntax:

// nope - not valid syntax
type MyFn = (f: Field) => f.Value

You might try using projections like this but you'll lose the relationship between input and output.

type MyFn = Field => Field#Value
// which is the same as
type MyFn = Field => Any

We've already encountered the answer. Surprise! Field.Fold is what we're looking for. It's a value, you can pass it around; you can apply it via its apply method to obtain a dependently-typed result.

Let's try it out:

def blah(field: Field, getValue: Field.Fold): field.Value =
  getValue(field)

Hmmm, yes, well it does work but it's admittedly not very interesting in that shape. It can only represent f => f.Value...

4. Dependently-Typed Functions

What if we want a function of any shape instead of just f => f.Value? Maybe you want to be able to print each field to the screen: f => f.Value => Unit. Maybe you want to reduce lists of each value to a single value: f => List[f.Value] => f.Value. Or perform validation: f => f.Value => Either[Error, f.Value].

Let's find an abstraction that can represent each example above. They all have the shape: f => {something including f.Value}. Add some type aliases for the right-hand sides:

type GetValue  [Value] = Value
type Print     [Value] = Value => Unit
type ReduceList[Value] = List[Value] => Value
type Validate  [Value] = Value => Either[Error, Value]

There's our abstraction:

type F[Value] = // various

with the fold being various cases of f => F[f.Value] where F[Value] = ….

Let's update the fold to allow this:

final case class Fold[F[_]](name: Field.Name.type => F[Field.Name.Value],
                            age : Field.Age .type => F[Field.Age .Value]) {
  def apply(f: Field): F[f.Value] =
    f.fold(this)                        
}

Now let's try it out:

// f: Field => f.Value
val getValue = Field.Fold[GetValue](
  name = _ => "George",
  age  = _ => 99)

// f: Field => f.Value => Unit
val printValue = Field.Fold[Print](
  name = _ => n => println(s"My name is $n."),
  age  = _ => i => println(s"I am $i years old."))

Great. Dependently-typed function values. And how do we use them? Just like normal functions.

val f = Field.Age
val v = getValue(f) // v = 99
printValue(f)(v)    // prints: I am 99 years old.

Very good. I hope you found the interesting. Thanks for reading!

For more exhilaration, as an exercise to the reader, try to:

  • define composition of fold functions
  • create multiple folds for subsets of the same data type

Appendix

Type parameters

In terms of representation, using type parameters is isomorphic (ignoring variance). The following represents the exact same information:

sealed trait Field[V]
object Field {
  case object Name extends Field[Int]
  case object Age  extends Field[String]
}

In terms of usage however, the two representations are far from the same and have a drastic impact on how you'll use them. Some things are easier, some harder.

For example, in terms of generic access existential types make life easier: its simply Field instead of Field[_]. This is even more evident when there's a constraint on the type like Field[_ <: AnyRef] whilst Field remains the same.

I've come across other scenarios but honestly can't remember right now, something something implicits... Instead take a look at this slide deck of @julienrf's for more comparison: https://julienrf.github.io/2017/existential-types/

Code

The final code in full of this blog post is available here: https://gist.github.com/japgolly/162f102d516abf86b54359ef0b1d3b65

Sunday, 20 March 2016

Testing Scala.JS on Firefox & Chrome from SBT

It's been fantastic being able to write Scala and compile it to JavaScript thanks to Scala.JS. Going further, Scala.JS lets you write accompanying unit tests, run them from SBT, and choose a target environment from {Rhino, Node.JS, Phantom.JS}. If you needed DOM in your testing, your only option was Phantom.JS which seems great at first but—because it only simulates DOM—you soon discover that there are many cases in which its behaviour diverges from normal browsers (eg. DOM types for <td> tags), or isn't supported at all (text selection, anything to do with focus, more). Oh, and it's also riddled with bugs, many significant and long-standing. So while Phantom.JS's effort is appreciated, it's no substitute for a real browser. This was where the story ended until very recently.

Recently, the Scala.JS team released scala-js-env-selenium which allows you to use Selenium in the same way you would the other JS environments. That means that Scala.JS can now interface with real browsers, namely Firefox and Chrome. Awesome!

The next step, and the purpose of this blog post, is to effectively integrate it into your SBT/Scala/Scala.JS project. Let me tell you about my ideal environment and then I'll show you how to achieve it.

Goals

In my ideal environment:
  • I write my Scala.JS unit tests the same way I already do, and I can continue to run them against Node.JS or Phantom.JS because it's fast. No changes there.
  • To run tests against Firefox or Chrome, I simply prefix my SBT command with firefox: or chrome:.
    For example, firefox:test would run tests in Firefox, chrome:testOnly a.b.c would run my a.b.c test in Chrome.
  • Testing in FF/Chrome doesn't require recompilation (All in the Land know Scalac is slow). It should use all of the same bits and bobs (especially the output JS) that normal tests use.
  • I specify that certain tests will only run in FF and/or Chrome.
    For example, tests that use focus should skip Phantom.JS where I know focus doesn't work.
  • I run testAll to run the same tests in all environments (fast-env & Firefox & Chrome). This will happen concurrently.
  • I can use FF/Chrome headlessly (i.e. without the windows popping up when the browsers are launched and running).

1. Selenium support.

Add this to your project/plugins.sbt:
libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "0.1.1"
Then install ChromeDriver which is needed so that Selenium can interface with Chrome.

2. General SBT Config

Create a file called project/InBrowserTesting.scala with the following content: This creates SBT configurations for each browser, then delegates most of the settings to the test:* settings.

3. Project-specific SBT config

Next you need to look at your existing SBT build settings.

For each cross-JVM/JS project, add:
  .configure(InBrowserTesting.cross)
For each JS-only project, add:
  .configure(InBrowserTesting.js)
For each JVM-only project, add:
  .configure(InBrowserTesting.jvm)

You might be wondering why JVM projects need any configuration at all. It's so that when testAll is run from a JVM project or the root project, you want it to run the JVM tests. Without this setting, testAll in a JVM project would do nothing at all, and testAll from the root would only run the JS tests.

4. Environment-Dependent Tests

I often forget that I'm writing JS when I write Scala.JS. As we're in JS land, to determine our environment all we have to do is check the user-agent. How you skip tests depends on the test framework you're using but all you have to do is put something like this in your test code:
  if (JsEnvUtils.isRealBrowser) {
    // test here
  } else {
    // skip
  }

5. Headlessness

Super simple (unless you're a Windows user). Install xvfb, "X Virtual FrameBuffer", which starts X without a graphics display.

Here are two different ways you can use it:
  1. Either start it in a separate window via
    Xvfb :1
    
    or in the background
    nohup Xvfb :1 &
    
    then launch SBT like this:
    DISPLAY=:1 sbt
    
  2. This tip comes from danielkza (thanks!). You can simply prepend your SBT command with xvfb-run -a to have an X server spun up on demand without the need to start it yourself. Beware though, that xvfb-run is a bit naive and susceptible to race conditions so while it'll be fine on your local machine, it may cause you problems on your CI server or similar.
You'll no longer see any Firefox and Chrome windows; all your output just appears in the SBT console as usual. Too easy.

Note: The :1 indicates the X display-number, which is a means to uniquely identify an X server on a host. The 1 is completely arbitrary—you can choose any number so long as there isn't another X server running that's already associated with it.

Done.

All goals described are thus achieved.
I hope you found this helpful.
Happy coding!

Monday, 23 February 2015

Zero-overhead Recursive ADT Coproducts

Zero-product Recursive AMD what???

Ok. Imagine this: you're building some app, and in certain parts users can type text with special tokens. (It's like in Twitter, when typing “Hello @japgolly” the “@japgolly” part gets special treatment.) You parse various types of tokens. You might have different locations to type text, and rules about which tokens are allowed in each location. You want the compiler to enforce those rules but you also want to handle tokens generically sometimes. How would you do such a thing in Scala?

Initial Attempts

Ideally you'd define an ADT (algebraic data type) for all tokens possible, then create new types for each location that form a subset of tokens allowed. If that were possible, here's what that would look like.

sealed trait Token
case class  PlainText(t: String)  extends Token
case object NewLine               extends Token
case class  Link(url: String)     extends Token
case class  Quote(q: List[Token]) extends Token

// type BlogTitle   = PlainText | Link    | Quote[BlogTitle]

// type BlogComment = PlainText | NewLine | Quote[BlogComment]

Now Scala won't let us create our BlogTitle type like shown above. It doesn't have a syntax for coproducts (which is what BlogTitle and BlogComment would be, also called “disjoint unions” and “sum-types”) over existing types. Seeing as we have control over the definition of the generic tokens, we can be tricky and inverse the declarations like this:

sealed trait BlogTitle
sealed trait BlogComment

sealed trait Token
case class  PlainText(t: String) extends Token with BlogTitle with BlogComment
case object NewLine              extends Token                with BlogComment
case class  Link(url: String)    extends Token with BlogTitle
case class  Quote(q: List[????]) extends Token with BlogTitle with BlogComment
//                        ↑ hmmm...

...but as you can see, we hit a wall when we to Quote, which is recursive. We want a Quote in a BlogTitle to only contain BlogTitle tokens, not just any type of token. We can continue our poor hack as follows.

abstract class Quote[A <: Token] extends Token { val q: List[A] }
case class QuoteInBlogTitle  (q: List[BlogTitle])   extends Quote[BlogTitle]
case class QuoteInBlogComment(q: List[BlogComment]) extends Quote[BlogComment]

Not pleasant. And we're not really sharing types anymore. What else could we do?

We could create separate ADTs for BlogTitle and BlogComment, that would mirror and wrap their matching generic tokens, then write converters from specific to generic. That's a lot of duplicated logic and tedium, plus we now double the allocations and memory needed. Let's try something else...

Shapeless

NOTE: This bit about Shapeless is an interesting detour, but it can be skipped.

We could use Shapeless! Shapeless is an ingeniously-sculpted library that facilitates abstractions that a panel of sane experts would deem impossible in Scala, one such abstraction being Coproducts. Here's what a solution looks like using Shapeless.

(Sorry I thought I had this working but just realised recursive coproducts don't work. I've commented-out Quote[A] for now. There is probably a way of doing this – Shapeless often doesn't take no from scalac for an answer – I'll update this if some kind 'netizen shares how.)

sealed trait Token
case class  PlainText(text: String) extends Token
case object NewLine                 extends Token
case class  Link(url: String)       extends Token
case class  Quote[A](q: List[A])    extends Token

type BlogTitle   = PlainText :+: Link         :+: /*Quote[BlogTitle]   :+: */ CNil
type BlogComment = PlainText :+: NewLine.type :+: /*Quote[BlogComment] :+: */ CNil

/* compiles → */ val title = Coproduct[BlogTitle](PlainText("cool"))
// error    → // val title = Coproduct[BlogTitle](NewLine)

So far so good. What would a Token ⇒ Text function look like?

object ToText extends Poly1 {
  implicit def caseText    = at[PlainText   ](_.text)
  implicit def caseNewLine = at[NewLine.type](_ => "\n")
  implicit def caseLink    = at[Link        ](_.url)
  // ...
}

val text: String = title.map(ToText).unify

Ok I'm a little unhappy because I'm very fold of pattern-matching in these situations, but the above does work effectively. One thing to be aware of with Shapeless, is how it works. To achieve its awesomeness, it must build up a hierarchy of proofs which incurs time and space costs at both compile- and run-time – its awesomeness ain't free. The val title = ... statement above creates at least 7 new classes at runtime, where I want 1. Depending on your usage and needs, that overhead might be nothing, but it might be significant. It's something to be aware of when you decide on your solution.

Zero-overhead Recursive ADT Coproducts

There's another way. I mentioned “zero-overhead” and it can be done. Here is a different solution that relies solely on standard Scala features, one such feature being path-dependent types.

You can create an abstract ADT, putting each constituent in a trait, then simply combine those traits into an object to have it reify a new, concrete, sealed ADT. Sealed! Let's see the new definition:

// Generic

sealed trait Base {
  sealed trait Token
}

sealed trait PlainTextT extends Base {
  case class PlainText(text: String) extends Token
}

sealed trait NewLineT extends Base {
  case class NewLine() extends Token
}

sealed trait LinkT extends Base {
  case class Link(url: String) extends Token
}

sealed trait QuoteT extends Base {
  case class Quote(content: List[Token]) extends Token
}

// Specific

object BlogTitle   extends PlainTextT with LinkT    with QuoteT
object BlogComment extends PlainTextT with NewLineT with QuoteT

Now let's use it:

   List[BlogTitle.Token](BlogTitle.PlainText("Hello")) // success
// List[BlogTitle.Token](BlogTitle.NewLine)   ← error: BlogTitle.NewLine doesn't exist
// List[BlogTitle.Token](BlogComment.NewLine) ← error: BlogComment tokens aren't BlogTitle tokens


// Specific
val blogTitleToText: BlogTitle.Token => String = {
  // case BlogTitle.NewLine   => ""     ← error: BlogTitle.NewLine doesn't exist
  // case BlogComment.NewLine => ""     ← error: BlogComment tokens not allowed
  case BlogTitle.PlainText(txt) => txt
  case BlogTitle.Link(url)      => url
  // Compiler warns missing BlogTitle.Quote(_) ✓
}

// General
val anyTokenToText: Base#Token => String = {
  case a: PlainTextT#PlainText => a.text
  case a: LinkT     #Link      => a.url
  case a: NewLineT  #NewLine   => "\n"
  // Compiler warns missing QuoteT#Quote ✓
}


// Recursive types
val t: BlogTitle  .Quote => List[BlogTitle  .Token] = _.content
val c: BlogComment.Quote => List[BlogComment.Token] = _.content
val g: QuoteT     #Quote => List[Base       #Token] = _.content

Look at that! That is awesome. These are some things that we get:

  • No duplicated definitions or logic.
  • Generic & specific hierarchies are sealed, meaning the compiler will let you know when you forget to cater for a case, or try to cater for a case not allowed.
  • Children of recursive types have the same specialisation.
    Eg. a BlogTitle can only quote using BlogTitle tokens.
  • Tokens can be processed generically.
  • Zero-overhead. No additional computation or new memory allocation needed to store tokens, or move them into a generic context. No implicits.
  • Nice, neat pattern-matching which makes me happy.
  • It's just plain ol' Scala traits so you're free to encode more constraints & organisation. You can consolidate traits, add type aliases, all that jazz.

There you go. Seems like a great solution to this particular scenario.

Nothing is without downsides though. Creation will likely be a little hairy; imagine writing a serialisation codec – the Generic ⇒ Binary part will be easy but Binary ⇒ Specific will be more effort. In my case I will only create this data thrice {serialisation, parsing, random data generation} but read and process it many, many times. Good tradeoff.