Skip to content

Commit bc4c7f9

Browse files
author
Abhijit Sarkar
committed
Complete ch03
1 parent a39c8b7 commit bc4c7f9

10 files changed

+265
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ The older code is available in branches.
99

1010
## Contents
1111

12+
2. [Algebraic Data Types](ch02)
13+
3. [Objects as Codata](ch03)
14+
1215
## Running tests
1316
```
1417
./.github/run.sh <directory>

ch03/src/Bool.scala

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package ch03
2+
3+
trait Bool:
4+
def `if`[A](t: A)(f: A): A
5+
6+
val True = new Bool:
7+
def `if`[A](t: A)(f: A): A = t
8+
9+
val False = new Bool:
10+
def `if`[A](t: A)(f: A): A = f
11+
12+
def and(l: Bool, r: Bool): Bool =
13+
new Bool:
14+
def `if`[A](t: A)(f: A): A =
15+
l.`if`(r)(False).`if`(t)(f)
16+
17+
def or(l: Bool, r: Bool): Bool =
18+
new Bool:
19+
def `if`[A](t: A)(f: A): A =
20+
l.`if`(True)(r).`if`(t)(f)
21+
22+
def not(b: Bool): Bool =
23+
new Bool:
24+
def `if`[A](t: A)(f: A): A =
25+
b.`if`(True)(False).`if`(f)(t)

ch03/src/Codata.scala

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package ch03
2+
3+
/*
4+
codata is a product of destructors, where destructors are functions from
5+
the codata type (and, optionally, some other inputs) to some type.
6+
7+
In Scala we define codata as a trait, and implement it as a final class,
8+
anonymous subclass, or an object.
9+
10+
We have two strategies for implementing methods using codata: structural corecursion, which we can
11+
use when the result is codata, and structural recursion, which we can use when an input is codata.
12+
13+
Data is connected to codata via fold: any data can instead be implemented as codata with
14+
a single destructor that is the fold for that data. This is called Church encoding.
15+
The reverse is also: we can enumerate all potential pairs of inputs and outputs of
16+
destructors to represent codata as data.
17+
18+
Data and codata offer different forms of extensibility: data makes it easy to add
19+
new functions, but adding new elements requires changing existing code, while it is
20+
easy to add new elements to codata but we change existing code if we add new functions.
21+
*/
22+
// Polymorphic function type, new in Scala 3.
23+
// https://www.youtube.com/watch?v=sauaDZ-1-zM
24+
type List[A, B] = (B, (A, B) => B) => B
25+
26+
// Polymorphic function of two types, A and B, that takes no arg
27+
// and returns the type defined above.
28+
val Empty: [A, B] => () => List[A, B] =
29+
// empty: B
30+
[A, B] => () => (empty, _) => empty
31+
32+
val Pair: [A, B] => (A, List[A, B]) => List[A, B] =
33+
// empty: B, f: (A, B) => B
34+
[A, B] => (head: A, tail: List[A, B]) => (empty, f) => f(head, tail(empty, f))
35+
36+
val list: [B] => () => List[Int, B] =
37+
[B] => () => Pair(1, Pair(2, Pair(3, Empty())))

ch03/src/List.scala

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ch03
2+
3+
package data:
4+
enum List[A]:
5+
case Pair(head: A, tail: List[A])
6+
case Empty()
7+
8+
def foldRight[B](empty: B)(f: (A, B) => B): B =
9+
this match
10+
case Pair(head, tail) => f(head, tail.foldRight(empty)(f))
11+
case Empty() => empty
12+
13+
package codata:
14+
trait List[A]:
15+
def foldRight[B](empty: B)(f: (A, B) => B): B
16+
17+
final class Pair(head: A, tail: List[A]) extends List[A]:
18+
def foldRight[B](empty: B)(f: (A, B) => B): B =
19+
f(head, tail.foldRight(empty)(f))
20+
21+
final class Empty extends List[A]:
22+
def foldRight[B](empty: B)(f: (A, B) => B): B =
23+
empty

ch03/src/Set.scala

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package ch03
2+
3+
trait Set[A]:
4+
5+
def contains(elt: A): Boolean
6+
7+
def insert(elt: A): Set[A] =
8+
InsertOneSet(elt, this)
9+
10+
def union(that: Set[A]): Set[A] =
11+
UnionSet(this, that)
12+
13+
final class ListSet[A](elements: scala.collection.immutable.List[A]) extends Set[A]:
14+
15+
def contains(elt: A): Boolean =
16+
elements.contains(elt)
17+
18+
override def insert(elt: A): Set[A] =
19+
ListSet(elt :: elements)
20+
21+
override def union(that: Set[A]): Set[A] =
22+
elements.foldLeft(that) { (set, elt) => set.insert(elt) }
23+
24+
object ListSet:
25+
def empty[A]: Set[A] = ListSet(List.empty)
26+
27+
final class InsertOneSet[A](element: A, source: Set[A]) extends Set[A]:
28+
29+
def contains(elt: A): Boolean =
30+
elt == element || source.contains(elt)
31+
32+
final class UnionSet[A](first: Set[A], second: Set[A]) extends Set[A]:
33+
34+
def contains(elt: A): Boolean =
35+
first.contains(elt) || second.contains(elt)
36+
37+
object Evens extends Set[Int]:
38+
39+
def contains(elt: Int): Boolean =
40+
(elt % 2 == 0)

ch03/src/Stream.scala

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package ch03
2+
3+
trait Stream[A]:
4+
def head: A
5+
def tail: Stream[A]
6+
override def toString: String = head.toString
7+
8+
def take(count: Int): scala.collection.immutable.List[A] =
9+
count match
10+
case 0 => Nil
11+
case n => head :: tail.take(n - 1)
12+
13+
def map[B](f: A => B): Stream[B] =
14+
val self = this
15+
new Stream[B]:
16+
def head: B = f(self.head)
17+
def tail: Stream[B] = self.tail.map(f)
18+
19+
def filter(p: A => Boolean): Stream[A] =
20+
lazy val self = if p(head) then this else tail.filter(p)
21+
new Stream[A]:
22+
def head: A = self.head
23+
def tail: Stream[A] = self.tail.filter(p)
24+
25+
def zip[B](that: Stream[B]): Stream[(A, B)] =
26+
val self = this
27+
new Stream[(A, B)]:
28+
def head: (A, B) = (self.head, that.head)
29+
def tail: Stream[(A, B)] = self.tail.zip(that.tail)
30+
31+
def scanLeft[B](zero: B)(f: (B, A) => B): Stream[B] =
32+
val self = this
33+
new Stream[B]:
34+
def head: B = zero
35+
def tail: Stream[B] = self.tail.scanLeft(f(zero, self.head))(f)
36+
37+
object Stream:
38+
def unfold[A, B](seed: A)(f: A => B, next: A => A): Stream[B] =
39+
new Stream[B]:
40+
def head: B = f(seed)
41+
def tail: Stream[B] = unfold(next(seed))(f, next)
42+
43+
val ones: Stream[Int] =
44+
new Stream[Int]:
45+
def head: Int = 1
46+
47+
def tail: Stream[Int] = ones
48+
49+
val alternating: Stream[Int] = Stream.unfold(true)(if _ then 1 else -1, !_)
50+
51+
val naturals: Stream[Int] = ones.scanLeft(1)(_ + _)
52+
53+
val naturals2: Stream[Int] = unfold(1)(x => x, _ + 1)
54+
55+
val naturals3: Stream[Int] =
56+
new Stream:
57+
def head = 1
58+
def tail: Stream[Int] = naturals.map(_ + 1)

ch03/test/src/BoolSpec.scala

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ch03
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class BoolSpec extends AnyFunSpec:
7+
describe("Bool"):
8+
it("and"):
9+
and(True, True).`if`("yes")("no") shouldBe "yes"
10+
and(True, False).`if`("yes")("no") shouldBe "no"
11+
and(False, True).`if`("yes")("no") shouldBe "no"
12+
and(False, False).`if`("yes")("no") shouldBe "no"
13+
14+
it("or"):
15+
or(True, True).`if`("yes")("no") shouldBe "yes"
16+
or(True, False).`if`("yes")("no") shouldBe "yes"
17+
or(False, True).`if`("yes")("no") shouldBe "yes"
18+
or(False, False).`if`("yes")("no") shouldBe "no"
19+
20+
it("not"):
21+
not(True).`if`("yes")("no") shouldBe "no"
22+
not(False).`if`("yes")("no") shouldBe "yes"
23+

ch03/test/src/CodataSpec.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package ch03
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class CodataSpec extends AnyFunSpec:
7+
describe("Codata"):
8+
it("list"):
9+
val sum = list()(0, (a, b) => a + b)
10+
sum shouldBe 6
11+
12+
val product = list()(1, (a, b) => a * b)
13+
product shouldBe 6
14+

ch03/test/src/SetSpec.scala

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package ch03
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class SetSpec extends AnyFunSpec:
7+
describe("Set"):
8+
it("evens"):
9+
val evensAndOne = Evens.insert(1)
10+
val evensAndOthers =
11+
Evens.union(ListSet.empty.insert(1).insert(3))
12+
13+
evensAndOne.contains(1) shouldBe true
14+
evensAndOthers.contains(1) shouldBe true
15+
evensAndOne.contains(2) shouldBe true
16+
evensAndOthers.contains(2) shouldBe true
17+
evensAndOne.contains(3) shouldBe false
18+
evensAndOthers.contains(3) shouldBe true
19+

ch03/test/src/StreamSpec.scala

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ch03
2+
3+
import org.scalatest.funspec.AnyFunSpec
4+
import org.scalatest.matchers.should.Matchers.shouldBe
5+
6+
class StreamSpec extends AnyFunSpec:
7+
describe("Stream"):
8+
it("ones"):
9+
Stream.ones.take(5) shouldBe List.fill(5)(1)
10+
11+
it("alternating"):
12+
Stream.alternating.take(5) shouldBe List.iterate(1, 5)(_ * -1)
13+
14+
it("filter"):
15+
Stream.alternating.filter(_ > 0).take(5) shouldBe List.fill(5)(1)
16+
17+
it("naturals"):
18+
Stream.naturals.take(5) shouldBe (1 to 5)
19+
Stream.naturals2.take(5) shouldBe (1 to 5)
20+
Stream.naturals3.take(5) shouldBe (1 to 5)
21+
22+
23+

0 commit comments

Comments
 (0)