diff --git a/.scalafmt.conf b/.scalafmt.conf index 974d1a1..c6078e0 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -3,6 +3,7 @@ align.preset = more maxColumn = 120 runner.dialect = scala3 assumeStandardLibraryStripMargin = true +# https://github.com/scalameta/scalameta/issues/4090 project.excludePaths = [ "glob:**/ch04/src/**.scala" ] diff --git a/ch05/src/Expression.scala b/ch05/src/Expression.scala new file mode 100644 index 0000000..69b17ca --- /dev/null +++ b/ch05/src/Expression.scala @@ -0,0 +1,48 @@ +package ch05 + +/* +The core of the interpreter strategy is a separation between description and action. +The description is the program, and the interpreter is the action that carries out the program. + +This separation is allows for composition of programs, and managing effects by delaying +them till the time the program is run. We sometimes call this structure an algebra, with +constructs and combinators defining programs and destructors defining interpreters. +The interpreter is then a structural recursion over this ADT. + +We saw that the straightforward implementation is not stack-safe, and which caused us to +to introduction the idea of tail recursion and continuations. We reified continuations +functions, and saw that we can convert any program into continuation-passing style which +has every method call in tail position. Due to Scala runtime limitations not all calls +in tail position can be converted to tail calls, so we reified calls and returns into +data structures used by a recursive loop called a trampoline. + */ +enum Expression: + case Literal(value: Double) + case Addition(left: Expression, right: Expression) + case Subtraction(left: Expression, right: Expression) + case Multiplication(left: Expression, right: Expression) + case Division(left: Expression, right: Expression) + + def +(that: Expression): Expression = + Addition(this, that) + + def -(that: Expression): Expression = + Subtraction(this, that) + + def *(that: Expression): Expression = + Multiplication(this, that) + + def /(that: Expression): Expression = + Division(this, that) + + def eval: Double = + this match + case Literal(value) => value + case Addition(left, right) => left.eval + right.eval + case Subtraction(left, right) => left.eval - right.eval + case Multiplication(left, right) => left.eval * right.eval + case Division(left, right) => left.eval / right.eval + +object Expression: + def apply(value: Double): Expression = + Literal(value) diff --git a/ch05/src/ExpressionC.scala b/ch05/src/ExpressionC.scala new file mode 100644 index 0000000..4d9a4fd --- /dev/null +++ b/ch05/src/ExpressionC.scala @@ -0,0 +1,42 @@ +package ch05 + +// Continuation-Passing style. +enum ExpressionC: + case Literal(value: Double) + case Addition(left: ExpressionC, right: ExpressionC) + case Subtraction(left: ExpressionC, right: ExpressionC) + case Multiplication(left: ExpressionC, right: ExpressionC) + case Division(left: ExpressionC, right: ExpressionC) + + def +(that: ExpressionC): ExpressionC = + Addition(this, that) + + def -(that: ExpressionC): ExpressionC = + Subtraction(this, that) + + def *(that: ExpressionC): ExpressionC = + Multiplication(this, that) + + def /(that: ExpressionC): ExpressionC = + Division(this, that) + + def eval: Double = + type Continuation = Double => Double + + def loop(expr: ExpressionC, cont: Continuation): Double = + expr match + case Literal(value) => cont(value) + case Addition(left, right) => + loop(left, l => loop(right, r => cont(l + r))) + case Subtraction(left, right) => + loop(left, l => loop(right, r => cont(l - r))) + case Multiplication(left, right) => + loop(left, l => loop(right, r => cont(l * r))) + case Division(left, right) => + loop(left, l => loop(right, r => cont(l / r))) + + loop(this, identity) + +object ExpressionC: + def apply(value: Double): ExpressionC = + Literal(value) diff --git a/ch05/src/ExpressionT.scala b/ch05/src/ExpressionT.scala new file mode 100644 index 0000000..232e3a4 --- /dev/null +++ b/ch05/src/ExpressionT.scala @@ -0,0 +1,66 @@ +package ch05 + +enum ExpressionT: + case Literal(value: Double) + case Addition(left: ExpressionT, right: ExpressionT) + case Subtraction(left: ExpressionT, right: ExpressionT) + case Multiplication(left: ExpressionT, right: ExpressionT) + case Division(left: ExpressionT, right: ExpressionT) + + import ExpressionT.* + def eval: Double = + def loop(expr: ExpressionT, cont: Continuation): Call = + expr match + case Literal(value) => Call.Continue(value, cont) + case Addition(left, right) => + Call.Loop( + left, + l => Call.Loop(right, r => Call.Continue(l + r, cont)) + ) + case Subtraction(left, right) => + Call.Loop( + left, + l => Call.Loop(right, r => Call.Continue(l - r, cont)) + ) + case Multiplication(left, right) => + Call.Loop( + left, + l => Call.Loop(right, r => Call.Continue(l * r, cont)) + ) + case Division(left, right) => + Call.Loop( + left, + l => Call.Loop(right, r => Call.Continue(l / r, cont)) + ) + + def trampoline(call: Call): Double = + call match + case Call.Continue(value, k) => trampoline(k(value)) + case Call.Loop(expr, k) => trampoline(loop(expr, k)) + case Call.Done(result) => result + + trampoline(loop(this, x => Call.Done(x))) + + def +(that: ExpressionT): ExpressionT = + Addition(this, that) + + def -(that: ExpressionT): ExpressionT = + Subtraction(this, that) + + def *(that: ExpressionT): ExpressionT = + Multiplication(this, that) + + def /(that: ExpressionT): ExpressionT = + Division(this, that) + +object ExpressionT: + def apply(value: Double): ExpressionT = + Literal(value) + + // Trampoline style. + type Continuation = Double => Call + + enum Call: + case Continue(value: Double, k: Continuation) + case Loop(expr: ExpressionT, k: Continuation) + case Done(result: Double) diff --git a/ch05/src/Regexp.scala b/ch05/src/Regexp.scala new file mode 100644 index 0000000..676eb44 --- /dev/null +++ b/ch05/src/Regexp.scala @@ -0,0 +1,43 @@ +package ch05 + +enum Regexp: + case Append(left: Regexp, right: Regexp) + case OrElse(first: Regexp, second: Regexp) + case Repeat(source: Regexp) + case Apply(string: String) + case Empty + + def ++(that: Regexp): Regexp = + Append(this, that) + + def orElse(that: Regexp): Regexp = + OrElse(this, that) + + def repeat: Regexp = + Repeat(this) + + def `*`: Regexp = this.repeat + + def matches(input: String): Boolean = + def loop(regexp: Regexp, idx: Int): Option[Int] = + regexp match + case Append(left, right) => + loop(left, idx).flatMap(loop(right, _)) + case OrElse(first, second) => + loop(first, idx).orElse(loop(second, idx)) + case Repeat(source) => + loop(source, idx) + .flatMap(loop(regexp, _)) + .orElse(Some(idx)) + case Apply(string) => + Option.when(input.startsWith(string, idx))(idx + string.size) + case Empty => None + + // Check we matched the entire input + loop(this, 0).map(_ == input.size).getOrElse(false) + +object Regexp: + val empty: Regexp = Empty + + def apply(string: String): Regexp = + Apply(string) diff --git a/ch05/src/RegexpC.scala b/ch05/src/RegexpC.scala new file mode 100644 index 0000000..85e1c61 --- /dev/null +++ b/ch05/src/RegexpC.scala @@ -0,0 +1,64 @@ +package ch05 + +// Continuation-Passing style. +enum RegexpC: + case Append(left: RegexpC, right: RegexpC) + case OrElse(first: RegexpC, second: RegexpC) + case Repeat(source: RegexpC) + case Apply(string: String) + case Empty + + def ++(that: RegexpC): RegexpC = + Append(this, that) + + def orElse(that: RegexpC): RegexpC = + OrElse(this, that) + + def repeat: RegexpC = + Repeat(this) + + def `*`: RegexpC = this.repeat + + def matches(input: String): Boolean = + // Define a type alias so we can easily write continuations. + type Continuation = Option[Int] => Option[Int] + + def loop( + regexp: RegexpC, + idx: Int, + cont: Continuation + ): Option[Int] = + regexp match + case Append(left, right) => + val k: Continuation = _ match + case None => cont(None) + case Some(i) => loop(right, i, cont) + loop(left, idx, k) + + case OrElse(first, second) => + val k: Continuation = _ match + case None => loop(second, idx, cont) + case some => cont(some) + loop(first, idx, k) + + case Repeat(source) => + val k: Continuation = + _ match + case None => cont(Some(idx)) + case Some(i) => loop(regexp, i, cont) + loop(source, idx, k) + + case Apply(string) => + cont(Option.when(input.startsWith(string, idx))(idx + string.size)) + + case Empty => + cont(None) + + // Check we matched the entire input + loop(this, 0, identity).map(_ == input.size).getOrElse(false) + +object RegexpC: + val empty: RegexpC = Empty + + def apply(string: String): RegexpC = + Apply(string) diff --git a/ch05/src/RegexpT.scala b/ch05/src/RegexpT.scala new file mode 100644 index 0000000..e892eb5 --- /dev/null +++ b/ch05/src/RegexpT.scala @@ -0,0 +1,86 @@ +package ch05 + +enum RegexpT: + def ++(that: RegexpT): RegexpT = + Append(this, that) + + def orElse(that: RegexpT): RegexpT = + OrElse(this, that) + + def repeat: RegexpT = + Repeat(this) + + def `*`: RegexpT = this.repeat + + import RegexpT.* + def matches(input: String): Boolean = + def loop(regexp: RegexpT, idx: Int, cont: Continuation): Call = + regexp match + case Append(left, right) => + val k: Continuation = _ match + case None => Call.Continue(None, cont) + case Some(i) => Call.Loop(right, i, cont) + Call.Loop(left, idx, k) + + case OrElse(first, second) => + val k: Continuation = _ match + case None => Call.Loop(second, idx, cont) + case some => Call.Continue(some, cont) + Call.Loop(first, idx, k) + + case Repeat(source) => + val k: Continuation = + _ match + case None => Call.Continue(Some(idx), cont) + case Some(i) => Call.Loop(regexp, i, cont) + Call.Loop(source, idx, k) + + // The following could directly call 'cont' with the Option + // if Scala had support for full tail calls. + case Apply(string) => + Call.Continue( + Option.when(input.startsWith(string, idx))(idx + string.size), + cont + ) + + case Empty => + Call.Continue(None, cont) + + def trampoline(next: Call): Option[Int] = + next match + case Call.Loop(regexp, index, continuation) => + trampoline(loop(regexp, index, continuation)) + case Call.Continue(index, continuation) => + trampoline(continuation(index)) + case Call.Done(index) => index + + // Check we matched the entire input + trampoline(loop(this, 0, Call.Done(_))) + .map(_ == input.size) + .getOrElse(false) + + case Append(left: RegexpT, right: RegexpT) + case OrElse(first: RegexpT, second: RegexpT) + case Repeat(source: RegexpT) + case Apply(string: String) + case Empty + +object RegexpT: + val empty: RegexpT = Empty + + def apply(string: String): RegexpT = + Apply(string) + + /* + Scala's runtimes don't support full tail calls, so calls from a continuation + to loop or from loop to a continuation will use a stack frame. + So, instead of making a call, we return a value that reifies the call we want to make. + This idea is the core of trampolining. + */ + // Define a type alias so we can easily write continuations. + type Continuation = Option[Int] => Call + + enum Call: + case Loop(regexp: RegexpT, index: Int, continuation: Continuation) + case Continue(index: Option[Int], continuation: Continuation) + case Done(index: Option[Int]) diff --git a/ch05/test/src/ExpressionCSpec.scala b/ch05/test/src/ExpressionCSpec.scala new file mode 100644 index 0000000..662dd56 --- /dev/null +++ b/ch05/test/src/ExpressionCSpec.scala @@ -0,0 +1,11 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe + +class ExpressionCSpec extends AnyFunSpec: + describe("ExpressionC"): + it("eval"): + val fortyTwo = ((ExpressionC(15.0) + ExpressionC(5.0)) * ExpressionC(2.0) + ExpressionC(2.0)) / ExpressionC(1.0) + fortyTwo.eval shouldBe 42.0d + diff --git a/ch05/test/src/ExpressionSpec.scala b/ch05/test/src/ExpressionSpec.scala new file mode 100644 index 0000000..618d285 --- /dev/null +++ b/ch05/test/src/ExpressionSpec.scala @@ -0,0 +1,11 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe + +class ExpressionSpec extends AnyFunSpec: + describe("Expression"): + it("eval"): + val fortyTwo = ((Expression(15.0) + Expression(5.0)) * Expression(2.0) + Expression(2.0)) / Expression(1.0) + fortyTwo.eval shouldBe 42.0d + diff --git a/ch05/test/src/ExpressionTSpec.scala b/ch05/test/src/ExpressionTSpec.scala new file mode 100644 index 0000000..b9c1d3b --- /dev/null +++ b/ch05/test/src/ExpressionTSpec.scala @@ -0,0 +1,11 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe + +class ExpressionTSpec extends AnyFunSpec: + describe("ExpressionT"): + it("eval"): + val fortyTwo = ((ExpressionT(15.0) + ExpressionT(5.0)) * ExpressionT(2.0) + ExpressionT(2.0)) / ExpressionT(1.0) + fortyTwo.eval shouldBe 42.0d + diff --git a/ch05/test/src/RegexpCSpec.scala b/ch05/test/src/RegexpCSpec.scala new file mode 100644 index 0000000..b2fd628 --- /dev/null +++ b/ch05/test/src/RegexpCSpec.scala @@ -0,0 +1,25 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe +import org.scalatest.prop.TableDrivenPropertyChecks.* + +class RegexpCSpec extends AnyFunSpec: + describe("RegexpC"): + it("matches"): + val txts = + Table( + ("txt", "match"), + ("Scala", true), + ("Scalalalala", true), + ("Sca", false), + ("Scalal", false), + ("Scalaland", false) + ) + // left-associative + val regexp = RegexpC("Sca") ++ RegexpC("la") ++ RegexpC("la").repeat + + forAll (txts) { (txt: String, `match`: Boolean) => + regexp.matches(txt) shouldBe `match` + } + diff --git a/ch05/test/src/RegexpSpec.scala b/ch05/test/src/RegexpSpec.scala new file mode 100644 index 0000000..64f30bd --- /dev/null +++ b/ch05/test/src/RegexpSpec.scala @@ -0,0 +1,25 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe +import org.scalatest.prop.TableDrivenPropertyChecks.* + +class RegexpSpec extends AnyFunSpec: + describe("Regexp"): + it("matches"): + val txts = + Table( + ("txt", "match"), + ("Scala", true), + ("Scalalalala", true), + ("Sca", false), + ("Scalal", false), + ("Scalaland", false) + ) + // left-associative + val regexp = Regexp("Sca") ++ Regexp("la") ++ Regexp("la").repeat + + forAll (txts) { (txt: String, `match`: Boolean) => + regexp.matches(txt) shouldBe `match` + } + diff --git a/ch05/test/src/RegexpTSpec.scala b/ch05/test/src/RegexpTSpec.scala new file mode 100644 index 0000000..9096b06 --- /dev/null +++ b/ch05/test/src/RegexpTSpec.scala @@ -0,0 +1,10 @@ +package ch05 + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.shouldBe + +class RegexpTSpec extends AnyFunSpec: + describe("RegexpT"): + it("matches"): + RegexpT("a").repeat.matches("a" * 20000) shouldBe true +