Lowering in Scala

https://paperize.co/?s=unsplash

In most programming languages there are many features that are implemented on top of a subset of that language. These features are often referred to as syntactic sugar. Often times thinking about these pieces of "sugar" in their lowered form allows us to comprehend what is going on a little bit better as well as removing some of the magic.

Case Classes

One of my personal favorite features of Scala is the case class.  Case classes on the surface seem pretty simple but there are a lot of moving parts to them.

Given a case class such as case class Taco(meat: Boolean, lettuce: Boolean) this gets several traits attached to it like Product and Serializable

class Taco(val meat: Boolean, val lettuce: Boolean) extends Product with Serializable {
  override def productArity: Int = 2

  override def productElement(n: Int): Any = n match {
    case 0 => meat
    case 1 => lettuce
  }

  override def canEqual(that: Any): Boolean = that.isInstanceOf[Taco]

  def copy(meat: Boolean = this.meat, lettuce: Boolean = this.lettuce) = new Taco(meat, lettuce)
}

Then we get some nice helper methods like apply, that allow use to easily make case classes without new, and unapply which allows us to pattern match on a type

object Taco {
  def apply(meat: Boolean, lettuce: Boolean): Taco = new Taco(meat, lettuce)

  def unapply(arg: Taco): Option[(Boolean, Boolean)] = Some((arg.meat, arg.lettuce))
}

Curried Functions

Next up is a nice functional programming feature that allows us to take a method like def add(a: Int, b: Int) = a + b and create partially applied version of that function like so:

val plus2 = add(2, _)

This then gets lowered to an anonymous method with the applied parameters:

val plus2 = ((x$1: Int) => add(2, x$1))

or

def plus2(x$1: Int) = add(2, x$1)

For comprehensions

For comprehensions get lowered into their map and flatMap representations:

def product[A, B](optionA: Option[A], optionB: Option[B]): Option[(A, B)] =
  for {
    a <- optionA
    b <- optionB
  } yield (a, b)

becomes:

def product[A, B](optionA: Option[A], optionB: Option[B]): Option[Tuple2[A, B]] =
  optionA.flatMap(a => optionB.map(b => Tuple2.apply(a, b)));

Context Bounds

For context bounds we start with something like:

def performFunction[A: Encoder](a: A): Json = ???

which gets turned into

def performFunction[A](a: A)(implicit ev: Encoder[A]): Json = ???

Dive in

If you want to learn more and see what things look like under the hood with Scala it is pretty easy and you can do it with the following code. Have fun!

import scala.reflect.runtime.universe._


object Main extends App {

  println(showCode(reify {
    // the code you want lowered goes here
    def performFunction[A: Encoder](a: A): Json = ???
  }.tree))

}

If you liked this article follow me on twitter @_mrunleaded_