diff --git a/.github/run.sh b/.github/run.sh
index 9f32449..3e09d8c 100755
--- a/.github/run.sh
+++ b/.github/run.sh
@@ -45,8 +45,8 @@ fi
 
 if (( no_lint == 0 )); then
 	if [[ -z "${CI}" ]]; then
-	  ./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll modules[_].sources
+	  ./mill mill.scalalib.scalafmt.ScalafmtModule/reformatAll modules[_].__.sources
 	else
-		./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll modules[_].sources
+		./mill mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll modules[_].__.sources
 	fi
 fi
diff --git a/.mill-version b/.mill-version
index aa22d3c..e01e0dd 100644
--- a/.mill-version
+++ b/.mill-version
@@ -1 +1 @@
-0.12.3
+0.12.4
diff --git a/README.md b/README.md
index 180620a..848339e 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@ The older code is available in branches.
 7. [Monoids and Semigroups](ch07)
 8. [Functors](ch08)
 9. [Monads](ch09)
+10. [Monad Transformers](ch10)
+11. [Semigroupal and Applicative](ch11)
+12. [Foldable and Traverse](ch12)
 
 ## Running tests
 ```
diff --git a/build.mill b/build.mill
index 30b443a..aea893f 100644
--- a/build.mill
+++ b/build.mill
@@ -38,11 +38,9 @@ trait CatsModule extends ScalaModule with Cross.Module[String] with ScalafmtModu
   )
 
   object test extends ScalaTests with TestModule.ScalaTest {
-   val commonDeps = Seq(
+   def ivyDeps = Agg(
      ivy"org.scalatest::scalatest:${v.scalatestVersion}",
      ivy"org.scalatestplus::scalacheck-1-18:${v.scalacheckVersion}"
    )
-   
-   def ivyDeps = Task{commonDeps}
   }
 }
diff --git a/ch02/test/src/MyListSpec.scala b/ch02/test/src/MyListSpec.scala
index 798e34a..6675e41 100644
--- a/ch02/test/src/MyListSpec.scala
+++ b/ch02/test/src/MyListSpec.scala
@@ -30,10 +30,3 @@ class MyListSpec extends AnyFunSpec:
     it("evens"):
       val actual = MyList.iterate(0, 5)(_ + 1).map(_ * 2)
       actual.toSeq shouldBe (0 to 8 by 2)
-
-
-
-
-
-
-
diff --git a/ch03/test/src/BoolSpec.scala b/ch03/test/src/BoolSpec.scala
index eada01e..eb35f72 100644
--- a/ch03/test/src/BoolSpec.scala
+++ b/ch03/test/src/BoolSpec.scala
@@ -20,4 +20,3 @@ class BoolSpec extends AnyFunSpec:
     it("not"):
       not(True).`if`("yes")("no") shouldBe "no"
       not(False).`if`("yes")("no") shouldBe "yes"
-
diff --git a/ch03/test/src/CodataSpec.scala b/ch03/test/src/CodataSpec.scala
index ec88cb4..73f2265 100644
--- a/ch03/test/src/CodataSpec.scala
+++ b/ch03/test/src/CodataSpec.scala
@@ -11,4 +11,3 @@ class CodataSpec extends AnyFunSpec:
 
       val product = list()(1, (a, b) => a * b)
       product shouldBe 6
-
diff --git a/ch03/test/src/SetSpec.scala b/ch03/test/src/SetSpec.scala
index 23034bc..6ca901e 100644
--- a/ch03/test/src/SetSpec.scala
+++ b/ch03/test/src/SetSpec.scala
@@ -11,9 +11,8 @@ class SetSpec extends AnyFunSpec:
         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
-
+      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
diff --git a/ch03/test/src/StreamSpec.scala b/ch03/test/src/StreamSpec.scala
index 4b2e0b5..8738d7f 100644
--- a/ch03/test/src/StreamSpec.scala
+++ b/ch03/test/src/StreamSpec.scala
@@ -18,6 +18,3 @@ class StreamSpec extends AnyFunSpec:
       Stream.naturals.take(5) shouldBe (1 to 5)
       Stream.naturals2.take(5) shouldBe (1 to 5)
       Stream.naturals3.take(5) shouldBe (1 to 5)
-
-
-
diff --git a/ch05/test/src/ExpressionCSpec.scala b/ch05/test/src/ExpressionCSpec.scala
index 662dd56..5461d10 100644
--- a/ch05/test/src/ExpressionCSpec.scala
+++ b/ch05/test/src/ExpressionCSpec.scala
@@ -8,4 +8,3 @@ class ExpressionCSpec extends AnyFunSpec:
     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
index 618d285..4e99d53 100644
--- a/ch05/test/src/ExpressionSpec.scala
+++ b/ch05/test/src/ExpressionSpec.scala
@@ -8,4 +8,3 @@ class ExpressionSpec extends AnyFunSpec:
     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
index b9c1d3b..6e52bc7 100644
--- a/ch05/test/src/ExpressionTSpec.scala
+++ b/ch05/test/src/ExpressionTSpec.scala
@@ -8,4 +8,3 @@ class ExpressionTSpec extends AnyFunSpec:
     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
index b2fd628..7b73d2f 100644
--- a/ch05/test/src/RegexpCSpec.scala
+++ b/ch05/test/src/RegexpCSpec.scala
@@ -9,17 +9,16 @@ class RegexpCSpec extends AnyFunSpec:
     it("matches"):
       val txts =
         Table(
-            ("txt", "match"),
-            ("Scala", true),
-            ("Scalalalala", true),
-            ("Sca", false),
-            ("Scalal", false),
-            ("Scalaland", false)
+          ("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) =>
+      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
index 64f30bd..2dea72c 100644
--- a/ch05/test/src/RegexpSpec.scala
+++ b/ch05/test/src/RegexpSpec.scala
@@ -9,17 +9,16 @@ class RegexpSpec extends AnyFunSpec:
     it("matches"):
       val txts =
         Table(
-            ("txt", "match"),
-            ("Scala", true),
-            ("Scalalalala", true),
-            ("Sca", false),
-            ("Scalal", false),
-            ("Scalaland", false)
+          ("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) =>
+      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
index 9096b06..e8b9034 100644
--- a/ch05/test/src/RegexpTSpec.scala
+++ b/ch05/test/src/RegexpTSpec.scala
@@ -7,4 +7,3 @@ class RegexpTSpec extends AnyFunSpec:
   describe("RegexpT"):
     it("matches"):
       RegexpT("a").repeat.matches("a" * 20000) shouldBe true
-
diff --git a/ch06/test/src/CatSpec.scala b/ch06/test/src/CatSpec.scala
index 29eacf1..6de6896 100644
--- a/ch06/test/src/CatSpec.scala
+++ b/ch06/test/src/CatSpec.scala
@@ -6,19 +6,19 @@ import cats.syntax.show.toShow
 import cats.syntax.eq.catsSyntaxEq
 
 class CatSpec extends AnyFunSpec:
-    describe("Cat"):
-        it("Show"):
-            Cat("Garfield", 41, "ginger and black").show shouldBe "Garfield is a 41 year-old ginger and black cat."
+  describe("Cat"):
+    it("Show"):
+      Cat("Garfield", 41, "ginger and black").show shouldBe "Garfield is a 41 year-old ginger and black cat."
 
-        it("Eq"):
-            val cat1 = Cat("Garfield", 38, "orange and black")
-            val cat2 = Cat("Heathcliff", 32, "orange and black")
+    it("Eq"):
+      val cat1 = Cat("Garfield", 38, "orange and black")
+      val cat2 = Cat("Heathcliff", 32, "orange and black")
 
-            cat1 === cat2 shouldBe false
-            cat1 =!= cat2 shouldBe true
+      cat1 === cat2 shouldBe false
+      cat1 =!= cat2 shouldBe true
 
-            val optionCat1 = Option(cat1)
-            val optionCat2 = Option.empty[Cat]
+      val optionCat1 = Option(cat1)
+      val optionCat2 = Option.empty[Cat]
 
-            optionCat1 === optionCat2 shouldBe false
-            optionCat1 =!= optionCat2 shouldBe true
\ No newline at end of file
+      optionCat1 === optionCat2 shouldBe false
+      optionCat1 =!= optionCat2 shouldBe true
diff --git a/ch07/test/src/LibSpec.scala b/ch07/test/src/LibSpec.scala
index 2c97eb4..3a12759 100644
--- a/ch07/test/src/LibSpec.scala
+++ b/ch07/test/src/LibSpec.scala
@@ -19,4 +19,4 @@ class LibSpec extends AnyFunSpec:
 
     it("should add options"):
       val opts = List(Option(22), Option(20))
-      add(opts) shouldBe Option(42)
\ No newline at end of file
+      add(opts) shouldBe Option(42)
diff --git a/ch08/test/src/TreeSpec.scala b/ch08/test/src/TreeSpec.scala
index 3c4e964..bc73a7a 100644
--- a/ch08/test/src/TreeSpec.scala
+++ b/ch08/test/src/TreeSpec.scala
@@ -1,7 +1,7 @@
 package ch08
 import org.scalatest.funspec.AnyFunSpec
 import org.scalatest.matchers.should.Matchers.shouldBe
-import cats.syntax.functor.toFunctorOps  // map
+import cats.syntax.functor.toFunctorOps // map
 
 class TreeSpec extends AnyFunSpec:
   describe("Tree Functor"):
@@ -11,4 +11,4 @@ class TreeSpec extends AnyFunSpec:
 
     it("should map on branch"):
       val actual = Tree.branch(Tree.leaf(10), Tree.leaf(20)).map(_ * 2)
-      actual shouldBe Tree.branch(Tree.leaf(20), Tree.leaf(40))
\ No newline at end of file
+      actual shouldBe Tree.branch(Tree.leaf(20), Tree.leaf(40))
diff --git a/ch09/test/src/LibSpec.scala b/ch09/test/src/LibSpec.scala
index 3216d3d..ed729ea 100644
--- a/ch09/test/src/LibSpec.scala
+++ b/ch09/test/src/LibSpec.scala
@@ -11,12 +11,12 @@ import scala.concurrent.{Await, Future}
 
 class LibSpec extends AnyFunSpec:
   describe("MonadError"):
-    it("validateAdult"):    
-        Lib.validateAdult[Try](18).success.value shouldBe 18
-        Lib.validateAdult[Try](8).failure.exception shouldBe an[IllegalArgumentException]
-        type ExceptionOr[A] = Either[Throwable, A]
-        Lib.validateAdult[ExceptionOr](-1).left.value shouldBe an[IllegalArgumentException]
-        
+    it("validateAdult"):
+      Lib.validateAdult[Try](18).success.value shouldBe 18
+      Lib.validateAdult[Try](8).failure.exception shouldBe an[IllegalArgumentException]
+      type ExceptionOr[A] = Either[Throwable, A]
+      Lib.validateAdult[ExceptionOr](-1).left.value shouldBe an[IllegalArgumentException]
+
   describe("Writer"):
     it("factorial should maintain the order of logging"):
       val computations = Future.sequence(
diff --git a/ch09/test/src/TreeSpec.scala b/ch09/test/src/TreeSpec.scala
index 900751b..dd7cf4f 100644
--- a/ch09/test/src/TreeSpec.scala
+++ b/ch09/test/src/TreeSpec.scala
@@ -2,14 +2,15 @@ package ch09
 
 import org.scalatest.funspec.AnyFunSpec
 import org.scalatest.matchers.should.Matchers.shouldBe
-import cats.syntax.flatMap.toFlatMapOps  // flatMap
-import cats.syntax.functor.toFunctorOps  // map
+import cats.syntax.flatMap.toFlatMapOps // flatMap
+import cats.syntax.functor.toFunctorOps // map
 import Tree.given
 
 class TreeSpec extends AnyFunSpec:
   describe("Tree monad"):
     it("should support flatMap, map, and for-comprehension"):
-      val actual = Tree.branch(Tree.leaf(100), Tree.leaf(200))
+      val actual = Tree
+        .branch(Tree.leaf(100), Tree.leaf(200))
         .flatMap(x => Tree.branch(Tree.leaf(x - 1), Tree.leaf(x + 1)))
       val expected = Tree.branch(
         Tree.branch(Tree.leaf(99), Tree.leaf(101)),
@@ -18,9 +19,9 @@ class TreeSpec extends AnyFunSpec:
       actual shouldBe expected
 
       val actual2 = for
-        a <- Tree.branch(Tree.leaf(100), Tree.leaf(200))  // flatMap
-        b <- Tree.branch(Tree.leaf(a - 10), Tree.leaf(a + 10))  // flatMap
-        c <- Tree.branch(Tree.leaf(b - 1), Tree.leaf(b + 1))  // map
+        a <- Tree.branch(Tree.leaf(100), Tree.leaf(200))       // flatMap
+        b <- Tree.branch(Tree.leaf(a - 10), Tree.leaf(a + 10)) // flatMap
+        c <- Tree.branch(Tree.leaf(b - 1), Tree.leaf(b + 1))   // map
       yield c
       val expected2 = Tree.branch(
         Tree.branch(
diff --git a/ch10/src/Lib.scala b/ch10/src/Lib.scala
new file mode 100644
index 0000000..b115b44
--- /dev/null
+++ b/ch10/src/Lib.scala
@@ -0,0 +1,63 @@
+package ch10
+
+import scala.concurrent.Future
+import cats.data.EitherT
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Await
+import scala.concurrent.duration.*
+
+/*
+10.4 Exercise: Monads: Transform and Roll Out
+
+The Autobots, well-known robots in disguise, frequently send messages
+during battle requesting the power levels of their team mates.
+This helps them coordinate strategies and launch devastating attacks.
+
+Transmissions take time in Earth’s viscous atmosphere, and messages are
+occasionally lost due to satellite malfunction or sabotage by pesky Decepticons8.
+
+Optimus Prime is getting tired of the nested for comprehensions in his neural matrix.
+Help him by rewriting Response using a monad transformer.
+ */
+object Lib:
+
+  type Response[A] = EitherT[Future, String, A]
+
+  val powerLevels = Map(
+    "Jazz"      -> 6,
+    "Bumblebee" -> 8,
+    "Hot Rod"   -> 10
+  )
+
+  /*
+  Implement getPowerLevel to retrieve data from a set of imaginary allies.
+  If an Autobot isn’t in the powerLevels map, return an error message reporting
+  that they were unreachable. Include the name in the message for good effect.
+   */
+  def getPowerLevel(ally: String): Response[Int] =
+    powerLevels.get(ally) match
+      case Some(avg) => EitherT.right(Future(avg))
+      case None      => EitherT.left(Future(s"$ally unreachable"))
+
+  /*
+  Two autobots can perform a special move if their combined power level is greater than 15.
+  If either ally is unavailable, fail with an appropriate error message.
+   */
+  def canSpecialMove(ally1: String, ally2: String): Response[Boolean] =
+    for
+      lvl1 <- getPowerLevel(ally1)
+      lvl2 <- getPowerLevel(ally2)
+    yield (lvl1 + lvl2) > 15
+
+  /*
+  Write a method tacticalReport that takes two ally names and prints
+  a message saying whether they can perform a special move.
+   */
+  def tacticalReport(ally1: String, ally2: String): String =
+    val stack: Future[Either[String, Boolean]] =
+      canSpecialMove(ally1, ally2).value
+
+    Await.result(stack, 1.second) match
+      case Left(msg)    => s"Comms error: $msg"
+      case Right(true)  => s"$ally1 and $ally2 are ready to roll out!"
+      case Right(false) => s"$ally1 and $ally2 need a recharge."
diff --git a/ch10/src/ch10.worksheet.sc b/ch10/src/ch10.worksheet.sc
new file mode 100644
index 0000000..f1c15fe
--- /dev/null
+++ b/ch10/src/ch10.worksheet.sc
@@ -0,0 +1,75 @@
+import cats.data.{EitherT, OptionT, Writer}
+import cats.syntax.applicative.catsSyntaxApplicativeId // pure
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration.*
+import scala.concurrent.{Await, Future}
+
+// 10.2 A Transformative Example
+/*
+we build ListOption from the inside out: we pass List, the type of the outer monad,
+as a parameter to OptionT, the transformer for the inner monad.
+ */
+type ListOption[A] = OptionT[List, A]
+
+// We can create instances of ListOption using the OptionT constructor, or more conveniently using pure
+val result1: ListOption[Int] = OptionT(List(Option(10)))
+val result2: ListOption[Int] = 32.pure[ListOption]
+
+result1.flatMap { (x: Int) =>
+  result2.map { (y: Int) =>
+    x + y
+  }
+}
+
+// 10.3.2 Building Monad Stacks
+
+// Alias Either to a type constructor with one parameter
+type ErrorOr[A] = Either[String, A]
+
+// Build our final monad stack using OptionT
+type ErrorOrOption[A] = OptionT[ErrorOr, A]
+
+val a = 10.pure[ErrorOrOption]
+val b = 32.pure[ErrorOrOption]
+
+val c = a.flatMap(x => b.map(y => x + y))
+
+type FutureEither[A] = EitherT[Future, String, A]
+
+type FutureEitherOption[A] = OptionT[FutureEither, A]
+
+val futureEitherOr: FutureEitherOption[Int] =
+  for
+    a <- 10.pure[FutureEitherOption]
+    b <- 32.pure[FutureEitherOption]
+  yield a + b
+
+// 10.3.3 Constructing and Unpacking Instances
+val intermediate = futureEitherOr.value
+
+val stack = intermediate.value
+
+Await.result(stack, 1.second)
+
+// 10.3.5 Usage Patterns
+type Logged[A] = Writer[List[String], A]
+
+// Methods generally return untransformed stacks
+def parseNumber(str: String): Logged[Option[Int]] =
+  util.Try(str.toInt).toOption match
+    case Some(num) => Writer(List(s"Read $str"), Some(num))
+    case None      => Writer(List(s"Failed on $str"), None)
+
+// Consumers use monad transformers locally to simplify composition
+def addAll(a: String, b: String, c: String): Logged[Option[Int]] =
+  val result = for
+    a <- OptionT(parseNumber(a))
+    b <- OptionT(parseNumber(b))
+    c <- OptionT(parseNumber(c))
+  yield a + b + c
+
+  result.value
+
+// This approach doesn't force OptionT on other users' code:
+addAll("1", "2", "3")
+addAll("1", "a", "3")
diff --git a/ch10/test/src/Lib.scala b/ch10/test/src/Lib.scala
new file mode 100644
index 0000000..79392d1
--- /dev/null
+++ b/ch10/test/src/Lib.scala
@@ -0,0 +1,16 @@
+package ch10
+
+import org.scalatest.funspec.AnyFunSpec
+import org.scalatest.matchers.should.Matchers.shouldBe
+
+class LibSpec extends AnyFunSpec:
+  describe("Autobots"):
+    it("should generate a tactitcal report"):
+      val report1 = Lib.tacticalReport("Jazz", "Bumblebee")
+      report1 shouldBe "Jazz and Bumblebee need a recharge."
+
+      val report2 = Lib.tacticalReport("Bumblebee", "Hot Rod")
+      report2 shouldBe "Bumblebee and Hot Rod are ready to roll out!"
+
+      val report3 = Lib.tacticalReport("Jazz", "Ironhide")
+      report3 shouldBe "Comms error: Ironhide unreachable"
diff --git a/ch11/src/ch11.worksheet.sc b/ch11/src/ch11.worksheet.sc
new file mode 100644
index 0000000..a2ee4f6
--- /dev/null
+++ b/ch11/src/ch11.worksheet.sc
@@ -0,0 +1,59 @@
+import cats.Semigroupal
+import cats.syntax.apply.*    // tupled and mapN
+import cats.syntax.parallel.* // parTupled
+
+// 11.1.1 Joining Two Contexts
+/*
+While Semigroup allows us to join values, Semigroupal allows us to join contexts.
+If either parameter evaluates to None, the entire result is None.
+ */
+Semigroupal[Option].product(Some(123), Some("abc"))
+
+// 11.1.2 Joining Three or More Contexts
+Semigroupal.tuple3(Option(1), Option(2), Option.empty[Int])
+
+// 11.2 Apply Syntax
+(Option(123), Option("abc")).tupled
+
+final case class Cat(name: String, born: Int, color: String)
+
+(
+  Option("Garfield"),
+  Option(1978),
+  Option("Orange & black")
+).mapN(Cat.apply)
+
+/*
+11.3.1.1 Exercise: The Product of Lists
+Why does product for List produce the Cartesian product?
+ */
+(List(1, 2), List(3, 4)).tupled
+
+// def product[F[_]: Monad, A, B](x: F[A], y: F[B]): F[(A, B)] =
+//   x.flatMap(a => y.map(b => (a, b)))
+
+/*
+^^^This code is equivalent to a for comprehension.
+The semantics of flatMap are what give rise to the behaviour for List and Either.
+ */
+
+// 11.4 Parallel
+type ErrorOr[A] = Either[Vector[String], A]
+val error1: ErrorOr[Int] = Left(Vector("Error 1"))
+val error2: ErrorOr[Int] = Left(Vector("Error 2"))
+Semigroupal[ErrorOr].product(error1, error2)
+
+(error1, error2).tupled
+
+// To collect all the errors we simply replace tupled with its "parallel" version called parTupled.
+(error1, error2).parTupled
+
+/*
+11.4.0.1 Exercise: Parallel List
+Does List have a Parallel instance? If so, what does the Parallel instance do?
+ */
+
+// List does have a Parallel instance, and it zips the List insted of creating the cartesian product.
+(List(1, 2), List(3, 4)).parTupled
+
+// Semigroupal and Applicative effectively provide alternative encodings of the same notion of joining contexts.
diff --git a/ch12/src/ch12.worksheet.sc b/ch12/src/ch12.worksheet.sc
new file mode 100644
index 0000000..2fd06dc
--- /dev/null
+++ b/ch12/src/ch12.worksheet.sc
@@ -0,0 +1,133 @@
+import cats.syntax.applicative.catsSyntaxApplicativeId
+import cats.{Applicative, Traverse}
+import scala.concurrent.{Await, Future}
+import cats.data.Validated
+import cats.syntax.apply.* // mapN
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration.*
+import cats.syntax.traverse.toTraverseOps // traverse, sequence
+
+/*
+12.1.2 Exercise: Reflecting on Folds
+
+Try using foldLeft and foldRight with an empty list
+as the accumulator and :: as the binary operator.
+
+What results do you get in each case?
+ */
+
+// Folding from left to right reverses the list.
+List(1, 2, 3).foldLeft(List.empty[Int])((a, i) => i :: a)
+
+// Folding right to left copies the list, leaving the order intact.
+List(1, 2, 3).foldRight(List.empty[Int])((i, a) => i :: a)
+
+/*
+12.1.3 Exercise: Scaf-fold-ing Other Methods
+
+Implement substitutes for List's map, flatMap, filter, and sum methods in terms of foldRight.
+ */
+def map[A, B](list: List[A])(f: A => B): List[B] =
+  list.foldRight(List.empty[B])((x, acc) => f(x) :: acc)
+
+def flatMap[A, B](list: List[A])(f: A => List[B]): List[B] =
+  // ::: == ++
+  list.foldRight(List.empty[B])((x, acc) => f(x) ::: acc)
+
+def filter[A](list: List[A])(f: A => Boolean): List[A] =
+  list.foldRight(List.empty[A])((x, acc) => if f(x) then x :: acc else acc)
+
+def listTraverse[F[_]: Applicative, A, B](list: List[A])(func: A => F[B]): F[List[B]] =
+  list.foldLeft(List.empty[B].pure[F]) { (accum, item) =>
+    (accum, func(item)).mapN(_ :+ _)
+  }
+
+def listSequence[F[_]: Applicative, B](list: List[F[B]]): F[List[B]] =
+  listTraverse(list)(identity)
+/*
+12.2.2.1 Exercise: Traversing with Vectors
+
+What is the result of the following?
+
+The argument is of type List[Vector[Int]], so we're using the Applicative
+for Vector and the return type is going to be Vector[List[Int]].
+Vector is a monad, so its semigroupal combine function is based on flatMap.
+We end up with a cross-product.
+ */
+listSequence(List(Vector(1, 2), Vector(3, 4)))
+
+/*
+12.2.2.2 Exercise: Traversing with Options
+
+What is the return type of this method? What does it produce for the following inputs?
+ */
+def process(inputs: List[Int]) =
+  listTraverse(inputs)(n => if (n % 2 == 0) Some(n) else None)
+
+/*
+The arguments to listTraverse are of types List[Int] and Int => Option[Int],
+so, the return type is Option[List[Int]]. Again, Option is a monad, so, the
+semigroupal combine function follows from flatMap. The semantics are, therefore,
+fail-fast error handling: if all inputs are even, we get a list of outputs.
+Otherwise we get None.
+ */
+process(List(2, 4, 6))
+process(List(1, 2, 3))
+
+/*
+12.2.2.3 Exercise: Traversing with Validated
+
+What does this method produce for the following inputs?
+ */
+type ErrorsOr[A] = Validated[List[String], A]
+
+def process2(inputs: List[Int]): ErrorsOr[List[Int]] =
+  listTraverse(inputs) { n =>
+    if (n % 2 == 0)
+    then Validated.valid(n)
+    else Validated.invalid(List(s"$n is not even"))
+  }
+
+/*
+The return type here is ErrorsOr[List[Int]], which expands to Validated[List[String], List[Int]].
+The semantics for semigroupal combine on validated are accumulating error handling, so, the
+result is either a list of even Ints, or a list of errors detailing which Ints failed the test.
+ */
+process2(List(2, 4, 6))
+process2(List(1, 2, 3))
+
+// 12.2.3 Traverse in Cats
+val hostnames = List(
+  "alpha.example.com",
+  "beta.example.com",
+  "gamma.demo.com"
+)
+
+def getUptime(hostname: String): Future[Int] =
+  Future(hostname.length * 60)
+
+val totalUptime: Future[List[Int]] =
+  Traverse[List].traverse(hostnames)(getUptime)
+
+Await.result(totalUptime, 1.second)
+
+val numbers = List(Future(1), Future(2), Future(3))
+
+val numbers2: Future[List[Int]] =
+  Traverse[List].sequence(numbers)
+
+Await.result(numbers2, 1.second)
+
+Await.result(hostnames.traverse(getUptime), 1.second)
+
+Await.result(numbers.sequence, 1.second)
+
+/*
+Foldable abstracts the foldLeft and foldRight methods we know from collections in the standard library.
+It adds stack-safe implementations of these methods to a handful of extra data types, and defines a
+host of situationally useful additions. That said, Foldable doesn't introduce much that we didn't already know.
+
+The real power comes from Traverse, which abstracts and generalises the traverse and sequence methods
+we know from Future. Using these methods we can turn an F[G[A]] into a G[F[A]] for any F with an
+instance of Traverse and any G with an instance of Applicative.
+ */
diff --git a/mill b/mill
index 81ca72e..44386d6 100755
--- a/mill
+++ b/mill
@@ -1,51 +1,222 @@
 #!/usr/bin/env sh
 
 # This is a wrapper script, that automatically download mill from GitHub release pages
-# You can give the required mill version with MILL_VERSION env variable
+# You can give the required mill version with --mill-version parameter
 # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION
+#
+# Original Project page: https://github.com/lefou/millw
+# Script Version: 0.4.12
+#
+# If you want to improve this script, please also contribute your changes back!
+#
+# Licensed under the Apache License, Version 2.0
 
 set -e
 
 if [ -z "${DEFAULT_MILL_VERSION}" ] ; then
-  DEFAULT_MILL_VERSION=0.11.11
+  DEFAULT_MILL_VERSION=0.12.4
 fi
 
-if [ -z "$MILL_VERSION" ] ; then
+
+if [ -z "${GITHUB_RELEASE_CDN}" ] ; then
+  GITHUB_RELEASE_CDN=""
+fi
+
+
+MILL_REPO_URL="https://github.com/com-lihaoyi/mill"
+
+if [ -z "${CURL_CMD}" ] ; then
+  CURL_CMD=curl
+fi
+
+# Explicit commandline argument takes precedence over all other methods
+if [ "$1" = "--mill-version" ] ; then
+  shift
+  if [ "x$1" != "x" ] ; then
+    MILL_VERSION="$1"
+    shift
+  else
+    echo "You specified --mill-version without a version." 1>&2
+    echo "Please provide a version that matches one provided on" 1>&2
+    echo "${MILL_REPO_URL}/releases" 1>&2
+    false
+  fi
+fi
+
+# Please note, that if a MILL_VERSION is already set in the environment,
+# We reuse it's value and skip searching for a value.
+
+# If not already set, read .mill-version file
+if [ -z "${MILL_VERSION}" ] ; then
   if [ -f ".mill-version" ] ; then
-    MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)"
+    MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)"
   elif [ -f ".config/mill-version" ] ; then
-    MILL_VERSION="$(head -n 1 .config/mill-version 2> /dev/null)"
-  elif [ -f "mill" ] && [ "$0" != "mill" ] ; then
-    MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2)
-  else
-    MILL_VERSION=$DEFAULT_MILL_VERSION
+    MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)"
   fi
 fi
 
-if [ "x${XDG_CACHE_HOME}" != "x" ] ; then
-  MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download"
-else
-  MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download"
+MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill"
+
+if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then
+  MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download"
+fi
+
+# If not already set, try to fetch newest from Github
+if [ -z "${MILL_VERSION}" ] ; then
+  # TODO: try to load latest version from release page
+  echo "No mill version specified." 1>&2
+  echo "You should provide a version via '.mill-version' file or --mill-version option." 1>&2
+
+  mkdir -p "${MILL_DOWNLOAD_PATH}"
+  LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || (
+    # we might be on OSX or BSD which don't have -d option for touch
+    # but probably a -A [-][[hh]mm]SS
+    touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest"
+  ) || (
+    # in case we still failed, we retry the first touch command with the intention
+    # to show the (previously suppressed) error message
+    LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest"
+  )
+
+  # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993
+  # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then
+  if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then
+    # we know a current latest version
+    MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null)
+  fi
+
+  if [ -z "${MILL_VERSION}" ] ; then
+    # we don't know a current latest version
+    echo "Retrieving latest mill version ..." 1>&2
+    LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null  | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest"
+    MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null)
+  fi
+
+  if [ -z "${MILL_VERSION}" ] ; then
+    # Last resort
+    MILL_VERSION="${DEFAULT_MILL_VERSION}"
+    echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2
+  else
+    echo "Using mill version ${MILL_VERSION}" 1>&2
+  fi
 fi
-MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}"
 
-version_remainder="$MILL_VERSION"
-MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
-MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}"
+MILL="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}"
 
-if [ ! -s "$MILL_EXEC_PATH" ] ; then
-  mkdir -p "$MILL_DOWNLOAD_PATH"
-  if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then
-    ASSEMBLY="-assembly"
+try_to_use_system_mill() {
+  if [ "$(uname)" != "Linux" ]; then
+    return 0
+  fi
+
+  MILL_IN_PATH="$(command -v mill || true)"
+
+  if [ -z "${MILL_IN_PATH}" ]; then
+    return 0
+  fi
+
+  SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}")
+  if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then
+	  # MILL_IN_PATH is (very likely) a shell script and not the mill
+	  # executable, ignore it.
+	  return 0
+  fi
+
+  SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}")
+  SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}")
+  SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}")
+
+  if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then
+    mkdir -p "${MILL_USER_CACHE_DIR}"
+  fi
+
+  SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info"
+  if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then
+    parseSystemMillInfo() {
+        LINE_NUMBER="${1}"
+        # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the
+        # variable definition in that line in two halves and return
+        # the value, and finally remove the quotes.
+        sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\
+            cut -d= -f2 |\
+            sed 's/"\(.*\)"/\1/'
+    }
+
+    CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1)
+    CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2)
+    CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3)
+    CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4)
+
+    if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \
+           && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \
+           && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then
+      if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then
+          MILL="${SYSTEM_MILL_PATH}"
+          return 0
+      else
+          return 0
+      fi
+    fi
+  fi
+
+  SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p')
+
+  cat <<EOF > "${SYSTEM_MILL_INFO_FILE}"
+CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}"
+CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}"
+CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}"
+CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}"
+EOF
+
+  if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then
+    MILL="${SYSTEM_MILL_PATH}"
+  fi
+}
+try_to_use_system_mill
+
+# If not already downloaded, download it
+if [ ! -s "${MILL}" ] ; then
+
+  # support old non-XDG download dir
+  MILL_OLD_DOWNLOAD_PATH="${HOME}/.mill/download"
+  OLD_MILL="${MILL_OLD_DOWNLOAD_PATH}/${MILL_VERSION}"
+  if [ -x "${OLD_MILL}" ] ; then
+    MILL="${OLD_MILL}"
+  else
+    case $MILL_VERSION in
+      0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* )
+        DOWNLOAD_SUFFIX=""
+        DOWNLOAD_FROM_MAVEN=0
+        ;;
+      0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* )
+        DOWNLOAD_SUFFIX="-assembly"
+        DOWNLOAD_FROM_MAVEN=0
+        ;;
+      *)
+        DOWNLOAD_SUFFIX="-assembly"
+        DOWNLOAD_FROM_MAVEN=1
+        ;;
+    esac
+
+    DOWNLOAD_FILE=$(mktemp mill.XXXXXX)
+
+    if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then
+      DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist/${MILL_VERSION}/mill-dist-${MILL_VERSION}.jar"
+    else
+      MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/')
+      DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}"
+      unset MILL_VERSION_TAG
+    fi
+
+    # TODO: handle command not found
+    echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2
+    ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}"
+    chmod +x "${DOWNLOAD_FILE}"
+    mkdir -p "${MILL_DOWNLOAD_PATH}"
+    mv "${DOWNLOAD_FILE}" "${MILL}"
+
+    unset DOWNLOAD_FILE
+    unset DOWNLOAD_SUFFIX
   fi
-  DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download
-  MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/')
-  MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist/$MILL_VERSION/mill-dist-$MILL_VERSION.jar"
-  curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL"
-  chmod +x "$DOWNLOAD_FILE"
-  mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH"
-  unset DOWNLOAD_FILE
-  unset MILL_DOWNLOAD_URL
 fi
 
 if [ -z "$MILL_MAIN_CLI" ] ; then
@@ -53,15 +224,18 @@ if [ -z "$MILL_MAIN_CLI" ] ; then
 fi
 
 MILL_FIRST_ARG=""
-
- # first arg is a long flag for "--interactive" or starts with "-i"
-if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then
+if [ "$1" = "--bsp" ] || [ "$1" = "-i" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then
   # Need to preserve the first position of those listed options
   MILL_FIRST_ARG=$1
   shift
 fi
 
 unset MILL_DOWNLOAD_PATH
+unset MILL_OLD_DOWNLOAD_PATH
+unset OLD_MILL
 unset MILL_VERSION
+unset MILL_REPO_URL
 
-exec $MILL_EXEC_PATH $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@"
+# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes
+# shellcheck disable=SC2086
+exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@"
\ No newline at end of file