Skip to content

Commit 98652bc

Browse files
author
Abhijit Sarkar
committed
Complete ch05
1 parent 72a9350 commit 98652bc

13 files changed

+443
-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
]

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

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
import ExpressionT.*
11+
def eval: Double =
12+
def loop(expr: ExpressionT, cont: Continuation): Call =
13+
expr match
14+
case Literal(value) => Call.Continue(value, cont)
15+
case Addition(left, right) =>
16+
Call.Loop(
17+
left,
18+
l => Call.Loop(right, r => Call.Continue(l + r, cont))
19+
)
20+
case Subtraction(left, right) =>
21+
Call.Loop(
22+
left,
23+
l => Call.Loop(right, r => Call.Continue(l - r, cont))
24+
)
25+
case Multiplication(left, right) =>
26+
Call.Loop(
27+
left,
28+
l => Call.Loop(right, r => Call.Continue(l * r, cont))
29+
)
30+
case Division(left, right) =>
31+
Call.Loop(
32+
left,
33+
l => Call.Loop(right, r => Call.Continue(l / r, cont))
34+
)
35+
36+
def trampoline(call: Call): Double =
37+
call match
38+
case Call.Continue(value, k) => trampoline(k(value))
39+
case Call.Loop(expr, k) => trampoline(loop(expr, k))
40+
case Call.Done(result) => result
41+
42+
trampoline(loop(this, x => Call.Done(x)))
43+
44+
def +(that: ExpressionT): ExpressionT =
45+
Addition(this, that)
46+
47+
def -(that: ExpressionT): ExpressionT =
48+
Subtraction(this, that)
49+
50+
def *(that: ExpressionT): ExpressionT =
51+
Multiplication(this, that)
52+
53+
def /(that: ExpressionT): ExpressionT =
54+
Division(this, that)
55+
56+
object ExpressionT:
57+
def apply(value: Double): ExpressionT =
58+
Literal(value)
59+
60+
// Trampoline style.
61+
type Continuation = Double => Call
62+
63+
enum Call:
64+
case Continue(value: Double, k: Continuation)
65+
case Loop(expr: ExpressionT, k: Continuation)
66+
case Done(result: Double)

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

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

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)