Skip to content

Commit 3d7f461

Browse files
author
Abhijit Sarkar
committed
Complete ch05
1 parent 72a9350 commit 3d7f461

14 files changed

+433
-0
lines changed

.scalafmt.conf

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ align.preset = more
33
maxColumn = 120
44
runner.dialect = scala3
55
assumeStandardLibraryStripMargin = true
6+
# https://github.com/scalameta/scalameta/issues/4090
67
project.excludePaths = [
78
"glob:**/ch04/src/**.scala"
89
]

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The older code is available in branches.
1111

1212
2. [Algebraic Data Types](ch02)
1313
3. [Objects as Codata](ch03)
14+
4. [Contextual Abstraction](ch04)
15+
5. [Reified Interpreters](ch05)
1416

1517
## Running tests
1618
```

ch05/src/Expression.scala

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package ch05
2+
3+
/*
4+
The core of the interpreter strategy is a separation between description and action.
5+
The description is the program, and the interpreter is the action that carries out the program.
6+
7+
This separation is allows for composition of programs, and managing effects by delaying
8+
them till the time the program is run. We sometimes call this structure an algebra, with
9+
constructs and combinators defining programs and destructors defining interpreters.
10+
The interpreter is then a structural recursion over this ADT.
11+
12+
We saw that the straightforward implementation is not stack-safe, and which caused us to
13+
to introduction the idea of tail recursion and continuations. We reified continuations
14+
functions, and saw that we can convert any program into continuation-passing style which
15+
has every method call in tail position. Due to Scala runtime limitations not all calls
16+
in tail position can be converted to tail calls, so we reified calls and returns into
17+
data structures used by a recursive loop called a trampoline.
18+
*/
19+
enum Expression:
20+
case Literal(value: Double)
21+
case Addition(left: Expression, right: Expression)
22+
case Subtraction(left: Expression, right: Expression)
23+
case Multiplication(left: Expression, right: Expression)
24+
case Division(left: Expression, right: Expression)
25+
26+
def +(that: Expression): Expression =
27+
Addition(this, that)
28+
29+
def -(that: Expression): Expression =
30+
Subtraction(this, that)
31+
32+
def *(that: Expression): Expression =
33+
Multiplication(this, that)
34+
35+
def /(that: Expression): Expression =
36+
Division(this, that)
37+
38+
def eval: Double =
39+
this match
40+
case Literal(value) => value
41+
case Addition(left, right) => left.eval + right.eval
42+
case Subtraction(left, right) => left.eval - right.eval
43+
case Multiplication(left, right) => left.eval * right.eval
44+
case Division(left, right) => left.eval / right.eval
45+
46+
object Expression:
47+
def apply(value: Double): Expression =
48+
Literal(value)

ch05/src/ExpressionC.scala

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ch05
2+
3+
// Continuation-Passing style.
4+
enum ExpressionC:
5+
case Literal(value: Double)
6+
case Addition(left: ExpressionC, right: ExpressionC)
7+
case Subtraction(left: ExpressionC, right: ExpressionC)
8+
case Multiplication(left: ExpressionC, right: ExpressionC)
9+
case Division(left: ExpressionC, right: ExpressionC)
10+
11+
def +(that: ExpressionC): ExpressionC =
12+
Addition(this, that)
13+
14+
def -(that: ExpressionC): ExpressionC =
15+
Subtraction(this, that)
16+
17+
def *(that: ExpressionC): ExpressionC =
18+
Multiplication(this, that)
19+
20+
def /(that: ExpressionC): ExpressionC =
21+
Division(this, that)
22+
23+
def eval: Double =
24+
type Continuation = Double => Double
25+
26+
def loop(expr: ExpressionC, cont: Continuation): Double =
27+
expr match
28+
case Literal(value) => cont(value)
29+
case Addition(left, right) =>
30+
loop(left, l => loop(right, r => cont(l + r)))
31+
case Subtraction(left, right) =>
32+
loop(left, l => loop(right, r => cont(l - r)))
33+
case Multiplication(left, right) =>
34+
loop(left, l => loop(right, r => cont(l * r)))
35+
case Division(left, right) =>
36+
loop(left, l => loop(right, r => cont(l / r)))
37+
38+
loop(this, identity)
39+
40+
object ExpressionC:
41+
def apply(value: Double): ExpressionC =
42+
Literal(value)

ch05/src/ExpressionT.scala

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package ch05
2+
3+
enum ExpressionT:
4+
case Literal(value: Double)
5+
case Addition(left: ExpressionT, right: ExpressionT)
6+
case Subtraction(left: ExpressionT, right: ExpressionT)
7+
case Multiplication(left: ExpressionT, right: ExpressionT)
8+
case Division(left: ExpressionT, right: ExpressionT)
9+
10+
def eval: Double =
11+
// Trampoline style.
12+
type Continuation = Double => Call
13+
14+
enum Call:
15+
case Continue(value: Double, k: Continuation)
16+
case Loop(expr: ExpressionT, k: Continuation)
17+
case Done(result: Double)
18+
19+
def loop2(left: ExpressionT, right: ExpressionT, cont: Continuation, op: (Double, Double) => Double): Call =
20+
Call.Loop(
21+
left,
22+
l => Call.Loop(right, r => Call.Continue(op(l, r), cont))
23+
)
24+
25+
def loop(expr: ExpressionT, cont: Continuation): Call =
26+
expr match
27+
case Literal(value) => Call.Continue(value, cont)
28+
case Addition(left, right) => loop2(left, right, cont, _ + _)
29+
case Subtraction(left, right) => loop2(left, right, cont, _ - _)
30+
case Multiplication(left, right) => loop2(left, right, cont, _ * _)
31+
case Division(left, right) => loop2(left, right, cont, _ / _)
32+
33+
def trampoline(call: Call): Double =
34+
call match
35+
case Call.Continue(value, k) => trampoline(k(value))
36+
case Call.Loop(expr, k) => trampoline(loop(expr, k))
37+
case Call.Done(result) => result
38+
39+
trampoline(loop(this, x => Call.Done(x)))
40+
41+
def +(that: ExpressionT): ExpressionT =
42+
Addition(this, that)
43+
44+
def -(that: ExpressionT): ExpressionT =
45+
Subtraction(this, that)
46+
47+
def *(that: ExpressionT): ExpressionT =
48+
Multiplication(this, that)
49+
50+
def /(that: ExpressionT): ExpressionT =
51+
Division(this, that)
52+
53+
object ExpressionT:
54+
def apply(value: Double): ExpressionT =
55+
Literal(value)

ch05/src/Regexp.scala

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ch05
2+
3+
enum Regexp:
4+
case Append(left: Regexp, right: Regexp)
5+
case OrElse(first: Regexp, second: Regexp)
6+
case Repeat(source: Regexp)
7+
case Apply(string: String)
8+
case Empty
9+
10+
def ++(that: Regexp): Regexp =
11+
Append(this, that)
12+
13+
def orElse(that: Regexp): Regexp =
14+
OrElse(this, that)
15+
16+
def repeat: Regexp =
17+
Repeat(this)
18+
19+
def `*`: Regexp = this.repeat
20+
21+
def matches(input: String): Boolean =
22+
def loop(regexp: Regexp, idx: Int): Option[Int] =
23+
regexp match
24+
case Append(left, right) =>
25+
loop(left, idx).flatMap(loop(right, _))
26+
case OrElse(first, second) =>
27+
loop(first, idx).orElse(loop(second, idx))
28+
case Repeat(source) =>
29+
loop(source, idx)
30+
.flatMap(loop(regexp, _))
31+
.orElse(Some(idx))
32+
case Apply(string) =>
33+
Option.when(input.startsWith(string, idx))(idx + string.size)
34+
case Empty => None
35+
36+
// Check we matched the entire input
37+
loop(this, 0).map(_ == input.size).getOrElse(false)
38+
39+
object Regexp:
40+
val empty: Regexp = Empty
41+
42+
def apply(string: String): Regexp =
43+
Apply(string)

ch05/src/RegexpC.scala

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package ch05
2+
3+
// Continuation-Passing style.
4+
enum RegexpC:
5+
case Append(left: RegexpC, right: RegexpC)
6+
case OrElse(first: RegexpC, second: RegexpC)
7+
case Repeat(source: RegexpC)
8+
case Apply(string: String)
9+
case Empty
10+
11+
def ++(that: RegexpC): RegexpC =
12+
Append(this, that)
13+
14+
def orElse(that: RegexpC): RegexpC =
15+
OrElse(this, that)
16+
17+
def repeat: RegexpC =
18+
Repeat(this)
19+
20+
def `*`: RegexpC = this.repeat
21+
22+
def matches(input: String): Boolean =
23+
// Define a type alias so we can easily write continuations.
24+
type Continuation = Option[Int] => Option[Int]
25+
26+
def loop(
27+
regexp: RegexpC,
28+
idx: Int,
29+
cont: Continuation
30+
): Option[Int] =
31+
regexp match
32+
case Append(left, right) =>
33+
val k: Continuation = _ match
34+
case None => cont(None)
35+
case Some(i) => loop(right, i, cont)
36+
loop(left, idx, k)
37+
38+
case OrElse(first, second) =>
39+
val k: Continuation = _ match
40+
case None => loop(second, idx, cont)
41+
case some => cont(some)
42+
loop(first, idx, k)
43+
44+
case Repeat(source) =>
45+
val k: Continuation =
46+
_ match
47+
case None => cont(Some(idx))
48+
case Some(i) => loop(regexp, i, cont)
49+
loop(source, idx, k)
50+
51+
case Apply(string) =>
52+
cont(Option.when(input.startsWith(string, idx))(idx + string.size))
53+
54+
case Empty =>
55+
cont(None)
56+
57+
// Check we matched the entire input
58+
loop(this, 0, identity).map(_ == input.size).getOrElse(false)
59+
60+
object RegexpC:
61+
val empty: RegexpC = Empty
62+
63+
def apply(string: String): RegexpC =
64+
Apply(string)

ch05/src/RegexpT.scala

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package ch05
2+
3+
enum RegexpT:
4+
def ++(that: RegexpT): RegexpT =
5+
Append(this, that)
6+
7+
def orElse(that: RegexpT): RegexpT =
8+
OrElse(this, that)
9+
10+
def repeat: RegexpT =
11+
Repeat(this)
12+
13+
def `*`: RegexpT = this.repeat
14+
15+
def matches(input: String): Boolean =
16+
/*
17+
Scala's runtimes don't support full tail calls, so calls from a continuation
18+
to loop or from loop to a continuation will use a stack frame.
19+
So, instead of making a call, we return a value that reifies the call we want to make.
20+
This idea is the core of trampolining.
21+
*/
22+
// Define a type alias so we can easily write continuations.
23+
type Continuation = Option[Int] => Call
24+
25+
enum Call:
26+
case Loop(regexp: RegexpT, index: Int, continuation: Continuation)
27+
case Continue(index: Option[Int], continuation: Continuation)
28+
case Done(index: Option[Int])
29+
30+
def loop(regexp: RegexpT, idx: Int, cont: Continuation): Call =
31+
regexp match
32+
case Append(left, right) =>
33+
val k: Continuation = _ match
34+
case None => Call.Continue(None, cont)
35+
case Some(i) => Call.Loop(right, i, cont)
36+
Call.Loop(left, idx, k)
37+
38+
case OrElse(first, second) =>
39+
val k: Continuation = _ match
40+
case None => Call.Loop(second, idx, cont)
41+
case some => Call.Continue(some, cont)
42+
Call.Loop(first, idx, k)
43+
44+
case Repeat(source) =>
45+
val k: Continuation =
46+
_ match
47+
case None => Call.Continue(Some(idx), cont)
48+
case Some(i) => Call.Loop(regexp, i, cont)
49+
Call.Loop(source, idx, k)
50+
51+
// The following could directly call 'cont' with the Option
52+
// if Scala had support for full tail calls.
53+
case Apply(string) =>
54+
Call.Continue(
55+
Option.when(input.startsWith(string, idx))(idx + string.size),
56+
cont
57+
)
58+
59+
case Empty =>
60+
Call.Continue(None, cont)
61+
62+
def trampoline(next: Call): Option[Int] =
63+
next match
64+
case Call.Loop(regexp, index, continuation) =>
65+
trampoline(loop(regexp, index, continuation))
66+
case Call.Continue(index, continuation) =>
67+
trampoline(continuation(index))
68+
case Call.Done(index) => index
69+
70+
// Check we matched the entire input
71+
trampoline(loop(this, 0, Call.Done(_)))
72+
.map(_ == input.size)
73+
.getOrElse(false)
74+
75+
case Append(left: RegexpT, right: RegexpT)
76+
case OrElse(first: RegexpT, second: RegexpT)
77+
case Repeat(source: RegexpT)
78+
case Apply(string: String)
79+
case Empty
80+
81+
object RegexpT:
82+
val empty: RegexpT = Empty
83+
84+
def apply(string: String): RegexpT =
85+
Apply(string)

ch05/test/src/ExpressionCSpec.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ch05
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class ExpressionCSpec extends AnyFunSpec:
7+
describe("ExpressionC"):
8+
it("eval"):
9+
val fortyTwo = ((ExpressionC(15.0) + ExpressionC(5.0)) * ExpressionC(2.0) + ExpressionC(2.0)) / ExpressionC(1.0)
10+
fortyTwo.eval shouldBe 42.0d
11+

ch05/test/src/ExpressionSpec.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ch05
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class ExpressionSpec extends AnyFunSpec:
7+
describe("Expression"):
8+
it("eval"):
9+
val fortyTwo = ((Expression(15.0) + Expression(5.0)) * Expression(2.0) + Expression(2.0)) / Expression(1.0)
10+
fortyTwo.eval shouldBe 42.0d
11+

ch05/test/src/ExpressionTSpec.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ch05
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class ExpressionTSpec extends AnyFunSpec:
7+
describe("ExpressionT"):
8+
it("eval"):
9+
val fortyTwo = ((ExpressionT(15.0) + ExpressionT(5.0)) * ExpressionT(2.0) + ExpressionT(2.0)) / ExpressionT(1.0)
10+
fortyTwo.eval shouldBe 42.0d
11+

0 commit comments

Comments
 (0)