ADTs, OOP, rules and Pattern-matching (2)

10. June 2008 09:44 by CarlosLoria in General  //  Tags:   //   Comments (0)

 We are reviewing some programming languages which provide explicit support for the integration between OO, ADTs (i.e. rules and pattern-matching). We refer to Part 1  for an introduction. We currently focus on Scala and TOM in our work. Scala provides the notion of extractor as a way to pattern-matching extensibility. Extractors allow defining views of classes such that we can see them in the form of patterns. Extractors behave like adapters that construct and destruct objects as required during pattern-matching according to a protocol assumed in Scala. By this way, it results possible to separate a pattern from the kind of objects the pattern denotes. We might consider pattern-matching as an aspect of an object (in the sense of Aspect Oriented Programming, AOP). The idea behind is really interesting, it is clarified in more detail in Matching Objects in Scala .  We want to perform some analysis in this post. Our main concern in this part, as expected, is pattern extensibility, we leave the ADT aside just for a while. We will consider a situation where more support for extensibility is required and propose using delegation to cope with it, without leaving the elegant idea and scope of extractors offered by Scala. Pattern-matching by delegation could be more directly supported by the language.

Let us now see the example. Because of the complete integration between Scala and Java, extractors in Scala nicely interact with proper Java classes, too. However, let us use just proper Scala class declarations in our case study, as an illustration. In this hypothetical case, we deal with a model of “beings” (“persons”, “robots”, societies” and the like. We want to write a very simple classifier of beings using pattern-matching. We assume that classes of model are sealed for the classifier (we are not allowed to modify them). Hence, patterns need to be attached externally. We start with some classes and assume more classes can be given, so we need to extend our initial classifier to cope with new kinds of beings.

Thus, initially the model is given by the following declarations (we only have “person” and “society” as kinds of beings):

trait Being

class Person extends Being {
  var name:String = _
  var age:Int = _
 
  def getName() = name
  def getAge()  = age
 
  def this(name:String, age:Int){this();this.name=name;this.age=age}
    
  override def toString() = "Person(" + name+ ", " +age+")"
}


class Society extends Being{
  var members:Array[Being] = _
  def getMembers = members
  def this(members:Array[Being]){this();this.members=members}
  override def toString() = "Society"
}

We have written classes in a Java-style, to remark that could be external to Scala. Now, let us introduce a first classifier. We encapsulate all parts in a class for our particular purposes, as we explain later on.

trait BeingClassifier{

    def classify(p:Being) =  println("Being   classified(-):" + p)
}

class BasicClassifier extends BeingClassifier{

   object Person {
     def apply(name:String, age:int)= new Person(name, age)
     def unapply(p:Person)            = Some(p.getName)
   }
   
   object Society{
     def apply(members:Array[Being]) = new Society(members)
     def unapply(s:Society) = Some(s.getMembers)
   }

   override def classify(b:Being) = b match {
      case Person(n)     => println("Person  classified(+):" + n)
      case Society(m)   => println("Society classified(+):" + b)
      case _                 => super.classify(b)
   }

}
object SomeBeingClassifier extends BasicClassifier{
}

Method “classify(b:Being)”, based on pattern-matching, tries to say whether “b” could or not be recognized and displays a message, correspondingly. Notice that objects “Person” and “Society” within class “BasicClassifier” are extractors. In each case, we have specified both “apply” and “unapply” methods of the protocol, however, just “unapply” is actually relevant here. Such methods uses destructors to extract those parts of interest for the pattern being defined. For instance, in this case, for a “Person” instance we just take its field “name” in the destruction (“age” is ignored). Thus, the pattern with shape “Person(n:String)” differs from any constructor declared in class Person. That is, pattern and constructor can be, in that sense, independent expressions in Scala, which is a nice feature.
Now, let us suppose we know about further classes “Fetus” and “Robot” deriving from “Being”, defined as follows:


class Fetus(name:String) extends Person(name,0)
{
  override def toString() = "Fetus"
}

class Robot extends Being{
  var model:String = _
  def getModel = model
  def this(model:String){this();this.model=model}
  override def toString() = "Robot("+model+")"
}

Suppose we use the following object for testing our model:

object testing extends Application {
  val CL  = SomeBeingClassifier  
  val p    = new Person("john",20)
  val r      = new Robot("R2D2")
  val f      = new Fetus("baby")
  val s    = new Society(Array(p, r, f))
 CL.classify(p)
 CL.classify(f)
 CL.classify(r)
 CL.classify(s)
}

We would get:

Person  classified(+):john
Person  classified(+):baby
Being   classified(-):Robot(R2D2)
Society classified(+):Society

In this case, we observe that “Robot” instances are negatively classified while “Fetus” instances are recognized as “Person” objects. Now, we want to extend our classification rule to handle these new classes. Notice, however, we do not have direct support in Scala for achieving that, unfortunately. A reason is that “object”-elements are final (instances) and consequently they can not be extended. In our implementation, we would need to create a new classifier extending the “BasicClassifier” above, straightforwardly. However, we notice that the procedure we follow is systematic and amenable to get automated.

class ExtendedClassifier extends BasicClassifier{
   object Fetus {
        def apply(name:String) =  new Fetus(name)
        def unapply(p:Fetus)   = Some(p.getName())
   }
   object Robot {
              def apply(model:String) = new Robot(model)
              def unapply(r:Robot)    = Some(r.getModel)
   }
   
   override def classify(p:Being) = p match {
         case Robot(n)     => println("Robot   classified(+):" + n)
         case Fetus(n)     => println("Fetus   classified(+):" + n)
         case _               => super.classify(p)
   }
   
}
object SomeExtendedClassifier extends ExtendedClassifier{
}

As expected, “ExtendedClassifier” delegates parent “BasicClassifier” performing further classifications that are not contemplated by its own classify method. If we now change our testing object:

object testing extends Application {
  val XCL  = SomeExtendedClassifier  
  val p = new Person("john",20)
  val r = new Robot("R2D2")
  val f = new Fetus("baby")
  val s = new Society(Array(p, r, f))
 XCL.classify(p)
 XCL.classify(f)
 XCL.classify(r)
 XCL.classify(s)
}

We would get the correct classification:

Person  classified(+):john
Fetus   classified(+):baby
Robot   classified(+):R2D2
Society classified(+):Society

As already mentioned, this procedure could be more directly supported by the language, such that a higher level of extensibility could be offered.