Skip to content

Instantly share code, notes, and snippets.

@arosien
Last active December 10, 2015 14:38
Show Gist options
  • Save arosien/4448358 to your computer and use it in GitHub Desktop.
Save arosien/4448358 to your computer and use it in GitHub Desktop.
Deriving and Demystifying Scala's Cake Pattern

The Basics

We're going to be linking the following traits and classes together:

trait FileSystem {
  def roots: Traversable[File]
}

class PathWalker(fs: FileSystem) {
  def find(globPattern: String): Traversable[File] = {
    // do something with fs.roots()
  }
}

Then we want to make a PathWalker, somehow, and do something with it:

// Client code: make a PathWalker and do something with it.
val pathWalker: PathWalker = ... // TODO
pathWalker.find("*.scala")

As you can see, a PathWalker needs a FileSystem. Let's abstract the choice of implementation of FileSystem into into a function, wrapping everything in a code block to represent some scope:

{
  trait FileSystem {
    def roots: Traversable[File]
  }

  def fileSystem: FileSystem // abstract
  
  class PathWalker {
    val fs = fileSystem  // Calls the factory method. No need for a constructor parameter anymore.
    
    def find(globPattern: String): Traversable[File] = {
      // do something with fs.roots()
    }
  }
  
  // Client code: make a PathWalker and do something with it.
  val pathWalker = new PathWalker
  pathWalker.find("*.scala")
}

You can't have an abstract method in a code block, so we'll make the block into a trait:

trait PathWalkerModule {
  trait FileSystem {
    def roots: Traversable[File]
  }

  def fileSystem: FileSystem // abstract
  
  class PathWalker {
    val fs = fileSystem
    
    def find(globPattern: String): Traversable[File] = {
      // do something with fs.roots()
    }
  }

  // Client code: make a PathWalker and do something with it.
  val pathWalker = new PathWalker
  pathWalker.find("*.scala")
}

Now fileSystem can remain abstract. But it's obviously silly to have the client code coupled with the class definitions, so we'll separate them:

trait PathWalkerModule {
  trait FileSystem {
    def roots: Traversable[File]
  }

  def fileSystem: FileSystem // abstract
  
  class PathWalker {
    val fs = fileSystem
    
    def find(globPattern: String): Traversable[File] = {
      // do something with fs.roots()
    }
  }
}

// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
  val module = new PathWalkerModule {
    def fileSystem: FileSystem // TODO!!!
  }
  
  val pathWalker = new module.PathWalker
  pathWalker.find("*.scala")
}

Now we can get a PathWalker that uses any kind of FileSystem, as long as we can define the fileSystem method in the PathWalkerModule.

But this is kind of weird: the choice of a FileSystem instance has nothing to do with creating a PathWalker, we just need an instance, and we're only linking the two instances together. To fix this we can refactor the code to separate the creation of a FileSystem from the creation of a PathWalker:

trait FileSystemModule {
  trait FileSystem {
    def roots: Traversable[File]
  }
  
  def fileSystem: FileSystem // abstract
}

trait PathWalkerModule {
  self: FileSystemModule => // Require FileSystemModule to be mixed-in.

  class PathWalker {
    val fs = fileSystem // i.e. FileSystemModule.fileSystem()
    
    def find(globPattern: String): Traversable[File] = {
      // do something with fs.roots()
    }
  }
}

// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
  val module = new PathWalkerModule with FileSystemModule {
    def fileSystem: FileSystem // TODO!!!
  }
  
  val pathWalker = new module.PathWalker
  pathWalker.find("*.scala")
}

Let's get rid of the TODO and create a "non-abstract" FileSystemModule trait:

/** Creates a "real" [FileSystem]. */
trait DefaultFileSystemModule extends FileSystemModule { 
  override def fileSystem: FileSystem = new DefaultFileSystem
  
  class DefaultFileSystem extends FileSystem {
    // ...
  }
}

So we finally have:

trait FileSystemModule {
  trait FileSystem {
    def roots: Traversable[File]
  }
  
  def fileSystem: FileSystem // abstract
}

trait PathWalkerModule {
  self: FileSystemModule => // Require FileSystemModule to be mixed-in.

  class PathWalker {
    val fs = fileSystem // i.e. FileSystemModule.fileSystem()
    
    def find(globPattern: String): Traversable[File] = {
      // do something with fs.roots()
    }
  }
}

/** Creates a "real" [FileSystem]. */
trait DefaultFileSystemModule extends FileSystemModule { 
  override def fileSystem: FileSystem = new DefaultFileSystem
  
  class DefaultFileSystem extends FileSystem {
    // ...
  }
}

// Some other code block, somewhere...
// Client code: make a PathWalker and do something with it.
{
  val module = new PathWalkerModule with DefaultFileSystemModule
  
  val pathWalker = new module.PathWalker
  pathWalker.find("*.scala")
}

So the general pattern looks like:

trait XModule { class X }
trait YModule { class Y }

trait ZModule {
  self: XModule with YModule =>
  
  class Z {
    val x = new X
    val y = new Y
    
    // do stuff with x and y
  }
}

Thus the name "cake": The ZModule "layer" is layered with the other "layers" containing ways to get other definitions and instances.

TODO

  • val vs. def in concrete factories/components for singletons.
  • Naming: XFactory vs. XComponent vs. XModule.
  • cake as first-class modules; deficiencies of "package" (no declared dependencies); maven as module w/o API declaration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment