Saturday, September 28, 2013

Data Exposure and Encapsulation

TL;DR - Scala lets you expose internal data, then later change underpinnings while maintaining the same interface.

The functional Way is an open Way. Internal data is available to anyone who wishes to extract it.
"But what about encapsulation??" - horrified OO programmer
"We do not fear your code, for you cannot screw up our immutable data." - calm FP programmer
"But what about change? If you expose internal structure, then you can never change internal structure. You'll break all my code!" - OO
"The surface is resilient. The essence is fluid. We can keep client code happy while expanding underneath." - FP
One of the ways that Scala achieves "scalability" is: make the code easy to write now, and still flexible to change later. An example is simple data structures: the case class. By default, anyone can pattern match to extract all the values from a case class. For instance:

case class PersonName(first: String, last: String)
val me = PersonName("Jessica", "Kerr")

val greeting = me match {
  case PersonName(first, last) => s"Hello, $first $last"
}

This is handy - it makes building and using simple datatypes painless. Fields are public, which is not frightening because they're final, safe from alteration.

The danger of this easy syntax is rigidity: if I add a middle-name field to PersonName, then all the code that pattern matched on it is broken. Construction, too.

case class PersonName(first: String, 
                      last: String, 
                      middle: Option[String])
val me = PersonName("Jessica", "Kerr") // broken!

me match {
  case PersonName(first, last) => s"Hello, $first $last" // broken!
}

We want to change the PersonName without breaking backwards compatibility. In Scala, all the handiness of the case class (construction, extractors) is syntax sugar. That means we can pull it apart and do whatever we want with it behind the scenes.

In Scala, the constructor that works without "new" in front of it is really function application on the PersonName companion object. [1] And function application on anything is really a call to its apply method. Case class syntax sugar provides an apply method that accepts all the fields, like:

object PersonName {
     def apply(first: String, 
               last:String, 
               middle: Option[String]) : PersonName =
            new PersonName(first, last, middle)
   ...
}

PersonName("first", "last", Some("middle")) expands to PersonName.apply("first", "last", Some("middle")) .
Since that's all the case class constructor really is, we can re-enable the old syntax by adding a bonus apply method to the PersonName object:

object PersonName {
    def apply(first: String, last: String): PersonName = 
       new PersonName(first,last, None)
}
val me = PersonName("Jessica", "Kerr") // fixed!

There's another secret method name behind the pattern match syntax. When Scala sees:

(p : PersonName) match {
   case PersonName(a : String,b : String) =>
}

Scala looks for a method called unapply on the PersonName object; unapply should accept a PersonName and return an optional tuple of two strings.

object PersonName {

  def unapply(pn: PersonName): Option[(String, String)] = 
       Some((pn.first, pn.last))
}

It's like the case statement is switching around the return value and the arguments of the unapply method. If calling unapply with the matched-against value (p) returns Some(tuple), then the match succeeds and the identifiers (a and b) get the values in the tuple.

Retaining support for the pattern match is harder than the constructor. We can't add another unapply extractor to the PersonName object; it conflicts with the generated one that comes with the case class.

Here's a solution that abstracts PersonName into a trait, then backs it with a different case class of our choice. Now the constructors and extractors are ours to control and manipulate.

trait PersonName {
    val first:String
    val last:String
  }

case class FullPersonName(first: String, 
              last: String,
              middle: Option[String]) extends PersonName

object PersonName {
  def apply(first: String, last: String): PersonName = 
          FullPersonName(first,last, None)
  def unapply(pn: PersonName): Option[(String, String)] = 
          Some((pn.first, pn.last))
}

This compatibility could also be met by changing PersonName from a case class into a regular class, and supplying apply and unapply methods as we like. What matters is not how we do it, but that we can.

In this way Scala makes the obvious thing easy, and custom behavior possible. Backwards compatibility enables reuse. In OO we encapsulate everything up front to make later change possible, with indirection and lots of boilerplate. In Scala we don't have to complicate our code to accommodate possible imagined future change. Stick with YAGNI now, without paying for it later. Reduced suffering for everyone! The world is a better place with FP.


--------------
[1] We could fix the broken constructor by supplying a default value for middle name.

case class PersonName(first: String, 
                      last: String,
                      middle: Option[String] = None)

That's simpler but less flexible.

2 comments:

  1. This horrified OO programmer is scratching is head hard. He don't get it, really. What's wrong with the solution he has in mind to implement the very same change in his good old OO langage ?

    His highly verbose PersonName class looks like :

    public class PersonName{
    public final String first;s
    public final String last;

    public final PersonName(String first, String last){
    this.first = first;
    this.last=last;
    }
    }
    Naïvely, adding a middle name without "breaking all my code" is for him just a matter of a few additional keystrokes :

    public class PersonName{
    static final String DEFAULT_MIDDLE =""; //let's not introduce Optionalor MayBe, that's not the point here ;)
    public final String first;
    public final String last;
    public final String middle;

    public final PersonName(String first, String last){
    this(first, DEFAULT_MIDDLE,last);
    }

    public final PersonName(String first, String middle, String last){
    this.first = first;
    this.middle=middle;
    this.last=last;
    }
    }

    What's wrong with this ? He don't know, and honestly hope calm FP programmer can give him some explanations.
    While waiting... With a constructor that now have three String's arguments, he'll probably give each parameter his own class (FirstName, MiddleName, LastName) to avoid confusing his fellow OO programmers. Sooner or later, they would probably mix first, last and middle name values when instanciating a PersonName.

    ReplyDelete
    Replies
    1. That is a great class! Nice and immutable. You have not encapsulated your data; it's visible to the world.

      You're right that my post isn't a strength of Scala over Java - it's a strength of functional style over traditional Java style, with getters and setters everywhere.

      What Java doesn't give you is deconstruction with pattern matching.

      In Haskell (if I'm not mistaken), adding a field to the constructor of PersonName would necessitate a change to every place that deconstructed it. Scala's OO-underneath pattern matching lets you have both.

      Also, I totally agree on FirstName MiddleName and LastName as separate classes! It pained me to use String for all of them, but it kept the examples shorter.

      Calm FP programmer is pleased with your code.

      Delete