http://caryrobbins.com/zero-talk
Cary Robbins
June 5, 2019
- Software development consultant for Estatico Studios
- Primarily work in Scala and Haskell, but love digging into new languages.
- Interested in collaborating? Send me a note at: cary@estatico.io
- Polymorphic values
- NewTypes
- Generic programming
Note: We're going to discuss 3 different strategies for improving existing abstractions with implementations that do not incur runtime overhead.
We will be using
asInstanceOf
a lot.
It's cool don't worry.
Note:
Sure, maybe it's not your bottleneck. Your bottleneck is probably IO. But this doesn't mean we shouldn't discover ways of optimizing our compiled code to eliminate overhead which is truly unnecessary.
Note:
Yes, this is true. Don't optimize for the sake of optimizing, and you should benchmark if you are doing heavy optimizations.
But what we're going to cover here are general optimizations which should not only improve runtime performance, but actually give you new ways of programming that you may not be aware of.
(Although, many of these work just as good if not better in ScalaJS)
implicit def eitherFunctor[L]: Functor[Either[L, ?]] = new Functor[Either[L, ?]] {
override def map[A, B](fa: Either[L, A])(f: A => B): Either[L, B] =
fa.right.map(f)
}
implicitly[Functor[Either[String, ?]]] // $anon$1@2cf46f9e
implicitly[Functor[Either[String, ?]]] // $anon$1@7cd0fda6 ← new instance!
Note: Here's an example of a polymorphic type class instance, the Functor instance for Either.
Functor's type argument must be a type which itself takes a single type argument. Either takes two, so we must pass the Left type argument into the implicit def.
Problem is...each time we summon the implicit instance, we alloc a new instance.
private val eitherFunctorCached = new Functor[Either[Nothing, ?]] {
override def map[A, B](fa: Either[Nothing, A])(f: A => B): Either[Nothing, B] =
fa.right.map(f)
}
implicit def eitherFunctor[L]: Functor[Either[L, ?]] =
eitherFunctorCached.asInstanceOf[Functor[Either[L, ?]]]
implicitly[Functor[Either[String, ?]]] // $anon$1@3c466028
implicitly[Functor[Either[String, ?]]] // $anon$1@3c466028 ← same instance!
Note: Here we implement the same Functor instance, except this time instead of making it polymorphic, we stub in the Nothing type for the Left type param so we can cache it into a val. We then have the implicit def simply call the val and cast it to the appropriate type.
Type parameters are erased at runtime
implicit def eitherFunctor[L]: Functor[Either[L, ?]] = new Functor[Either[L, ?]] {
override def map[A, B](fa: Either[L, A])(f: A => B): Either[L, B] =
fa.right.map(f)
}
// Bytecode
public eitherFunctor()LFunctor;
NEW $anon$1 // new anon Functor instance
DUP
INVOKESPECIAL $anon$1.<init> ()V // Functor constructor "init"
ARETURN
Note:
Why does this work? Type parameters are erased at runtime.
private val eitherFunctorCached = new Functor[Either[Nothing, ?]] {
override def map[A, B](fa: Either[Nothing, A])(f: A => B): Either[Nothing, B] =
fa.right.map(f)
}
implicit def eitherFunctor[L]: Functor[Either[L, ?]] =
eitherFunctorCached.asInstanceOf[Functor[Either[L, ?]]]
// Bytecode
public eitherFunctor()LFunctor; // ← Runtime type is still Functor
ALOAD 0
INVOKESPECIAL eitherFunctorCached ()LFunctor;
ARETURN // Same runtime type ↑
@cached
implicit def eitherFunctor[L]: Functor[Either[L, ?]] = new Functor[Either[L, ?]] {
override def map[A, B](fa: Either[L, A])(f: A => B): Either[L, B] =
fa.right.map(f)
}
implicit def eitherFunctor[L]: Functor[Either[L, ?]] = __cached__eitherFunctor.asInstanceOf[Functor[Either[L, ?]]]
private val __cached__eitherFunctor = new Functor[Either[Nothing, ?]] { override def map[A, B](fa: Either[Nothing, A])(f: A => B): Either[Nothing, B] = fa.right.map(f) }
Haskell's newtype
gives us type safety without the cost
newtype WidgetId = WidgetId String
myId = WidgetId "a" -- ← Is just a String at runtime
upperId :: WidgetId -> WidgetId
upperId (WidgetId s) = WidgetId (map toUpper s)
-- The unwrap ↑ and rewrap ↑ do not occur at runtime
Note:
In Haskell, the newtype
keyword allows us to define a new type
which incurs no overhead and is just some other type at runtime.
In this case, we're defining a WidgetId
to improve type safety
so we know that this value is more than just a String
.
Wrapping and unwrapping newtype values incurs no overhead at runtime; it is all omitted during compilation.
instance ToJSON WidgetId where
toJSON (WidgetId s) = object [ "widgetId" .= toJSON s ]
newtype Sum = Sum Int
instance Monoid Sum where mempty = 0 mappend (Sum x) (Sum y) = Sum (x + y)
newtype Product = Product Int
instance Monoid Product where mempty = 1 mappend (Product x) (Product y) = Product (x * y)
Note: Another common use case for newtypes is to give us the ability to provide alternative type class implementations.
Here, instead of using String's default ToJSON instance, we can provide our own implementation for our WidgetId.
The canonical example of this is that Int forms a Monoid via addition and multiplication. Instead of having to decide which instance we want Int to use, we can say that Int is not a Monoid on its own, and that only Sum and Product (which are newtype wrappers around Int) are.
ids :: [String]
ids = ["a","b","c","d","e"]
xs :: [WidgetId]
xs = coerce ids :: [WidgetId]
final case class WidgetId(value: String) extends AnyVal
def getWidget(id: WidgetId): Widget = ???
// Bytecode
public getWidget(Ljava/lang/String;)LWidget;
Note: Here is a value class. This allows us to define a new class which, at runtime, should be represented as a String. We can confirm this by looking at the compiled bytecode.
def optId = Option(WidgetId("a"))
// Bytecode
public optId()Lscala/Option;
GETSTATIC scala/Option$.MODULE$ : Lscala/Option$;
NEW WidgetId // ← We don't like this
DUP
LDC "a"
INVOKESPECIAL WidgetId.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL scala/Option$.apply (Ljava/lang/Object;)Lscala/Option;
ARETURN
Note: Let's see what's wrong with value classes. One of the big problems with them is when you try to use them with generics.
Also remember that since generics are erased at runtime, this method returns Option, not Option of String or WidgetId.
def unOptId = optId.get
// Bytecode
public unOptId()Ljava/lang/String;
ALOAD 0
INVOKEVIRTUAL optId ()Lscala/Option;
INVOKEVIRTUAL scala/Option.get ()Ljava/lang/Object;
CHECKCAST WidgetId // ← Why the allocation occurs
INVOKEVIRTUAL WidgetId.value ()Ljava/lang/String; // ← Unwrap ლ(ಠ益ಠლ)
ARETURN
Note:
- The allocation must occur so the JVM can validate that we actually have a WidgetId at runtime.
- Interestingly enough, we then just unwrap the value we had, getting back the String we wrapped to begin with.
def convIdList(xs: List[String]): List[WidgetId] =
xs.map(WidgetId.apply) // ← Hey JIT, try to optimize this
Note: Any time we need to convert container elements to value class instances, we'll always incur a performance overhead; this just can't be optimized away, not even by the JIT.
// Adapted from scalaz
type @@[A, T] = { type Tag = T; type Self = A }
def tag[A, T](a: A): A @@ T = a.asInstanceOf[A @@ T]
trait WidgetIdTag
type WidgetId = String @@ WidgetIdTag
def WidgetId(s: String): WidgetId = tag[String, WidgetIdTag](s)
Note:
Scalaz and shapeless provide something called tagged types
.
Note that Scalaz's and shapeless' implementation of tagged types
are different, but we'll cover the difference later.
A tagged type
will be defined with this @@
type operator. We'll
use a refinement to hold onto the types passed in but not expose it
so the compiler sees this as a unique type. This refinement will
be represented as a java Object at runtime.
To construct one, we'll just perform a cast using .asInstanceOf
.
This ends up being fine since any value we pass in will be an
instance of Object
.
def tag[A, T](a: A): A @@ T = a.asInstanceOf[A @@ T]
// Bytecode
public tag(Ljava/lang/Object;)Ljava/lang/Object;
ALOAD 1
ARETURN
def WidgetId(s: String): WidgetId = tag[String, WidgetIdTag](s)
// Bytecode
public WidgetId(Ljava/lang/String;)Ljava/lang/Object;
ALOAD 0
ALOAD 1
INVOKEVIRTUAL tag (Ljava/lang/Object;)Ljava/lang/Object;
ARETURN
def optId = Option(WidgetId("a"))
// Bytecode
public optId()Lscala/Option;
GETSTATIC scala/Option$.MODULE$ : Lscala/Option$;
ALOAD 0
LDC "a"
INVOKEVIRTUAL WidgetId (Ljava/lang/String;)Ljava/lang/Object;
INVOKEVIRTUAL scala/Option$.apply (Ljava/lang/Object;)Lscala/Option;
ARETURN
def unOptId = optId.get
// Bytecode
public unOptId()Ljava/lang/Object;
ALOAD 0
INVOKEVIRTUAL optId ()Lscala/Option;
INVOKEVIRTUAL scala/Option.get ()Ljava/lang/Object;
ARETURN
def convIdList(ids: List[String]): List[WidgetId] =
ids.asInstanceOf[List[WidgetId]]
// Bytecode
public convIdList(Lscala/collection/immutable/List;)Lscala/collection/immutable/List;
ALOAD 1
ARETURN
Note:
So far this isn't looking too bad. These are all constant operations
that are really easy for the JIT to inline. We have no new
allocations
and no checkcasts
.
- How do we deal with type class instances/implicits?
- How do we add methods?
- Can we make casting safer?
- Can we make it less ad hoc and reduce boilerplate?
type WidgetId = WidgetId.Type
object WidgetId {
type Base = Any { type WidgetId$newtype }
trait Tag extends Any
type Type <: Base with Tag
def apply(value: String): WidgetId = value.asInstanceOf[WidgetId]
implicit final class Ops(private val me: WidgetId) extends AnyVal {
def value: String = me.asInstanceOf[String]
}
}
Note:
Let's start with a companion object. This should be where implicits are defined for our newtype.
Base
-
- A unique type that gets compiled as Object in the bytecode
- Any
- Signals we want Object
- Aid in array construction for primitives
- Refinement
- Ensures that Any is actually used as the base type instead of trait mixins
Tag
- A unique trait that we'll mix into our newtype to aid in implicit resolution.
- Similarly as with
Base
above it, we need to extendAny
to help with primitives.
Type
-
- This is our final newtype
- Defined as an abstract type to prevent scalac from expanding the type alias which would break implicit resolution
WidigetId
- Top-level type alias for convenience and simplicity
apply
- Smart constructor for our newtype
Ops
- Extension methods for our newtype
WidgetId("a").toUpperCase // Does not compile
WidgetId("a").value.toUpperCase // Compiles
def upperStr(s: String) = s.toUpperCase
upperStr("a") // Compiles
upperStr(WidgetId("a")) // Does not compile
def upperWidgetId(id: WidgetId) = WidgetId(id.value.toUpperCase)
upperWidgetId("a") // Does not compile
upperWidgetId(WidgetId("a")) // Compiles
trait NewType[Repr] {
type Base = Any { type Repr$newtype = Repr }
trait Tag extends Any
type Type <: Base with Tag
def apply(value: Repr): Type = value.asInstanceOf[Type]
def repr(x: Type): Repr = x.asInstanceOf[Repr]
}
type WidgetId = WidgetId.Type
object WidgetId extends NewType[String] {
implicit final class Ops(private val me: WidgetId) extends AnyVal {
def value: Repr = repr(me)
}
}
Note:
So what does this buy us?
We get a smart constructor and destructure for free. We get safe casts for collection types. We get an idiomatic companion object that just works for implicit resolution.
public apply(Ljava/lang/Object;)Ljava/lang/Object;
ALOAD 0
ALOAD 1
INVOKESTATIC NewType.apply$ (LNewType;Ljava/lang/Object;)Ljava/lang/Object;
ARETURN
public static synthetic apply$(LNewType;Ljava/lang/Object;)Ljava/lang/Object;
ALOAD 0
ALOAD 1
INVOKESPECIAL NewType.apply (Ljava/lang/Object;)Ljava/lang/Object;
ARETURN
public default apply(Ljava/lang/Object;)Ljava/lang/Object;
ALOAD 1
ARETURN
def widgetIdValue = WidgetId("a").value
// Bytecode
public widgetIdValue()Ljava/lang/String;
GETSTATIC WidgetId$.MODULE$ : LWidgetId$;
GETSTATIC WidgetId$.MODULE$ : LWidgetId$;
LDC "a"
INVOKEVIRTUAL WidgetId$.apply (Ljava/lang/Object;)Ljava/lang/Object;
INVOKEVIRTUAL WidgetId$.Ops (Ljava/lang/Object;)Ljava/lang/Object;
INVOKEVIRTUAL WidgetId$Ops$.value$extension (Ljava/lang/Object;)Ljava/lang/String;
implicit final class Ops(private val self: Type) extends AnyVal {
def value: Repr = repr(self)
}
// Bytecode
public final value$extension(Ljava/lang/Object;)Ljava/lang/String;
GETSTATIC WidgetId$.MODULE$ : LWidgetId$;
ALOAD 1
INVOKEVIRTUAL WidgetId$.repr (Ljava/lang/Object;)Ljava/lang/Object;
CHECKCAST java/lang/String
ARETURN
def optId = Option(WidgetId("a"))
// Bytecode
public optId()Lscala/Option;
GETSTATIC scala/Option$.MODULE$ : Lscala/Option$;
GETSTATIC WidgetId$.MODULE$ : LWidgetId$;
LDC "a"
INVOKEVIRTUAL WidgetId$.apply (Ljava/lang/Object;)Ljava/lang/Object;
INVOKEVIRTUAL scala/Option$.apply (Ljava/lang/Object;)Lscala/Option;
ARETURN
def unOptId = optId.get
public unOptId()Ljava/lang/Object;
ALOAD 0
INVOKEVIRTUAL optId ()Lscala/Option;
INVOKEVIRTUAL scala/Option.get ()Ljava/lang/Object;
ARETURN
def convIdList(ids: List[String]): List[WidgetId] = ids.asInstanceOf[List[WidgetId]]
// Bytecode
public convIdList(Lscala/collection/immutable/List;)Lscala/collection/immutable/List;
ALOAD 1
ARETURN
type Feet = Feet.Type
object Feet extends NewType.Of[Int]
def mkFeet = Feet(12)
// Bytecode
public mkFeet()Ljava/lang/Object;
GETSTATIC Feet$.MODULE$ : LFeet$;
BIPUSH 12
INVOKESTATIC scala/runtime/BoxesRunTime.boxToInteger (I)Ljava/lang/Integer;
INVOKEVIRTUAL Feet$.apply (Ljava/lang/Object;)Ljava/lang/Object;
ARETURN
trait NewSubType[Repr] {
trait Tag extends Any
type Type <: Repr with Tag
def apply(x: Repr): Type = x.asInstanceOf[Type]
def repr(x: Type): Repr = x.asInstanceOf[Repr]
}
type Feet = Feet.Type
object Feet extends NewSubType[Int] {
implicit final class Ops(private val me: Feet) extends AnyVal {
def value: Int = repr(me)
}
}
Feet(1).signum // Compiles, returns Int
def add(x: Int, y: Int): Int = x + y add(Feet(1), Feet(2)) // Compiles, returns Int
def addFeet(x: Feet, y: Feet): Feet = Feet(x.value + y.value) addFeet(Feet(1), Feet(2)) // Compiles, returns Feet addFeet(1, 2) // Does not compile
def mkFeet = Feet(12)
// Bytecode
public mkFeet()I
GETSTATIC Feet$.MODULE$ : LFeet$;
BIPUSH 12
INVOKESTATIC scala/runtime/BoxesRunTime.boxToInteger (I)Ljava/lang/Integer;
INVOKEVIRTUAL Feet$.apply (Ljava/lang/Object;)Ljava/lang/Object; // oh
INVOKESTATIC scala/runtime/BoxesRunTime.unboxToInt (Ljava/lang/Object;)I // wat
IRETURN
type Feet = Feet.Type
object Feet extends NewSubType[Int] {
override def apply(x: Int): Feet = x.asInstanceOf[Feet]
}
public apply(I)I
ILOAD 1
INVOKESTATIC scala/runtime/BoxesRunTime.boxToInteger (I)Ljava/lang/Integer;
INVOKESTATIC scala/runtime/BoxesRunTime.unboxToInt (Ljava/lang/Object;)I // srsly
IRETURN
type Feet = Feet.Type
object Feet extends NewSubType.Of[Int] {
override def apply(x: Int): Feet = x.asInstanceOf[Feet]
}
% scalac -opt:l:inline ...
public apply(I)I
ILOAD 1
IRETURN // ...Did we just get it to work?
def mkFeet = Feet(12)
public mkFeet()I
GETSTATIC Feet$.MODULE$ : LFeet$;
BIPUSH 12
INVOKEVIRTUAL Feet$.apply (I)I
IRETURN // ...We did!
trait Coercible[A, B] {
final def apply(a: A): B = a.asInstanceOf[B]
}
trait NewType[Repr] {
type Base = Any { type Repr$newtype = Repr }
trait Tag extends Any
type Type <: Base with Tag
def apply(value: Repr): Type = value.asInstanceOf[Type]
def repr(x: Type): Repr = x.asInstanceOf[Repr]
implicit val coerceTo: Coercible[Repr, Type] = Coercible.instance
implicit val coerceFrom: Coercible[Type, Repr] = Coercible.instance
}
WidgetId("foo").coerce[String].toUpperCase
def upper(s: String) = s.toUpperCase
upper(WidgetId("foo").coerce) // Compiles, returns String
implicit def coercibleListTo[A, B](
implicit ev: Coercible[A, B]
): Coercible[List[A], List[B]] = Coercible.instance
List("a","b","c").coerce[List[WidgetId]]
trait TypeRole[A] {
type Role
}
object TypeRole {
def mk[A, R]: TypeRole[A] { type Role = R } =
_instance.asInstanceOf[TypeRole[A] { type Role = R }]
private val _instance = new TypeRole[Nothing] {}
type Nominal[A] = TypeRole[A] { type Role = types.Nominal }
type Representational[A] = TypeRole[A] { type Role = types.Representational }
object types {
sealed trait Representational
sealed trait Nominal
}
}
https://github.com/estatico/scala-newtype
import io.estatico.newtype.macros._
@newtype case class WidgetId(value: String)
@newsubtype case class Feet(value: Int)
@newtype(debug = true) case class WidgetId(value: String)
type WidgetId = WidgetId.Type object WidgetId { type Repr = String type Base = Any { type WidgetId$newtype } abstract trait Tag extends Any type Type <: Base with Tag
def apply(value: String): WidgetId = value.asInstanceOf[WidgetId];
def deriving[TC[_]](implicit ev: TC[Repr]): TC[Type] = ev.asInstanceOf[TC[Type]]
// ... }
@newtype case class Attributes(toList: List[(String, String)])
object Attributes {
implicit val monoid: Monoid[Attributes] = deriving
}
@newtype case class Slice[A](toVector: Vector[A])
object Slice {
implicit val monad: Monad[Slice] = derivingK
}
@newsubtype class PosInt(val toInt: Int)
object PosInt {
def of(x: Int): Option[PosInt] =
if (x < 0) None else Some(x.coerce[PosInt])
}
PosInt(1) // Compile error: PosInt.type does not take parameters
PosInt.of(1) // Some(1): Option[PosInt] PosInt.of(-1) // None: Option[PosInt]
@newtype class Maybe[A](val unsafeGet: A) {
def isEmpty: Boolean = unsafeGet == Maybe._empty
def isDefined: Boolean = unsafeGet != Maybe._empty
def map[B](f: A => B): Maybe[B] =
if (isEmpty) Maybe.empty else Maybe(f(unsafeGet))
def filter(p: A => Boolean): Maybe[A] =
if (isEmpty || !p(unsafeGet)) Maybe.empty else this
...
}
object Maybe {
def apply[A](a: A): Maybe[A] = if (a == null) empty else unsafe(a)
def unsafe[A](a: A): Maybe[A] = a.asInstanceOf[Maybe[A]]
def empty[A]: Maybe[A] = _empty.asInstanceOf[Maybe[A]]
private val _empty = new Empty
private final class Empty { override def toString = "Maybe.empty" }
}
@newtype case class OptionT[F[_], A](value: F[Option[A]]) {
def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] =
OptionT(F.map(value)(_.map(f)))
def flatMapF[B](f: A => F[Option[B]])(implicit F: Monad[F]): OptionT[F, B] =
OptionT(F.flatMap(value)(_.fold(F.pure[Option[B]](None))(f)))
def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] =
flatMapF(a => f(a).value)
...
}
import shapeless._
case class Foo(a: Int, b: String)
val foo = Foo(1, "hey")
val g = Generic[Foo].to(foo)
// g: Int :: String :: HNil
// = 1 :: "hey" :: HNil
foo eq g
// false
(1 :: "hey" :: HNil) == g
// true
new ::(1, new ::("hey", HNil)) == g
// true
sealed trait HList
final case class ::[+H, +T <: HList](head : H, tail : T) extends HList
sealed trait HNil extends HList
sealed trait GList
sealed trait #:[H, T <: GList] extends GList
sealed trait GNil extends GList
object GList {
@newtype case class Of[A, L <: GList](value: A)
object Of { // ^ phantom type
// It's valid (and desirable) to coerce to the tail of our GList
implicit def coerceToTail[A, H, T <: GList]: Coercible[Of[A, H #: T], Of[A, T]] =
Coercible.instance
}
}
trait GProduct[A] { type Repr <: GList }
object GProduct {
type Aux[A, R <: GList] = GProduct[A] { type Repr = R }
def to[A](a: A)(implicit ev: GProduct[A]): GList.Of[A, ev.Repr] = GList.Of(a)
}
trait IsGCons[A] {
type Head
type Tail <: GList
def head(a: GList.Of[A, Head #: Tail]): Head
final def tail(a: GList.Of[A, Head #: Tail]): GList.Of[A, Tail] =
a.coerce[GList.Of[A, Tail]]
}
object IsGCons {
type Aux[A, H, T <: GList] = IsGCons[A] { type Head = H ; type Tail = T }
}
@DeriveGProduct case class Foo(a: String, b: Int, c: Float, d: Double)
object Foo {
implicit val isGCons4: IsGCons.Aux[Foo, String, Int #: Float #: Double #: GNil] =
IsGCons.instance(_.a)
implicit val isGCons3: IsGCons.Aux[Foo, Int, Float #: Double #: GNil] =
IsGCons.instance(_.b)
implicit val isGCons2: IsGCons.Aux[Foo, Float, Double #: GNil] =
IsGCons.instance(_.c)
implicit val isGCons1: IsGCons.Aux[Foo, Double, GNil] =
IsGCons.instance(_.d)
implicit def gProduct: GProduct.Aux[Foo, String #: Int #: Float #: Double #: GNil] =
GProduct.instance
};
trait CsvEncoder[A] {
def encode(a: A): String
}
implicit def gCons[A, H, T <: GList](
implicit
hEnc: CsvEncoder[H],
tEnc: CsvEncoder[GList.Of[A, T]],
isGCons: IsGCons.Aux[A, H, T]
): CsvEncoder[GList.Of[A, H #: T]] =
CsvEncoder.instance(a => hEnc.encode(a.head) + ',' + tEnc.encode(a.tail))
implicit def gSingle[A, H](
implicit
hEnc: CsvEncoder[H],
isGCons: IsGCons.Aux[A, H, GNil]
): CsvEncoder[GList.Of[A, H #: GNil]] =
CsvEncoder.instance(a => hEnc.encode(a.head))
@DeriveGProduct case class Foo(a: String, b: Int, c: Float, d: Double)
object Foo {
implicit val csvFoo = CsvEncoder[Foo] = CsvEncoder.derive[Foo]
}
CsvEncoder.encode(Foo("ya", 2, 8.2f, 7.6))
// "ya,2,8.2,7.6"
- NewType Library - https://github.com/estatico/scala-newtype
@cached
Implementation - https://github.com/estatico/scala-cached- GProduct Implementation - https://github.com/carymrobbins/scala-derivable