Skip to content

Commit

Permalink
Complete chapter 3
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 17, 2023
1 parent d15bd87 commit 80eab21
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Install a BSP connection file:
mill mill.bsp.BSP/install
```

Then open VSCode command palette, and select "Metals: Switch build server".
Then open VSCode command palette, and select `Metals: Switch build server`.


## References
Expand Down
11 changes: 10 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ object chapter02 extends AdvancedScalaModule {
override def ivyDeps = Agg(
ivy"org.scalactic::scalactic:$scalatestVersion",
ivy"org.scalatest::scalatest:$scalatestVersion",
ivy"org.scalatestplus::scalacheck-1-17:$scalacheckVersion",
)
}
}

object chapter03 extends AdvancedScalaModule {
object test extends ScalaTests with TestModule.ScalaTest {
// // use `::` for scala deps, `:` for java deps
override def ivyDeps = Agg(
ivy"org.scalactic::scalactic:$scalatestVersion",
ivy"org.scalatest::scalatest:$scalatestVersion",
)
}
}
Expand Down
29 changes: 20 additions & 9 deletions chapter02/test/src/LibSpec.scala
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import org.scalatest.funspec.AnyFunSpec
import Lib.*
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.prop.TableDrivenPropertyChecks.Table
import org.scalatest.matchers.should.Matchers.shouldBe

class LibSpec extends AnyFunSpec:
class LibSpec extends AnyFunSpec with TableDrivenPropertyChecks:
describe("Chapter 2"):
it("fib should return the nth Fibonacci number"):
val fst20 = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181)
for ((expected, n) <- fst20.zipWithIndex)
assert(fib(n) == expected, s"for n=$n")
fib(n) shouldBe expected

val isSortedInput =
Table(
("as", "gt", "expected"),
(Array(1, 2, 3), (_: Int) > (_: Int), true),
(Array(1, 2, 1), (_: Int) > (_: Int), false),
(Array(3, 2, 1), (_: Int) < (_: Int), true),
(Array(1, 2, 3), (_: Int) < (_: Int), false)
)

it("isSorted should check if an array is sorted"):
assert(isSorted(Array(1, 2, 3), _ > _))
assert(!isSorted(Array(1, 2, 1), _ > _))
assert(isSorted(Array(3, 2, 1), _ < _))
assert(!isSorted(Array(1, 2, 3), _ < _))
forAll(isSortedInput) { (as: Array[Int], gt: (Int, Int) => Boolean, expected: Boolean) =>
isSorted(as, gt) shouldBe expected
}

it("curry should convert a two-argument function into an one-argument function that returns a function"):
val add = (x: Int, y: Int) => x + y
assert(curry(add)(1)(2) == 3)
curry(add)(1)(2) shouldBe 3

it("uncurry should reverse curry"):
val curriedAdd = (a: Int) => (b: Int) => a + b
assert(uncurry(curriedAdd)(1, 2) == 3)
uncurry(curriedAdd)(1, 2) shouldBe 3

it("compose should compose two functions"):
val plus2 = (x: Int) => x + 2
val times2 = (x: Int) => x * 2
assert(compose(times2, plus2)(2) == 8)
compose(times2, plus2)(2) shouldBe 8
12 changes: 12 additions & 0 deletions chapter03/src/Lib.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import List.*

object Lib:
/*
Exercise 3.1: What will be the result of the following match expression?
*/
val result = List(1, 2, 3, 4, 5) match
case Cons(x, Cons(2, Cons(4, _))) => x
case Nil => 42
case Cons(x, Cons(y, Cons(3, Cons(4, _)))) => x + y
case Cons(h, t) => h + sum(t)
case null => 101
214 changes: 214 additions & 0 deletions chapter03/src/List.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import scala.annotation.tailrec
enum List[+A]:
case Nil
case Cons(head: A, tail: List[A])

object List:
def apply[A](as: A*): List[A] =
if as.isEmpty then Nil
else Cons(as.head, apply(as.tail*))

def sum(ints: List[Int]): Int = ints match
case Nil => 0
case Cons(x, xs) => x + sum(xs)

def head[A](xs: List[A]): A = xs match
case Nil => sys.error("empty list")
case Cons(x, _) => x

/*
Exercise 3.2: Implement the function tail for removing the first element of a List.
*/
def tail[A](xs: List[A]): List[A] = xs match
case Nil => sys.error("empty list")
case Cons(_, ys) => ys

/*
Exercise 3.3: Implement the function setHead for replacing the first element of a list
with a different value.
*/
def setHead[A](xs: List[A], a: A): List[A] = xs match
case Nil => Cons(a, Nil)
case Cons(_, ys) => Cons(a, ys)

/*
Exercise 3.4: Implement the function drop, which removes the first n elements from a list.
Dropping n element from an empty list should return the empty list.
*/
def drop[A](xs: List[A], n: Int): List[A] =
if n <= 0 then xs
else
xs match
case Cons(_, ys) => drop(ys, n - 1)
case Nil => Nil

/*
Exercise 3.5: Implement dropWhile, which removes elements from the List prefix as
long as they match a predicate.
*/
def dropWhile[A](as: List[A], f: A => Boolean): List[A] =
as match
case Cons(hd, tl) if f(hd) => dropWhile(tl, f)
case _ => as

/*
Exercise 3.6: Implement a function, init, that returns a list containing of all
but the last element of a list.
*/
def init[A](xs: List[A]): List[A] = xs match
case Nil => sys.error("empty list")
case Cons(_, Nil) => Nil
case Cons(x, xs) => Cons(x, init(xs))

// def foldRight[A, B](as: List[A], acc: B, f: (A, B) => B): B = as match
// case Nil => acc
// case Cons(x, xs) => f(x, foldRight(xs, acc, f))

/*
Exercise 3.7: Can product, implemented using foldRight, immediately halt the
recursion and return 0.0 if it encounters a 0.0? Why or why not? Consider how
any short circuiting might work if you call foldRight with a large list.
---
No, foldRight traverses all the way to the end of the list before invoking the function.
*/

/*
Exercise 3.8: See what happens when you pass Nil and Cons themselves to foldRight,
like this: foldRight(List(1, 2, 3), Nil: List[Int], Cons(_, _)).
What do you think this says about the relationship between foldRight and the data
constructors of List?
--
Nothing happens, the original list is returned.
*/

/*
Exercise 3.9: Compute the length of a list using foldRight.
*/
def length[A](xs: List[A]): Int =
foldRight(xs, 0, (_, acc) => acc + 1)

/*
Exercise 3.10: foldRight is not stack safe. Write another general list-recursion function,
foldLeft, that is tail recursive. Start collapsing from the leftmost start of the list.
*/
@tailrec
def foldLeft[A, B](as: List[A], acc: B, f: (B, A) => B): B = as match
case Nil => acc
case Cons(x, xs) => foldLeft(xs, f(acc, x), f)

/*
Exercise 3.11: Write sum, product and a function to compute the length
of a list using foldLeft.
*/
def sumViaFoldLeft(xs: List[Int]): Int =
foldLeft(xs, 0, _ + _)

def productViaFoldLeft(xs: List[Int]): Int =
foldLeft(xs, 1, _ * _)

def lengthViaFoldLeft(xs: List[Int]): Int =
foldLeft(xs, 0, (acc, _) => acc + 1)

/*
Exercise 3.12: Write a function that returns the reverse of a list.
*/
def reverse[A](xs: List[A]): List[A] =
foldLeft(xs, Nil: List[A], (acc, x) => Cons(x, acc))

/*
Exercise 3.13: Can you write foldRight in terms of foldLeft? How about the other way around?
---
At each iteration, we create a function that remembers the current list
element and awaits a value of type B before producing a result.
Upon receiving such a value, we apply f to produce a value of type B,
which is then fed to the function from the previous step.
The output from foldLeft is a function which is fed the zero value,
at which point the function chain is evaluated in reverse.
Note that this implementation is not stack safe due to the creation
of an anonymous function at each step.
For a stack safe implementation, we can reverse the list and then
apply foldLeft.
*/
def foldRight[A, B](as: List[A], z: B, f: (A, B) => B): B = foldLeft(
as,
(b: B) => b,
(acc, a) => (b: B) => acc(f(a, b))
)(z)

/*
Exercise 3.14: Implement append in terms of either foldLeft or foldRight.
*/
def append[A](a1: List[A], a2: List[A]): List[A] =
foldRight(a1, a2, Cons(_, _))

/*
Exercise 3.15: Write a function that concatenates a list of lists into a
single list. Its runtime should be linear in the total length of all lists.
*/
def concat[A](xxs: List[List[A]]): List[A] =
foldRight(xxs, List[A](), append)

/*
Exercise 3.16: Write a function that transforms a list of integers by adding
1 to each element.
*/
def add1(xs: List[Int]): List[Int] =
foldRight(xs, List[Int](), (x, acc) => Cons(x + 1, acc))

/*
Exercise 3.17: Write a function that turns each value in a List[Double] into
a String.
*/
def doubleToString(xs: List[Double]): List[String] =
foldRight(xs, List[String](), (x, acc) => Cons(f"$x%2.2f", acc))

/*
Exercise 3.18: Write a function, map, that generalizes modifying each element
in a list while maintaining the structure of the list.
*/
def map[A, B](as: List[A], f: A => B): List[B] =
foldRight(as, List[B](), (x, acc) => Cons(f(x), acc))

/*
Exercise 3.19: Write a function, filter, that removes elements from a list
unless they satisfy a given predicate.
*/
// def filter[A](as: List[A], f: A => Boolean): List[A] =
// foldRight(as, List[A](), (x, acc) => if f(x) then Cons(x, acc) else acc)

/*
Exercise 3.20: Write a function, flatMap, that works like map except that
the function given will return a list instead of a single result, ensuring
that the list is inserted into the final resulting list.
*/
def flatMap[A, B](as: List[A], f: A => List[B]): List[B] =
foldRight(as, List[B](), (x, acc) => append(f(x), acc))

/*
Exercise 3.21: Use flatMap to implement filter.
*/
def filter[A](as: List[A], f: A => Boolean): List[A] =
flatMap(as, x => if f(x) then List(x) else List())

/*
Exercise 3.22: Write a function that accepts two lists and constructs a new
list by adding corresponding elements.
Exercise 3.23: Generalize the function you just wrote so it's not specific
to integers or addition.
*/
// Not stack safe!
def zipWith[A, B, C](xs: List[A], ys: List[B], f: (A, B) => C): List[C] = (xs, ys) match
case (Nil, _) => Nil
case (_, Nil) => Nil
case (Cons(x, xxs), Cons(y, yys)) => Cons(f(x, y), zipWith(xxs, yys, f))

/*
Exercise 3.24: Implement hasSubsequence to check whether a List contains
another List as a sunsequence.
*/
// 1. Does subsequence mean potentially not consecutive elements? No.
// 2. Check at every position.
def hasSubsequence[A](sup: List[A], sub: List[A]): Boolean = ???
36 changes: 36 additions & 0 deletions chapter03/src/Tree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
enum Tree[+A]:
case Leaf(value: A)
case Branch(left: Tree[A], right: Tree[A])

def size: Int =
fold(_ => 1, 1 + _ + _)
/*
Exercise 3.26: Write a function, depth, that returns the maximum
path length from the root to any leaf.
*/
def depth: Int =
fold(_ => 0, (d1, d2) => 1 + (d1 max d2))

/*
Exercise 3.27: Write a function, map, analogous to the method of
the same name on List that modifies each element in a tree with
a given function.
*/
def map[B](f: A => B): Tree[B] =
fold(a => Leaf(f(a)), Branch(_, _))

/*
Exercise 3.28: Generalize size, maximum, depth, and map, writing
a new function, fold, that abstracts over their similarities.
*/
def fold[B](f: A => B, g: (B, B) => B): B = this match
case Leaf(a) => f(a)
case Branch(l, r) => g(l.fold(f, g), r.fold(f, g))

object Tree:
/*
Exercise 3.25: Write a function, maximum, that returns the
maximum element in a Tree[Int].
*/
def maximum(t: Tree[Int]): Int =
t.fold(x => x, (x, y) => x.max(y))
8 changes: 8 additions & 0 deletions chapter03/test/src/LibSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import org.scalatest.funspec.AnyFunSpec
import Lib.*
import org.scalatest.matchers.should.Matchers.shouldBe

class LibSpec extends AnyFunSpec:
describe("Chapter 3"):
it("list pattern match should add first two values"):
result shouldBe 3
Loading

0 comments on commit 80eab21

Please sign in to comment.