Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lossyrob/8259bae00d255f9e7589e7c9c1a63c51 to your computer and use it in GitHub Desktop.
Save lossyrob/8259bae00d255f9e7589e7c9c1a63c51 to your computer and use it in GitHub Desktop.
Implicits around TileFeature: Mailing list response Feb 2017

I definitely understand the head spinning, code that relies heavily on implicits can be a tricky stack of code to sort through. Once you get the knack of them, hopefully you are converted into a fan as I was :)

First thing to read through would be this notebook, specifically sections "Organization" and "Type constraints"

Let's first take a look at why the compiling code compiles.

We are calling "tileToLayout" on an "RDD[(ProjectedExtent, Tile)]", with a specific cellType and layout definition. The reason we can call this is because of this implicit, as you pointed out:

https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/spark/src/main/scala/geotrellis/spark/tiling/TilerMethods.scala#L55-L57

class TilerMethods[K, V <: CellGrid: ClassTag: (? => TileMergeMethods[V]): (? => TilePrototypeMethods[V])](val self: RDD[(K, V)]) extends MethodExtensions[RDD[(K, V)]] {
  // ...
  def tileToLayout[K2: SpatialComponent: ClassTag](cellType: CellType, layoutDefinition: LayoutDefinition)
      (implicit ev: K => TilerKeyMethods[K, K2]): RDD[(K2, V)] =
    tileToLayout(cellType, layoutDefinition, Options.DEFAULT)
  // ...
}

There's a set of implicits in play here.

First of all, the TilerMethods class is a "MethodExtensions" class. Which as the linked notebook points out is used for organizing code. The TilerMethods class is implemented by the implicit class here: https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/spark/src/main/scala/geotrellis/spark/tiling/Implicits.scala#L33, and that implicit class is added to the implicit scope because the package object (imported by import geotrellis.spark._) extends it here https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/spark/src/main/scala/geotrellis/spark/package.scala#L62

Let's look at the type parameters of the TilerMethods class. K, the source key type (in our case ProjectedExtent) has no constraints, so can be anything. That's because the specification of K happens later, in the method call. V, the value type (or tile type), does have some constraints. Let's look at them one by one:

V <: CellGrid -> This is a type constraint, meaning we must be a subclass of CellGrid. I actually dislike that this is a subtype instead of a context bounds, and am looking to solve that in a future version. V: ClassTag -> This is a context bound, requiring an implicit ClassTag[V]. This is because Spark needs it. V: (? => TileMergeMethods[V]) -> Here's where the real fun starts. First of all, let's explain the ?.

The (? => T) syntax is provided by Kind Projector (https://github.com/non/kind-projector), and means that we can implicitly convert our V type into some T. This is useful for our whole MethodExtensions design pattern, where we want to qualify our type V as having some methods on it that are implicitly added via MethodExtension types.

So V: (? => TileMergeMethods[V]) means that if I have a V, then I can implicitly convert it to a TileMergeMethods[V], and so call methods on a V that actually are methods of TileMergeMethods[V]. e.g. the following code will work:

val v1: V = ???
val v2: V = ???
v1.merge(v2) // Or some other method of TileMergeMethods

Similarly, V: (? => TilePrototypeMethods[V]) means that I can call methods on V that belong to TilePrototypeMethods.

In our compiling case, type V = Tile. In our non-compiling version, type V = TileFeature[Tile, CustomMetadata]. Tile satisfies these type constraints, by the implicit classes seen in these places: https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/raster/src/main/scala/geotrellis/raster/package.scala#L65 and https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/raster/src/main/scala/geotrellis/raster/package.scala#L66. Does TilePrototype have these implicit MethodExtension classes created for them? No; but if you created them and made a PR they would be accepted :)

This isn't the only constraints at play here, so let's keep going through the example by looking at the method:

def tileToLayout[K2: SpatialComponent: ClassTag](cellType: CellType, layoutDefinition: LayoutDefinition)(implicit ev: K => TilerKeyMethods[K, K2]): RDD[(K2, V)] = // ...

The type parameter K2 has two context bounds, which are SpatialComponent and ClassTag. ClassTag we know, but SpatialComponent is new. Having an implicit SpatialComponent[K2] means that we can get and set a SpatialKey in and out of any K. This is useful for abstracting over SpaceTimeKey and SpatialKey: they both have spatial components that we can work with, and the SpatialComponent type class is how we require such functionality from our types.

Last but not least is the implicit parameter explicitly stated in the implicit parameter list (and not by context bound): (implicit ev: K => TilerKeyMethods[K, K2]). This can't be written as a context bound, because it is a constraint on the original K of the implicit class, not K2. If it were on K, we could state it as a context bound; however we don't have access to the K2 type there so it can't be a context bound on the class. At the end of the day, though, it means something we've seen before: we need an implicit conversion from K to TilerKeyMethods[K, K2], which allow us to do conversions of K into K2, or in our case ProjectedExtent to SpatialKey. You can see the implicit that satisfies that requirement here: https://github.com/locationtech/geotrellis/blob/bc09ab8c49fc0c34a98d53c626a0fdd7e82f858f/spark/src/main/scala/geotrellis/spark/package.scala#L167-L170. That constraint is just based on Keys though, and won't help your problem.

So the conclusion is, the reason your non-compiling code doesn't compile is because TileFeature[Tile, CustomMetadata] doesn't have an implicit way to merge or create a prototype. But, if you just add those, it should "Just Work". E.g. if your CustomMetadata had a merge method, you could do something like this:

implicit class CustomTileMergeMethods[T: <: CellGrid: TileMergeMethods](val self: TileFeature[T, CustomMetadata]) extends TileMergeMethods[TileFeature[T, CustomMetadata]] {
  def merge(other: TileFeature[T, CustomMetadata]): TileFeature[T, CustomMetadata] = TileFeature(self.tile.merge(other.tile), self.data.merge(other.data))
  // ...
}

etc.

Let me know if what remains unclear, or if this explanation is helpful.

Cheers, Rob

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment