Appendix D. A cheat sheet for Scala’s DSL-friendly features

This appendix will assist you as you become familiar with the DSL-friendly features of Scala. Please don’t treat this information as a comprehensive language overview. For a more complete and detailed discussion of the language and its syntax, see the references in section D.2.

D.1. DSL-friendly features of Scala

Scala is an object-functional language that runs on the JVM. It has great interoperability with Java by virtue of having the same object model (and more). Scala has a nice, concise syntax, offers type inference, and a whole bunch of mechanisms for designing abstractions based on a combination of OO and functional paradigms.

Table D.1. Scala feature overview
Class-based OOP
Scala is OO. You can define classes that have instance variables and methods. But besides being a class, a Scala abstraction can be of many other types, each with its own set of features and applicability. We’re going to look at most of them in this appendix. When you design a DSL using Scala, it’s common to model the entities of your domain as classes or as any of the other ways to group related functionalities. For details about class definition syntax, refer to [1] in section D.2. class Account(val no: Int, val name:
String) {
def balance: Int = {
//.. implementation
}
//..
} A class definition can take parameters. In this snippet, val implies that no and name are immutable and cannot be reassigned. balance is a method that you can define. It doesn’t take any argument and returns an Int.
Case classes
You can add the word case before a class definition and get a lot of mileage out of the abstraction that the compiler generates. It’s called a case class in Scala. For a case class, the compiler automatically does the following:
  • Converts the constructor arguments into immutable vals. You can explicitly specify var to decide otherwise.
  • Implements equals, hashCode, and toString methods to the class.
  • Lets you invoke a shorthand notation for the constructor. You don’t need to specify the keyword new while instantiating an object of the class. The compiler gives you a companion object that contains the apply() constructor and an extractor on the constructor arguments.
Case classes are also useful for pattern matching. They’re the most idiomatic way to implement algebraic data types in Scala. Because of the built-in features of immutability that case classes offer, they’re often used to build immutable value objects when designing DSLs
abstract class Term
case class Var(name: String)
extends Term
case class Fun(arg: String, body:
Term) extends Term For this case class definition, you can instantiate as val p = Var("p"), without having to explicitly specify new.
Traits
Traits are yet another way to specify abstractions in Scala. They’re similar to Java interfaces in that you can leave out the implementation for concrete classes. But unlike interfaces, you can specify partial implementations of some methods in a trait. Traits offer a way to implement mixins in Scala and can also be used to design the correct way to use multiple inheritance. trait Audit {
def record_trail {
//.. implementation
}
def view_trail // open
}
class SavingsAccount extends Account
with Audit {
//..
} Traits are a great way to design open reusable abstractions without committing to a specific implementation. Note how in this definition, the method view_trail is kept open for implementation by the abstraction that mixes in the trait.
Higher-order functions & closures
In Scala, functions are first-class values, and you can pass a function as an argument to yet another function. You can also have a function return another function. Functions as first-class values give Scala the main power for functional programming. Higher-order functions make code less verbose and express the correct verb semantics of your DSL. Closures let you write fewer classes and objects and more functional artifacts. val hasLower =
bookTitle.exists(_.isLowerCase)
def foo(bar: (Int, Int)=>Int) {
//..
} In the first example, the method exists takes a function as an argument that it applies to each character of the string. In Java, this would’ve been way more verbose. The second example shows the literal syntax of a function in Scala.
Pattern matching
Like all functional programming languages, Scala has pattern matching. You can match arbitrary expressions with a first-match-wins policy in Scala. Case classes and pattern matching are a potent combination for implementing functional idioms in Scala. You can implement an extensible Visitor pattern using case classes. As you see in chapter 6, pattern matching plays an important role in making expressive business rules def foo(i: Int) = i match {
case 10 => //..
case 12 => //..
case _ =>
} The previous rendering is a more concise Scala version of Java’s switch/case statement. But pattern matching has many other uses. val obj = doStuff()
var cast:Foo = obj match {
case x:Foo => x
case _ => null
} The earlier example is a more idiomatic way of implementing an instanceOf check used in Java. trait Account
case class Checking(no: Int) extends
Account
case class Savings(no: Int, rate:
Double) extends Account
def process(acc: Account) = acc match
{
case Checking(no) => // do stuff
case Savings(no, rt) => // do stuff
} Case classes can be used directly for pattern matching. Note that earlier I said that case classes implement extractors by default.
Objects as modules
Objects define executable modules in Scala. After you define abstractions using classes and traits, you can compose them together into a concrete abstraction using the object syntax. Object syntax is the closest approximation of statics that are used in Java. object RuleComponent extends Rule
with CountryLocale with Calendar { //
..
} RuleComponent is a singleton object created out of the specified abstractions.
Implicit arguments
You can leave out the last argument of a function by declaring it to be implicit. The compiler will look for a matching argument from the enclosing scope of the function. def shout(at: String)(implicit curse:
String) {
println("hey: " + at + " " + curse)
}
implicit val curse = "Damn! "
shout("Rob") The compiler will complain if it can’t find a matching argument from the enclosing scope.
Implicit type conversions (aka Pimp My Library)
You can extend existing libraries without making any changes to them by using implicit type conversions. It works much like Ruby monkey patching, but it’s controlled within the lexical scope. Martin Odersky named this the Pimp My Library pattern (see [2] in section D.2). The implicit conversion function is automatically applied by the compiler. This helps you make your legacy abstractions smart with improved APIs. class RichArray[T](value: Array[T]) {
def append(other: Array[T])
: Array[T] = {
//.. implementation
}
}
implicit def enrichArray[T](xs:
Array[T]) = new RichArray[T] The implicit definition of enrichArray serves as the conversion function from Array to RichArray.
Partial functions
A partial function is one that’s defined only for a set of values of its arguments. Partial functions in Scala are modeled as blocks of pattern-matching case statements. PartialFunctions are used idiomatically to define the message receive loop in Scala actors. val onlyTrue:
PartialFunction[Boolean, Int] = {
case true => 100
} onlyTrue is a PartialFunction that’s defined for a limited domain. It’s defined only for the Boolean value true. The PartialFunction trait contains a method isDefinedAt that returns true for the domain values for which the PartialFunction is defined. Here’s an example: scala> onlyTrue isDefinedAt(true)
res1: Boolean = true
scala> onlyTrue isDefinedAt(false)
res2: Boolean = false
Generics and type parameters
Scala offers type parameters that you specify as part of your class and method declarations. You can also specify explicit constraints on these types that your abstraction will honor. You get an automatic level of constraint checking by the compiler without having to write a single line of validation logic. With Scala, you can abstract many of your DSL constraints within the type system. class Trade[Account <:
TradingAccount](account: Account) {
//..
} In this class definition, you won’t be able to create an instance of Trade with an account that doesn’t satisfy the specified constraint.

D.2. References

  1. Wampler, Dean, and Alex Payne. 2009. Programming Scala: Scalability = Functional Programming + Objects. O’Reilly Media.
  2. Odersky, Martin. Pimp My Library. Artima Developer. http://www.artima.com/weblogs/viewpost.jsp?thread=179766.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.54.6