While working on a Scala hobby project, I wanted to convert an Option[A]
into an Option[B]
. I’ve noticed that it’s often straightforward to come up with an ugly way to do things in Scala, but with a little more thought it’s almost always possible to find a much more elegant and concise way to do things. I had initially written the following:
def intersect(ray: Ray): Option[Intersection] = { val opt = shape intersect ray // Returns an Option[(DifferentialGeometry, Double)] if (opt.isEmpty) None else { val (dg, t) = opt.get Some(new Intersection(dg, t, this)) } }
This code is part of my Scala ray tracer (which isn’t yet publicly available – at the moment it’s too incomplete). It calls the intersect
method on a shape, which returns an Option
containing two values: a DifferentialGeometry
object that contains information about the intersection point on the surface of the shape, and a Double
which is the distance of the intersection point along the ray.
The intersect
method above is part of class Primitive
, which represents an object in the scene – something that has a shape, a material and other properties. This method converts the Option[(DifferentialGeometry, Double)]
into an Option[Intersection]
– class Intersection
represents an intersection between a primitive and a ray.
I wasn’t satisfied with the code above, and after looking at the Scala API documentation I discovered that it can be written much more concisely by using the collect
method of class Option
, like this:
def intersect(ray: Ray): Option[Intersection] = shape intersect ray collect { case (dg, t) => new Intersection(dg, t, this) }
The collect
method takes a partial function as an argument. It returns None
for inputs for which the partial function isn’t defined, and a Some(...)
for inputs for which the partial function is defined. The partial function can be written as { case value => ... }
. By writing the code this way, I don’t have to deal with checking, unpacking and packing the Option
objects myself, which makes the code nice and clean.
edit: Using map
instead of collect
is a little bit more efficient, as Brian Howard points out. See the comments for details.
Since your function applies to _all_ of the `Some(…)` values coming out of `shape intersect ray`, you could do this more efficiently (no need to check whether the function is defined) with `map` instead of `collect`. Or, you could use the `for` syntax (which is equivalent):
for ((dg, t) <- shape intersect ray) yield new Intersection(dg, t, this)
Thanks Brian!
You’re right, you can also replace
collect
bymap
. I’ve tried it out in my ray tracer, and also using thefor
syntax. There’s no noticeable performance improvement when I do this, though. Looking at the implementation ofmap
andcollect
in classOption
:There is indeed an extra condition in the
if
-statement incollect
, so in principle it’s less efficient.I don’t like the
for
syntax for this; when I see afor
, I’d expect a loop, but there’s not really a loop here.