5

I've noticed that the Scala standard library uses two different strategies for organizing classes, traits, and singleton objects.

  1. Using packages whose members are them imported. This is, for example, how you get access to scala.collection.mutable.ListBuffer. This technique is familiar coming from Java, Python, etc.

  2. Using type members of traits. This is, for example, how you get access to the Parser type. You first need to mix in scala.util.parsing.combinator.Parsers. This technique is not familiar coming from Java, Python, etc, and isn't much used in third-party libraries.

I guess one advantage of (2) is that it organizes both methods and types, but in light of Scala 2.8's package objects the same can be done using (1). Why have both these strategies? When should each be used?

2 Answers 2

5

The nomenclature of note here is path-dependent types. That's the option number 2 you talk of, and I'll speak only of it. Unless you happen to have a problem solved by it, you should always take option number 1.

What you miss is that the Parser class makes reference to things defined in the Parsers class. In fact, the Parser class itself depends on what input has been defined on Parsers:

abstract class Parser[+T] extends (Input => ParseResult[T]) 

The type Input is defined like this:

type Input = Reader[Elem] 

And Elem is abstract. Consider, for instance, RegexParsers and TokenParsers. The former defines Elem as Char, while the latter defines it as Token. That means the Parser for the each is different. More importantly, because Parser is a subclass of Parsers, the Scala compiler will make sure at compile time you aren't passing the RegexParsers's Parser to TokenParsers or vice versa. As a matter of fact, you won't even be able to pass the Parser of one instance of RegexParsers to another instance of it.

Sign up to request clarification or add additional context in comments.

Comments

4

The second is also known as the Cake pattern. It has the benefit that the code inside the class that has a trait mixed in becomes independent of the particular implementation of the methods and types in that trait. It allows to use the members of the trait without knowing what's their concrete implementation.

trait Logging { def log(msg: String) } trait App extends Logging { log("My app started.") } 

Above, the Logging trait is the requirement for the App (requirements can also be expressed with self-types). Then, at some point in your application you can decide what the implementation will be and mix the implementation trait into the concrete class.

trait ConsoleLogging extends Logging { def log(msg: String) = println(msg) } object MyApp extends App with ConsoleLogging 

This has an advantage over imports, in the sense that the requirements of your piece of code aren't bound to the implementation defined by the import statement. Furthermore, it allows you to build and distribute an API which can be used in a different build somewhere else provided that its requirements are met by mixing in a concrete implementation.

However, there are a few things to be careful with when using this pattern.

  1. All of the classes defined inside the trait will have a reference to the outer class. This can be an issue where performance is concerned, or when you're using serialization (when the outer class is not serializable, or worse, if it is, but you don't want it to be serialized).
  2. If your 'module' gets really large, you will either have a very big trait and a very big source file, or will have to distribute the module trait code across several files. This can lead to some boilerplate.
  3. It can force you to have to write your entire application using this paradigm. Before you know it, every class will have to have its requirements mixed in.
  4. The concrete implementation must be known at compile time, unless you use some sort of hand-written delegation. You cannot mix in an implementation trait dynamically based on a value available at runtime.

I guess the library designers didn't regard any of the above as an issue where Parsers are concerned.

2 Comments

This is a very good answer, but to another question. The design goals here are completely unrelated to the cake pattern.
You're right - the notes about Parsers in your answer are more detailed. However, he did ask when each of these import strategies should be used.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.