środa, 29 czerwca 2011

Recently, Mark Needham asked an interresting question: in what cases Scala structural types can be legitimately combined with self-type annotations.
Let me digress here to present the context first.
This all has begun with Mark's post that presents a code snippet combining the two features. Unlike some people I wasn't really excited about the example, quite opposite to be honest.
I expressed my criticism at the post comment, but I'll repeat the points here:

1. Structural types are optimal to uniformly handle types, that share some members, yet have no common ancestor. Like Socket and ServerSocket in java.net - both have close(), isConnected(), isBound(), getInetAddress() methods, yet you cannot treat them uniformly in Java. Moreover you cannot change those classes, because they are from the standard library. In the Mark's example all classes were under his control, so he could make them implement a common trait easily.

2. Self-type annotations shine when you have a coherent set of abstract members you want to have available for your trait's methods. Just wrap the related members into a separate trait and declare it as your self type. The first benefit you get is that the syntax clearly communicates the connection between the abstract members (they are grouped in one type). The other, your traits are now connected at one level higher, without the need for enumerating all necessary abstract members in your main trait.

There is little sense in using structural types for classes that you control. Often there is also little sense in extracting the only abstract member into its own type (named or structural, regardless) just to declare it as a self-type. I don't mean always, because in some cases you want it to communicate, that the abstract member is a (possibly only, but still) part of some important, independent concept.
In Mark's example, I just couldn't tell what concept is the "val peopleNodes: NodeSeq" member a part of. For that reason I found introduction of self-type there somewhat artificial.

Then there comes a question: is there a valid use case for combining structural types with self-type annotations. That was hard question for me, because I've never imagined such combination, nor met one. Nevertheless I've tried to imagine a case, where it could make sense.

So first, we should have at least two classes without a common ancestor, but structurally similar in order to meaningfully apply the Scala's structural types. Socket and ServerSocket from java.net came to my mind.
Second, we need to have trait with a bunch of abstract members and we should be able to divide the members into separate coherent groups in order for self-type annotation to make sense. I've invented a totally artificial "LoggableSocket" trait, that represents a socket-like object with an ability to log its status in some form. Without structural types and self-type annotations it (and its use) would look like:

import java.net._

trait LoggableSocket {
  def isConnected(): Boolean
  def isBound(): Boolean
  def log(message: String): Unit
  def logStatus() = {
    log("Socket status: connected [" + isConnected() +
      "], bound [" + isBound() + "]")
  }
}

// helper trait
trait SysoutLogging {
  def log(s: String) = println(s)
}
// usage
val s = new Socket() with LoggableSocket with SysoutLogging
s.logStatus()


Of course at least one thing is not that nice here: the abstract members related to Sockets are intermixed with the member related to logging. Let's fix it by extracting them to their own type and then declare it as the LoggableSocket's self-type:

trait SocketLike {
  def isConnected(): Boolean
  def isBound(): Boolean
}

trait LoggableSocket {
  self: SocketLike =>
  def log(message: String): Unit
  def logStatus() = {
    log("Socket status: connected [" + isConnected() +
      "], bound [" + isBound() + "]")
  }
}
// ...
val s = new Socket() with LoggableSocket with SysoutLogging // Ups! Does not compile

What has just happened? It doesn't compile, because Socket doesn't implement the SocketLike trait, which we've just declared as self-type dependency of LoggableSocket. Socket is a JDK class, so we cannot make it implement that interface.
We could either instantiate it adding another "with" after the constructor:

val s = new Socket() with SocketLike with LoggableSocket with SysoutLogging

(but the number of "with"s quickly gets out of control) or, we could - yes - introduce a structural type here:

trait LoggableSocket {
  self: {
    def isConnected(): Boolean
    def isBound(): Boolean
  } =>
  def log(message: String): Unit
  def logStatus() = {
    log("Socket status: connected [" + isConnected() +
      "], bound [" + isBound() + "]")
  }
}
// ...
val s = new Socket() with LoggableSocket with SysoutLogging

...and everything works like a charm again. This time the related members are grouped into a meaningful way. That clearly communicates, that they are of a different nature from the third abstract member, the log method. In addition this works for impossible-to-modify JDK classes.

So as you can see - yes, combining the two features: self-type annotations and structured types occasionally makes sense.
Though I would be far from calling that example natural or a real-world one :)
I really doubt you would find many cases, when the combination makes sense. But maybe I just cannot come up with an example? Would be very interrested if you found a valid real-world one.

1 komentarz: