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.
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.
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:
|
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. |
3.147.54.6