Skip to content

Instantly share code, notes, and snippets.

@sam
Last active December 21, 2015 09:19
Show Gist options
  • Save sam/6284227 to your computer and use it in GitHub Desktop.
Save sam/6284227 to your computer and use it in GitHub Desktop.
Implicits in Scala
name := "implicits"
version := "1.0-SNAPSHOT"
scalaVersion := "2.10.2"
scalacOptions in ThisBuild ++= Seq(
"-language:_",
"-feature",
"-unchecked",
"-deprecation")
// You can execute this file by downloading it to a directory,
// changing to it, and running: sbt run
object ImplicitMethodCalls extends App {
// Again we start off with an Int
val i = 1
// Now we want to do something really evil.
// We want to be able to treat Ints like Seqs so we
// can map them.
// We could do it the right way and just write this:
val plusOne = Seq(i).map { n => n + 1 }
println(plusOne)
// But that's sane. We're going for EVIL.
implicit def iAmEvil(i: Int): Seq[Int] = Seq(i)
// Now we can call Seq methods on Int!
// Int has a "+" method obviously. Let's try that:
println(i + 10)
// Right. So that worked as expected. Int has a "+"
// method so no conversion necessary.
// Int *doesn't* have a "map" method though.
// Let's try that:
println(i.map(_ + 2))
// WHOA.
// So what happened is this: The compiler doesn't
// find a "map" method on Int. Normally this is where
// you'd get a compiler error. Not so quick though!
// First it checks it's list of in-scope implicits. Are there
// any implicits that convert an Int to a thing that has a "map"
// method? Why yes. "iAmEvil" will convert an Int to a Seq[Int].
// And sequences have a map method. We're back in business!
// NOTE: This search is only performed in the context of a
// missing method. Similar to Ruby's method_missing. If
// both types (Int and Seq[Int]) have a method (the "+" method for
// example), the compiler is going to resolve the one that
// makes the most sense (the one that doesn't actually require
// any conversion to work: Int.+ in this case).
// What if you wanted to add a whole set of methods though?
implicit class Cow(i: Int) {
def moo = println("MOO!")
def totes = println("I AM TOTES A COW!")
}
// Implicit classes are basically just like implicit methods in the way that
// they're resolved. Does Int have a "moo" method? Nope? OK. Is there an
// implicit in-scope that takes an Int and has a "moo" method? Yup. Cow.
// The moral of the story is that for implicits it doesn't matter if your type
// is a constructor argument or a method parameter. It just matters if there's
// an X that can convert it to a Y that has a method Z. Proof:
i.moo
i.totes
// There's that word "convert" again. As you've probably figured out,
// implicit method calls are really no different than implicit type conversion.
// It's the same functionality that enables both. We're just giving the use-cases/
// techniques a label. At the end of the day they're fundamentally the same as
// far as the compiler is concerned.
}
object ImplicitParameters extends App {
// Implicit Parameters are a bit different. Whereas
// Implicit Method Invocation or Implicit Type Conversion
// only needs to have an implicit in-scope to turn an A into a B,
// an implicit parameter needs to both have an in-scope value
// defined as an implicit as well as have an implicit parameter
// in it's signature:
val i = 1
implicit val j = 2
def foo(i: Int)(implicit j: Int) = i + j
println(foo(i))
// The implicit parameter itself declares an implicit value, so if
// you had another function that took an implicit parameter of
// that type, and you called it within the body of your method,
// you wouldn't have to explicitly pass it on.
// This is actually a common technique for "contextual" values
// like scala.concurrent.ExecutionContext. Like so (we'll just stub a
// Trait for this example):
trait ExecutionContext
def nestSomeOperation(i: Int)(implicit ec: ExecutionContext) = {
weNeedToGoDeeper(i)
}
def weNeedToGoDeeper(i: Int)(implicit ec: ExecutionContext) = {
println(s"$i INCEPTION!!!")
}
// A common use-case for needing an ExecutionContext is when
// dealing with Futures.
//
// Imagine for a moment that println was a Future that required
// an ExecutionContext.
//
// Sure, you could import the default ExecutionContext at the top
// of every file, and your methods could implicitly use that without
// having to declare it as an implicit parameter, but then you're
// locked to it in all your source code. If in another project your
// buddy Bob wants to provide his own ExecutionContext so he
// can tune the threads, timeouts, etc, he's out of luck. You've
// gone and hard-coded an imported implicit.
// So what can you and Bob work out?
// Instead write your code like above. Then it'll work for projects that
// are fine with the default since they can just add:
// import ExecutionContext at scala.concurrent.ExecutionContext.Implicits.global
// to their callers. In Bob's case he can explicitly pass his
// own ExecutionContext though and the implicit parameters in the
// nested methods will make it flow through system, guaranteeing
// the right context is used:
val myEC = new Object with ExecutionContext {}
nestSomeOperation(i)(myEC)
// You'll want to avoid declaring an implicit in your library's code
// since you'd be taking away user choice again since you can't
// have two implicits of the same type in scope. It produces an ambiguous
// implicit error. So that would prevent Bob from declaring "myEC" as an
// implicit value and having everything resolve automatically for him.
}
// You can execute this file by downloading it to a directory,
// changing to it, and running: sbt run
object ImplicitTypeConversionExample extends App {
// So we start off with an Int
val i = 1
// And we define a method to print Strings
def doit(s: String) = println(s"I'VE GONE AND DONE IT! $s")
// Now we want to print our Int:
// doit(i)
// That won't actually even compile since an Int is clearly not a String.
// We can get around that though by supplying an Implicit method
// that takes an Int parameter and returns a String. Thus satisfying
// our method call.
implicit def intToString(i: Int): String = i.toString
// Now our code will work!
doit(i)
// What happens is that the compiler knows it can't call the method the
// way we've written it, so it checks if there are any implicit Int => String
// functions in-scope. This is called "implicit type conversion". It finds
// the "intToString" method in it's list of in-scope implicits, sees that
// using it would let us call our doit(String) method, and so uses it.
//
// The name of our implicit method is unimportant.
//
// That's worth repeating. When first starting out with implicits, you'll
// sooner or later find yourself in a situation where things aren't working
// the way you thought they should.
//
// The name of our implicit method is unimportant!
//
// It doesn't matter at all (other than it not conflict with something else obviously).
// You'll be tempted to rename it to see if that makes things work. It won't.
// Because...
//
// THE NAME OF OUR IMPLICIT METHOD IS UNIMPORTANT.
//
// The only thing that matters is that it's marked as "implicit", and
// the function signature of "Int => String" to satisfy the caller.
//
// On a related note, the name of the parameters is equally unimportant.
//
// You'd never expect our doit(i:Int) method to change behavior just because
// you changed the parameter symbol "i" to "thisIsMyInteger" right? Same deal here.
}
object ImplicitZChaining extends App {
// We just tossed in a Z to get gist.github to sort this
// example file after the others. :-p
// You can chain implicits together. Check this:
val a = 1
implicit class B(i: Int) {
def c(implicit d: String) = i.toString + d
}
implicit val d = "!"
println(a.c)
// Let's work backwards:
// "a" is an Int. Int doesn't have a "c" method.
// So the compiler looks for something that takes
// an Int and has a "c" method. It finds the
// implicit class B. Cool beans. But wait! There's more!
//
// The "c" method on "B" requires a String. Luckily we've
// made the parameter implicit, so it can pick up any
// matching implicits in scope of the caller.
//
// We declare an implicit String "d" with a value of "!" then.
// So "c"'s method signature can be satisfied, and ultimately
// compile down to: 1.toString + "!"
// One last thing to keep in mind that can serve to confuse:
// import statements are local to the file or scope you declare
// them in. By itself this doesn't really appear to have much
// to do with implicits, but since you'll often be importing
// implicits from other objects you'll probably bump into it
// sooner rather than later and wonder why Class S of Trait T
// isn't picking up the implicit you clearly imported in T.
// The solution is to make them something that _is_ inherited.
// Like a val, or method call.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment