Orphan instances in Scala
scalatypeclasseshaskellinstancesstyle
1116 Words … ⏲ Reading Time: 5 Minutes, 4 Seconds
2016-07-08 00:00 +0000
Orphan Instances in Scala
Type classes in Scala are interesting, in that even though they aren’t part of the language, they are widely used. This is mostly attributed to type classes’ usefulness and the ability for Scala.
Type classes first appeared in Haskell as a way to implement overloading in a principled way. Since then many other applications have been found for them. I will not describe type classes in detail (if you are unfamiliar with them, you should read Sohum Banerjea’s excellent intro), instead I would like to focus on orphan instances.
An orphan instance in Scala means an instance that exists neither in the type’s companion object nor the type class’ companion object. For example:
object FooPackage {
case class Foo(msg: String)
}
object DoFooPackage {
trait DoFoo[A] {
def doSomething(f: A): String
}
object DoFoo {
def apply[A](implicit ev: DoFoo[A]) = ev
}
}
object InstanceFoo {
implicit object FooInst extends DoFoo[Foo] {
def doSomething(f: Foo) = "I am a Foo! My msg is: " + f.msg
}
}
In the above example, FooInst
is said to be an “orphan instance”. This is
because it is not attached to neither DoFoo
or Foo
(via their companion
objects). This means that in order to use the type class, we must import three
packages: the package with Foo
, the package with DoFoo
and the package with
the instance declaration (in the code above, we used objects to represent
packages).
import FooPackage._
import DoFooPackage._
import InstanceFoo._
DoFoo[Foo].doSomething(Foo("Hello, World!"))
Something you will notice from this style is that we can now define a different
instance for Foo and import it in order to change doSomething
’s behaviour.
For example:
object InstanceFooV2 {
implicit object FooInst2 extends DoFoo[Foo] {
def doSomething(f: Foo) = "I am a second version of Foo! I say: " + f.msg
}
}
Now we can change the way the program works by simply changing an import! If the previous statement didn’t scare you, it should have.
The examples that we have used so far have been quite simple, we are importing a couple of packages and that’s it. But in reality, your import statement is probably 10-20 (or maybe even more) lines long, and filled with wild-card imports, this leads to a couple of problems.
The first problem has to do with referential transparency. If you see 1 + 1
anywhere in your code, you would like it to always return 2
; it would be
quite odd if in some parts of your code 1 + 1 = 3
. Same applies to
type-classes, if we see doSomething(Foo("Hello"))
, it would be great if that
didn’t change next time we see it. However, having multiple orphan instances
breaks this transparency; what doSomething(Foo("Hello"))
does is now
dependent on one of the many imports at the top of the file. This can make
refactoring and cleaning a very scary process.
The second problem you might encounter when using orphans is that changing a
single line in your (very long) import statement can have drastic effects on
the program while still compiling. This happens when declaring overlapping
instances. This won’t happen with the example above since importing both
InstanceFoo
and InstanceFoo2
(and then trying to call doSomething
) would
have given an implicit conflict compile error due to both of them being instances
for the same exact type (Foo). However, this doesn’t happen with overlapping
instances.
object ListInstance {
implicit def listInstance[A](implicit ev: DoFoo[A]): DoFoo[List[A]] = new DoFoo[List[A]] {
def doSomething(list: List[A]) =
"I am a list...of something that defines `DoFoo`:\n\t" + list.map(e => ev.doSomething(e)).mkString(",")
}
}
object ListofFooInstance {
implicit object ListFooInst extends DoFoo[List[Foo]] {
def doSomething(listOfFoo: List[Foo]) =
"I am a specific instance of list of foos!\n\t" + listOfFoo.map(e => doSomething(e)).mkString("\n")
}
}
Above, we made a generic instance for the container “List”. As part of the
instance definition, we stated that the inner type has to also define a DoFoo
instance; this means that doSomething(List(Foo("Hello"), Foo("World")))
will
work fine. However, we also defined a specific instance for List[Foo]
. What
would happen if we import both of them? According to the Scala language
specification:
If there are several eligible arguments which match the implicit
parameter’s type, a most specific one will be chosen using the rules of
static overloading resolution (§6.26.3).
This means that the ListofFooInstance
will be picked if we import both. But,
more importantly, if we remove it from the import statement, it will then pick
the ListInstance
and continue to compile even though it is doing something
very different. This can, again, make cleaning and refactoring a painful
process.
So what if you do actually want to have a different representation (in a type-class) for the same type? This is where value classes come to the rescue. A value class allows us to create a new type that exists only at compile time: at runtime, it is replaced by the underlying type. But because it is a different type, we can create a new, non-orphan instance for it. And because it is a simple type wrapper, we can quickly and easily switch from it to the underlying type:
package NewFoo
case class NewFoo(toFoo: Foo) extends AnyVal
object NewFoo {
implicit object NewFooInst extends DoFoo[NewFoo] {
def doSomething(nf: NewFoo) =
"A new message for Foo! It is: " + nf.toFoo.msg
}
}
Of course, this means that in order to use this new functionality you have to
wrap Foo
, like so doSomething(NewFoo(Foo("Hello")))
. However, it is now
explicit that this is different; we can expect that line to do the same thing
everywhere and never what doSomething(Foo("Hello"))
would do.
Now, this does not mean you should avoid orphans like the plague. There are
certain cases where they can’t be avoided (or avoiding them would be too annoying).
For example, if you have two libraries: one has a case class you would like to
use and the other has a type class you would also like to use. You could create
a new type wrapper (like NewFoo
) around the class, but if you are going to
be using said class extensively, then maybe it is ok to simply create an orphan
instance for it (just make sure you don’t create more than one). If you have
access to either the type class or the case class, just place the instance in
either one’s companion object.
In conclusion, orphan instances can be helpful at time, but they can quickly grow unwieldy as your codebase grows and becomes more complex if not kept in check. Value classes can help avoid them all together, at the cost of having to wrap unwrap values. But these may be worth the cost as your codebase grows and your type classes become more complex.