Skip to content

Commit

Permalink
Complete ch03
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 19, 2024
1 parent a39c8b7 commit bc4c7f9
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The older code is available in branches.

## Contents

2. [Algebraic Data Types](ch02)
3. [Objects as Codata](ch03)

## Running tests
```
./.github/run.sh <directory>
Expand Down
25 changes: 25 additions & 0 deletions ch03/src/Bool.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ch03

trait Bool:
def `if`[A](t: A)(f: A): A

val True = new Bool:
def `if`[A](t: A)(f: A): A = t

val False = new Bool:
def `if`[A](t: A)(f: A): A = f

def and(l: Bool, r: Bool): Bool =
new Bool:
def `if`[A](t: A)(f: A): A =
l.`if`(r)(False).`if`(t)(f)

def or(l: Bool, r: Bool): Bool =
new Bool:
def `if`[A](t: A)(f: A): A =
l.`if`(True)(r).`if`(t)(f)

def not(b: Bool): Bool =
new Bool:
def `if`[A](t: A)(f: A): A =
b.`if`(True)(False).`if`(f)(t)
37 changes: 37 additions & 0 deletions ch03/src/Codata.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ch03

/*
codata is a product of destructors, where destructors are functions from
the codata type (and, optionally, some other inputs) to some type.
In Scala we define codata as a trait, and implement it as a final class,
anonymous subclass, or an object.
We have two strategies for implementing methods using codata: structural corecursion, which we can
use when the result is codata, and structural recursion, which we can use when an input is codata.
Data is connected to codata via fold: any data can instead be implemented as codata with
a single destructor that is the fold for that data. This is called Church encoding.
The reverse is also: we can enumerate all potential pairs of inputs and outputs of
destructors to represent codata as data.
Data and codata offer different forms of extensibility: data makes it easy to add
new functions, but adding new elements requires changing existing code, while it is
easy to add new elements to codata but we change existing code if we add new functions.
*/
// Polymorphic function type, new in Scala 3.
// https://www.youtube.com/watch?v=sauaDZ-1-zM
type List[A, B] = (B, (A, B) => B) => B

// Polymorphic function of two types, A and B, that takes no arg
// and returns the type defined above.
val Empty: [A, B] => () => List[A, B] =
// empty: B
[A, B] => () => (empty, _) => empty

val Pair: [A, B] => (A, List[A, B]) => List[A, B] =
// empty: B, f: (A, B) => B
[A, B] => (head: A, tail: List[A, B]) => (empty, f) => f(head, tail(empty, f))

val list: [B] => () => List[Int, B] =
[B] => () => Pair(1, Pair(2, Pair(3, Empty())))
23 changes: 23 additions & 0 deletions ch03/src/List.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ch03

package data:
enum List[A]:
case Pair(head: A, tail: List[A])
case Empty()

def foldRight[B](empty: B)(f: (A, B) => B): B =
this match
case Pair(head, tail) => f(head, tail.foldRight(empty)(f))
case Empty() => empty

package codata:
trait List[A]:
def foldRight[B](empty: B)(f: (A, B) => B): B

final class Pair(head: A, tail: List[A]) extends List[A]:
def foldRight[B](empty: B)(f: (A, B) => B): B =
f(head, tail.foldRight(empty)(f))

final class Empty extends List[A]:
def foldRight[B](empty: B)(f: (A, B) => B): B =
empty
40 changes: 40 additions & 0 deletions ch03/src/Set.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package ch03

trait Set[A]:

def contains(elt: A): Boolean

def insert(elt: A): Set[A] =
InsertOneSet(elt, this)

def union(that: Set[A]): Set[A] =
UnionSet(this, that)

final class ListSet[A](elements: scala.collection.immutable.List[A]) extends Set[A]:

def contains(elt: A): Boolean =
elements.contains(elt)

override def insert(elt: A): Set[A] =
ListSet(elt :: elements)

override def union(that: Set[A]): Set[A] =
elements.foldLeft(that) { (set, elt) => set.insert(elt) }

object ListSet:
def empty[A]: Set[A] = ListSet(List.empty)

final class InsertOneSet[A](element: A, source: Set[A]) extends Set[A]:

def contains(elt: A): Boolean =
elt == element || source.contains(elt)

final class UnionSet[A](first: Set[A], second: Set[A]) extends Set[A]:

def contains(elt: A): Boolean =
first.contains(elt) || second.contains(elt)

object Evens extends Set[Int]:

def contains(elt: Int): Boolean =
(elt % 2 == 0)
58 changes: 58 additions & 0 deletions ch03/src/Stream.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package ch03

trait Stream[A]:
def head: A
def tail: Stream[A]
override def toString: String = head.toString

def take(count: Int): scala.collection.immutable.List[A] =
count match
case 0 => Nil
case n => head :: tail.take(n - 1)

def map[B](f: A => B): Stream[B] =
val self = this
new Stream[B]:
def head: B = f(self.head)
def tail: Stream[B] = self.tail.map(f)

def filter(p: A => Boolean): Stream[A] =
lazy val self = if p(head) then this else tail.filter(p)
new Stream[A]:
def head: A = self.head
def tail: Stream[A] = self.tail.filter(p)

def zip[B](that: Stream[B]): Stream[(A, B)] =
val self = this
new Stream[(A, B)]:
def head: (A, B) = (self.head, that.head)
def tail: Stream[(A, B)] = self.tail.zip(that.tail)

def scanLeft[B](zero: B)(f: (B, A) => B): Stream[B] =
val self = this
new Stream[B]:
def head: B = zero
def tail: Stream[B] = self.tail.scanLeft(f(zero, self.head))(f)

object Stream:
def unfold[A, B](seed: A)(f: A => B, next: A => A): Stream[B] =
new Stream[B]:
def head: B = f(seed)
def tail: Stream[B] = unfold(next(seed))(f, next)

val ones: Stream[Int] =
new Stream[Int]:
def head: Int = 1

def tail: Stream[Int] = ones

val alternating: Stream[Int] = Stream.unfold(true)(if _ then 1 else -1, !_)

val naturals: Stream[Int] = ones.scanLeft(1)(_ + _)

val naturals2: Stream[Int] = unfold(1)(x => x, _ + 1)

val naturals3: Stream[Int] =
new Stream:
def head = 1
def tail: Stream[Int] = naturals.map(_ + 1)
23 changes: 23 additions & 0 deletions ch03/test/src/BoolSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ch03

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class BoolSpec extends AnyFunSpec:
describe("Bool"):
it("and"):
and(True, True).`if`("yes")("no") shouldBe "yes"
and(True, False).`if`("yes")("no") shouldBe "no"
and(False, True).`if`("yes")("no") shouldBe "no"
and(False, False).`if`("yes")("no") shouldBe "no"

it("or"):
or(True, True).`if`("yes")("no") shouldBe "yes"
or(True, False).`if`("yes")("no") shouldBe "yes"
or(False, True).`if`("yes")("no") shouldBe "yes"
or(False, False).`if`("yes")("no") shouldBe "no"

it("not"):
not(True).`if`("yes")("no") shouldBe "no"
not(False).`if`("yes")("no") shouldBe "yes"

14 changes: 14 additions & 0 deletions ch03/test/src/CodataSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ch03

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class CodataSpec extends AnyFunSpec:
describe("Codata"):
it("list"):
val sum = list()(0, (a, b) => a + b)
sum shouldBe 6

val product = list()(1, (a, b) => a * b)
product shouldBe 6

19 changes: 19 additions & 0 deletions ch03/test/src/SetSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ch03

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class SetSpec extends AnyFunSpec:
describe("Set"):
it("evens"):
val evensAndOne = Evens.insert(1)
val evensAndOthers =
Evens.union(ListSet.empty.insert(1).insert(3))

evensAndOne.contains(1) shouldBe true
evensAndOthers.contains(1) shouldBe true
evensAndOne.contains(2) shouldBe true
evensAndOthers.contains(2) shouldBe true
evensAndOne.contains(3) shouldBe false
evensAndOthers.contains(3) shouldBe true

23 changes: 23 additions & 0 deletions ch03/test/src/StreamSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ch03

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe

class StreamSpec extends AnyFunSpec:
describe("Stream"):
it("ones"):
Stream.ones.take(5) shouldBe List.fill(5)(1)

it("alternating"):
Stream.alternating.take(5) shouldBe List.iterate(1, 5)(_ * -1)

it("filter"):
Stream.alternating.filter(_ > 0).take(5) shouldBe List.fill(5)(1)

it("naturals"):
Stream.naturals.take(5) shouldBe (1 to 5)
Stream.naturals2.take(5) shouldBe (1 to 5)
Stream.naturals3.take(5) shouldBe (1 to 5)



0 comments on commit bc4c7f9

Please sign in to comment.