- Implicit values and parameter dependency injection
- Objects can be functions
- Traits are like interfaces
An InputStream;
A randomThing.Singing? Concurring?
Previously we talked about how implicit values are a simple form of parameter dependency injection. Typeclasses are a very specific way to use this feature. Today we'll explore them.
A typeclass is an "adapter" between one thing and another, implicitly injected into a method.
What???
Let's break that down.
While typeclasses don't specifically require the Adapter Pattern, it's an easy place to start, so...
With the adapter pattern, an adapter trait
(or abstract class
) implements a common API used by a method/function to "adapt" one object's behavior or data to be used by another. Or, said a bit differently, an adapter provides a "bridge" that allows two classes to work together.
Here's an adapter for opening InputStreams from Random Things:
import java.io._
import java.net._
import scala.io.{Source, Codec}
trait StreamAdapter[SRT] { def apply(someRandomThing: SRT): InputStream }
Now with StreamAdapter[SRT] we can interface between SRTs (Some Random Things) and InputStreams like this:
def readRandomThingFromInputStream_ReturnItAsAListOfString[SRT](
someRandomThing: SRT,
openStream: StreamAdapter[SRT]
): List[String] = {
val stream = openStream(someRandomThing)
val content = Source.fromInputStream(stream).getLines.toList
stream.close()
content
}
And to use this, We'll need some StreamAdapter implementations:
class FileStreamAdapter extends StreamAdapter[File] { override def apply(f: File) = new FileInputStream(f) }
class UrlStreamAdapter extends StreamAdapter[URL] { override def apply(u: URL) = u.openStream }
Then we can readRandomThingFromInputStream
using the adapter classes above.
readRandomThingFromInputStream_ReturnItAsAListOfString(new File("/etc/passwd"), new FileStreamAdapter).take(2)
// res0: List[String] = List(
// "root:x:0:0:root:/root:/bin/bash",
// "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin"
// )
readRandomThingFromInputStream_ReturnItAsAListOfString(new URL("http://www.cs.columbia.edu"), new UrlStreamAdapter).take(2)
// res1: List[String] = List(
// "<!DOCTYPE html>",
// "<html lang=\"en-US\" class=\"no-js\">"
// )
This works, and is about the best we could do if this were Java.
But since we're writing in Scala, what if we used what we learned last time along with some clever type inference?
Our goal would be to automatically inject the correct StreamAdapter into an implicit parameter of our readRandomThing...
method and we wouldn't have to explicitly pass any StreamAdapter
implementation at all! Here's how:
The idea is to use type inference to automatically pass the correct implict val
as the adapter. Here's what that looks like:
def readThing[SRT](someRandomThing: SRT)(implicit aStreamFrom: StreamAdapter[SRT]): List[String] = {
val stream = aStreamFrom(someRandomThing)
val content = Source.fromInputStream(stream).getLines.toList
stream.close()
content
}
In the method above, the type of SRT
is defined by the type of the parameter passed as someRandomThing
. Then the StreamAdapter[SRT], by inference, also has to be type-compatible with SRT
.
So, if we pass a File
as someRandomThing
, the aStreamFrom
parameter must be a StreamAdapter[File]
. And since the aStreamFrom
parameter is declared implicitly, we can inject our StreamAdapter[File]
using an implicit val.
Like this:
implicit val fileAdapter = new FileStreamAdapter
implicit val urlAdapter = new UrlStreamAdapter
And when we call readThing
, Scala will automatically infer the correct implicit val based on the type of the thing we pass as someRandomThing
.
readThing(new File("/etc/passwd")).take(2)
// res2: List[String] = List(
// "root:x:0:0:root:/root:/bin/bash",
// "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin"
// )
readThing(new URL("http://www.cs.columbia.edu")).take(2)
// res3: List[String] = List(
// "<!DOCTYPE html>",
// "<html lang=\"en-US\" class=\"no-js\">"
// )
And this is an example of a typeclass in action.
Here's the entire typeclass example again, reworked to tidy a few things.
object TypeclassExample {
import java.io._
import java.net._
import scala.io.{Source, Codec}
// The trait is our typeclass
trait StreamAdapter[SRT] { def apply(s: SRT): InputStream }
// Typeclass instances
implicit object FileStream extends StreamAdapter[File] { override def apply(f: File) = new FileInputStream(f) }
implicit object UrlStream extends StreamAdapter[URL] { override def apply(u: URL) = u.openStream }
// Typeclass usage: parameter injection along with type inference
def thingAsStrings[SRT](someRandomThing: SRT)(implicit aStreamFrom: StreamAdapter[SRT]): List[String] = {
val stream = aStreamFrom(someRandomThing)
val content = Source.fromInputStream(stream).getLines.toList
stream.close()
content
}
}
Here we're using SRT
both as the type of someRandomThing
and as the type parameter of the aStreamFrom
implicit parameter. The consequence is that the type of the object we pass as someRandomThing
must match the type parameter of the StreamAdapter
implementation that will be implicitly injected into aStreamFrom
.
- If we pass a
File
to thesomeRandomThing
parameter, the only thing that can be implicitly injected into theaStreamFrom
parameter is an instance ofStreamAdapter[File]
: in the example above, theFileStream
object. - If we pass a
Url
to thesomeRandomThing
parameter, the only thing that can be implicitly injected into theaStreamFrom
parameter is an instance ofStreamAdapter[Url]
: in the example above, theUrlStream
object.
In other words, in the following example:
import TypeclassExample._
thingAsStrings(new File("/etc/passwd")).take(2)
// res4: List[String] = List(
// "root:x:0:0:root:/root:/bin/bash",
// "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin"
// )
thingAsStrings(new URL("http://www.cs.columbia.edu")).take(2)
// res5: List[String] = List(
// "<!DOCTYPE html>",
// "<html lang=\"en-US\" class=\"no-js\">"
// )
- Scala automatically chooses
FileStream
to retrieve content in the first case because we passed aFile
as the someRandomThing (SRT
) parameter. - Similarly, scala automatically chooses
UrlStream
to retrieve content in the first case because we passed aUrl
as the someRandomThing (SRT
) parameter.
In order to enable our thingAsStrings
function to handle another data type, all we have to do is implement a new StreamAdapter
implementation for our new data type and declare an implicit value for Scala to inject into the aStreamFrom
parameter.
What can we do with this?
- Using typeclasses,
thingsAsStrings
can handle any input type now or in the future, regardless of the inheritence hierarchies available or any other constraint currently present in our code. - By decoupling the implementation of
thingsAsStrings
from its input type, we can trivially implement aStreamAdapter
that injects test data. The effect is to remove I/O operations fromthingsAsStrings
for testing purposes without resorting to heavy-weight alternatives like mocking frameworks.
This is the tip of the iceberg. As you continue your Scala exploration you'll encounter many more uses for Typeclasses.