Presentation at NY Scala Enthusiasts Meetup on 6/14/2010. Covers techniques for using Scala's flexible syntax and features to design internal DSLs and wrappers.
3. A Definition
A DSL is a custom way to represent logic designed to
solve a specific problem.
2
4. A Definition
A DSL is a custom way to represent logic designed to
solve a specific problem.
I’m going to try and give you tools to stretch Scala’s
syntax to match the way you think about the logic of your
specific problem.
2
5. Key Scala features
• Syntactic sugar
• Implicit methods
• Options
• Higher order functions
3
6. Syntactic Sugar
You can omit . and () for any method which takes a single
parameter.
map get “key” == map.get(“key”)
4
7. Syntactic Sugar
Methods whose names end in : bind to the right.
“key” other: obj == obj.other:(“value”)
val newList = item :: oldList
val newList = oldList.::(item)
5
9. Syntactic Sugar
the update() method
a(b) = c == a.update(b, c)
a(b, c) = d == a.update(b, c, d)
map(“key”) = “value” ==
map.update(“key”, “value”)
7
10. Syntactic Sugar
setters and getters
object X { var y = 0 }
object X {
private var _z: Int = 0
def y = _z
def y_=(i: Int) = _z = i
}
X.y => 0
X.y = 1 => Unit
X.y => 1
8
11. Syntactic Sugar
tuples
(a, b) == Tuple2[A,B](a, b)
(a, b, c) == Tuple3[A,B,C](a, b, c)
val (a, b) = sometuple // extracts
9
12. Syntactic Sugar
unapply() - used to extract values in pattern matching
object Square {
def unapply(p: Pair[Int, Int]) = p match {
case (x, y) if x == y => Some(x)
case _ => None
}
}
(2, 2) match {
case Square(side) => side*side
case _ => -1
} 10
16. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
13
17. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
13
18. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
13
19. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
s.side => 2 // member is exported
13
20. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
s.side => 2 // member is exported
val Square(x) = s // unapply() is defined
13
21. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
s.side => 2 // member is exported
val Square(x) = s // unapply() is defined
s.toString ==> “Square(2)”
13
22. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
s.side => 2 // member is exported
val Square(x) = s // unapply() is defined
s.toString ==> “Square(2)”
s.hashCode ==> 630363263
13
23. Syntactic Sugar
Case Classes are regular classes which export their
constructor parameters and which provide a recursive
decomposition mechanism via pattern matching.
case class Square(side: Int)
val s = Square(2) // apply() is defined
s.side => 2 // member is exported
val Square(x) = s // unapply() is defined
s.toString ==> “Square(2)”
s.hashCode ==> 630363263
s == Square(2) ==> true
13
25. Syntactic Sugar
Many classes can be used in for comprehensions because
they implement some combination of:
map[B](f: (A) => B): Option[B]
flatMap[B](f: (A) => Option[B]): Option[B]
filter(p: (A) => Boolean): Option[A]
foreach(f: (A) => Unit): Unit
14
26. Syntactic Sugar
Many classes can be used in for comprehensions because
they implement some combination of:
map[B](f: (A) => B): Option[B]
flatMap[B](f: (A) => Option[B]): Option[B]
filter(p: (A) => Boolean): Option[A]
foreach(f: (A) => Unit): Unit
for { i <- List(1,2,3)
val x = i * 3
if x % 2 == 0 } yield x ==> List(6)
List(1,2,3).map(_ * 3).filter(_ % 2 == 0)
==> List(6) 14
27. Implicit Methods
Implicit methods are a compromise between the
closed approach to extension of Java and the open
approach to extension in Ruby.
Implicit Methods are:
15
28. Implicit Methods
Implicit methods are a compromise between the
closed approach to extension of Java and the open
approach to extension in Ruby.
Implicit Methods are:
• lexically scoped / non-global
15
29. Implicit Methods
Implicit methods are a compromise between the
closed approach to extension of Java and the open
approach to extension in Ruby.
Implicit Methods are:
• lexically scoped / non-global
• statically typed
15
32. Implicit Methods
Implicit methods are inserted by the compiler under
the following rules:
• they are in scope
• the selection is unambiguous
16
33. Implicit Methods
Implicit methods are inserted by the compiler under
the following rules:
• they are in scope
• the selection is unambiguous
• it is not already in an implicit i.e. no nesting
16
34. Implicit Methods
Implicit methods are inserted by the compiler under
the following rules:
• they are in scope
• the selection is unambiguous
• it is not already in an implicit i.e. no nesting
• the code does not compile as written
16
35. Implicit Methods: Usage
Extending Abstractions: Java
class IntWrapper(int i) {
int timesTen() {
return i * 10;
}
}
int i = 2;
new IntWrapper(i).timesTen();
17
36. Implicit Methods: Usage
Extending Abstractions: Java
Java’s abstractions are totally sealed and we must use
explicit wrapping each time we wish to extend them.
Pros: safe
Cons: repetitive boilerplate at each call site
18
38. Implicit Methods: Usage
Extending Abstractions: Ruby
Ruby’s abstractions are totally open. We can modify
the behavior of existing classes and objects in pretty
much any way we want.
Pros: declarative, powerful and flexible
Cons: easily abused and can be extremely difficult to
debug
20
39. Implicit Methods: Usage
Extending Abstractions: Scala
class IntWrapper(i: Int) {
def timesTen = i * 10
}
implicit def wrapint(i: Int) =
new IntWrapper(i)
val i = 2
i.timesTen
21
40. Implicit Methods: Usage
Extending Abstractions: Scala
Scala’s approach is both powerful and safe. While it is
certainly possible to abuse, it naturally encourages a
safer approach through lexical scoping.
Pros: more powerful than java, safer than ruby
Cons: can result in unexpected behavior if not tightly
scoped
22
46. Implicit Methods: Usage
Normalizing parameters
remainder(120) ==> error: inferred
type arguments [Int] do not conform to
method remainder's type parameter
bounds [T <: Double]
24
47. Implicit Methods: Usage
Normalizing parameters
remainder(120) ==> error: inferred
type arguments [Int] do not conform to
method remainder's type parameter
bounds [T <: Double]
implicit def i2d(i: Int): Double =
i.toDouble
24
48. Implicit Methods: Usage
Normalizing parameters
remainder(120) ==> error: inferred
type arguments [Int] do not conform to
method remainder's type parameter
bounds [T <: Double]
implicit def i2d(i: Int): Double =
i.toDouble
remainder(120) ==> 0
24
50. Options
Options are scala’s answer to null.
Options are a very simple, 3-part class hierarchy:
• sealed abstract class Option[+A] extends
Product
25
51. Options
Options are scala’s answer to null.
Options are a very simple, 3-part class hierarchy:
• sealed abstract class Option[+A] extends
Product
• case final class Some[+A](val x : A)
extends Option[A]
25
52. Options
Options are scala’s answer to null.
Options are a very simple, 3-part class hierarchy:
• sealed abstract class Option[+A] extends
Product
• case final class Some[+A](val x : A)
extends Option[A]
• case object None extends Option[Nothing]
25
56. Options: Usage
Replacing Null Checks
A (contrived) Java example:
int result = -1;
int x = calcX();
if (x != null) {
int y = calcY();
if (y != null) {
result = x * y;
}
}
29
57. Options: Usage
Replacing Null Checks
def calcX: Option[Int]
def calcY: Option[Int]
for {
val x <- Some(3)
val y <- Some(2)
} yield x * y
==> Some(6)
30
66. Implicits + Options
val m = Map(“1” -> 1,“map” -> Map(“2” -> 2))
We’d like to be able to access the map like this:
m/”key1”/”key2”/”key3”
35
67. Implicits + Options
val m = Map(“1” -> 1,“map” -> Map(“2” -> 2))
We’d like to be able to access the map like this:
m/”key1”/”key2”/”key3”
But, we’d like to not have to constantly check nulls or
Options.
35
77. Higher Order Functions
Let’s say we’d like to write a little system for easily running
statements asynchronously via either threads or actors.
39
78. Higher Order Functions
Let’s say we’d like to write a little system for easily running
statements asynchronously via either threads or actors.
What we’d like to get to:
run (println(“hello”)) using threads
run (println(“hello”)) using actors
39
79. Higher Order Functions
trait RunCtx {
def run(f: => Unit): Unit
}
class Runner(f: => Unit) {
def using(ctx: RunCtx) = ctx run f
}
def run(f: => Unit) = new Runner(f)
40