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_