diff --git a/README.md b/README.md index 0efc9867441063c6dcff25492099004967eb3c4f..4484cf83d29013280d08cc6155ffff1d4c307a6b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ usually marked with `???`. | 6: Typeclasses | [`typeclasses`](src/main/scala/typeclasses/) | `testOnly typeclasses.*` (needs some uncommenting) | 7: Monads | [`monads`](src/main/scala/monads/) | `testOnly monads.*` (needs some uncommenting) | 8: Applicative Functors | [`applicative`](src/main/scala/applicative/) | `testOnly applicative.*` (needs some uncommenting) +| 8: Algebraic View on more Monads| [`readerwriter`](src/main/scala/readerwriter/) | `testOnly readerwriter.*` ## Usage tips: To keep your local solutions to the exercises when pulling from the repository, diff --git a/src/main/scala/readerwriter/Randoms.scala b/src/main/scala/readerwriter/Randoms.scala new file mode 100644 index 0000000000000000000000000000000000000000..04c7a0ddb3229e76fb8febd6ed2a95b188dbd7f4 --- /dev/null +++ b/src/main/scala/readerwriter/Randoms.scala @@ -0,0 +1,34 @@ +package readerwriter + +import readerwriter.internal.State, State._ +import util._ + +object Randoms { + def threeInts: State[RNG, (Int, Int, Int)] = ??? + + /*@formatter:off*/ + def randomInt: State[RNG, Int] = for { + rng <- get[RNG] + (rng2, i) = rng.nextInt + _ <- set(rng2) + } yield i + /*@formatter:on*/ + + + def nonNegativeInt: State[RNG, Int] = for { + i <- randomInt + } yield if (i < 0) -(i + 1) else i +} + +trait RNG { + def nextInt: (RNG, Int) +} + +case class Simple(seed: Long) extends RNG { + def nextInt: (RNG, Int) = { + val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL + val nextRNG = Simple(newSeed) + val n = (newSeed >>> 16).toInt + (nextRNG, n) + } +} diff --git a/src/main/scala/readerwriter/Readers.scala b/src/main/scala/readerwriter/Readers.scala new file mode 100644 index 0000000000000000000000000000000000000000..fb66563ec983eca3498bc311058ec078ec8c0117 --- /dev/null +++ b/src/main/scala/readerwriter/Readers.scala @@ -0,0 +1,25 @@ +package readerwriter + +import java.time.LocalDate + +import readerwriter.internal.Reader, Reader._ +import util._ + +final case class Request( + user: Option[String], + locale: String, + route: String, + params: Map[String, List[String]], + now: LocalDate, +) + +object Readers { + /* everything required to use ask and the monad operations + * is already imported */ + + def formatUser: Reader[Request, String] = ??? + + def formatTime: Reader[Request, String] = ??? + + def sayBye: Reader[Request, String] = ??? +} diff --git a/src/main/scala/readerwriter/Writers.scala b/src/main/scala/readerwriter/Writers.scala new file mode 100644 index 0000000000000000000000000000000000000000..cd90b40ae8778c2ae36962a9d1ed2b32678b61b3 --- /dev/null +++ b/src/main/scala/readerwriter/Writers.scala @@ -0,0 +1,39 @@ +package readerwriter + + +import readerwriter.internal.Writer, Writer._ +import util._ + + +object Writers { + /* everything required to use tell and the monad operations + * is already imported */ + + def collatzDepth(n: Int): Writer[List[String], Int] = ??? + + def collatzSearch(start: Int, limit: Int): Writer[List[String], Int] = ??? + +} + +object CollatzWithoutWriter { + def collatzDepth(n: Int): (List[String], Int) = + if (n == 1) + (List("got 1, doing nothing"), 0) + else if (n % 2 == 0) { + val (way, depth) = collatzDepth(n / 2) + (s"got $n, halving" :: way, depth + 1) + } else { + val (way, depth) = collatzDepth(n * 3 + 1) + (s"got $n, tripling plus one" :: way, depth + 1) + } + + def collatzSearch(start: Int, limit: Int): (List[String], Int) = { + val (way, depth) = collatzDepth(start) + if (depth < limit) { + val (way2, number) = collatzSearch(start + 1, limit) + (s"testing $start" :: way ++ (s"depth was $depth" :: way2), number) + } else + (s"testing $start" :: way ++ List(s"depth was $depth", s"returning $start"), start) + } +} + diff --git a/src/main/scala/readerwriter/internal/Reader.scala b/src/main/scala/readerwriter/internal/Reader.scala new file mode 100644 index 0000000000000000000000000000000000000000..ce27edd7aa79a8ecb71ab3f53e5d2ca26db687da --- /dev/null +++ b/src/main/scala/readerwriter/internal/Reader.scala @@ -0,0 +1,23 @@ +package readerwriter.internal + +import applicative.Monad + +case class Reader[R, A](run: R => A) + +object Reader { + def ask[R]: Reader[R, R] = Reader(x => x) + + implicit def readerM[R]: Monad[Reader[R, ?]] = new Monad[Reader[R, ?]] { + override def flatMap[A, B](fa: Reader[R, A])(f: A => Reader[R, B]): Reader[R, B] = Reader(in => { + val a = fa.run(in) + f(a).run(in) + }) + override def unit[A](a: A): Reader[R, A] = Reader(_ => a) + override def map2[A, B, C](fa: Reader[R, A], fb: Reader[R, B])(f: (A, B) => C): Reader[R, C] = Reader(in => { + val a = fa.run(in) + val b = fb.run(in) + f(a, b) + }) + override def map[A, B](fa: Reader[R, A])(f: A => B): Reader[R, B] = Reader(in => f(fa.run(in))) + } +} diff --git a/src/main/scala/readerwriter/internal/State.scala b/src/main/scala/readerwriter/internal/State.scala new file mode 100644 index 0000000000000000000000000000000000000000..2d93948190db057c8f91d26f59e0e7488dd134de --- /dev/null +++ b/src/main/scala/readerwriter/internal/State.scala @@ -0,0 +1,21 @@ +package readerwriter.internal +/* You don't need to edit any of the files inside this package. These are the actual implementations, and you should + * only use the provided methods, not rely on internal structure + */ + +import applicative.Monad + +case class State[S, A] private (run: S => (S, A)) + +case object State { + implicit def stateMonad[S]: Monad[State[S, ?]] = new Monad[State[S, ?]] { + override def unit[A](a: A): State[S, A] = State(s => (s, a)) + override def flatMap[A, B](fa: State[S, A])(f: A => State[S, B]): State[S, B] = State(s => { + val (s1, a) = fa.run(s) + f(a).run(s1) + }) + } + + def get[S]: State[S, S] = State(s => (s, s)) + def set[S](s: S): State[S, Unit] = State(_ => (s, ())) +} diff --git a/src/main/scala/readerwriter/internal/Writer.scala b/src/main/scala/readerwriter/internal/Writer.scala new file mode 100644 index 0000000000000000000000000000000000000000..1c9ccf849d70d162fbf4457a427ec0a37a24a77d --- /dev/null +++ b/src/main/scala/readerwriter/internal/Writer.scala @@ -0,0 +1,27 @@ +package readerwriter.internal + +import algebra.Monoid +import applicative.Monad + +final case class Writer[L, A](v: (L, A)) + +object Writer { + implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] { + def zero: List[Nothing] = List.empty + def op(l1: List[A], l2: List[A]): List[A] = l1 ++ l2 + } + + def tell[L](l: L): Writer[L, Unit] = Writer((l, ())) + + implicit def writerM[L: Monoid]: Monad[Writer[L, ?]] = new Monad[Writer[L, ?]] { + override def flatMap[A, B](fa: Writer[L, A])(f: A => Writer[L, B]): Writer[L, B] = { + val next = f(fa.v._2) + Writer((implicitly[Monoid[L]].op(fa.v._1, next.v._1), next.v._2)) + } + + def unit[A](a: A): Writer[L, A] = Writer((implicitly[Monoid[L]].zero, a)) + override def map2[A, B, C](fa: Writer[L, A], fb: Writer[L, B])(f: (A, B) => C): Writer[L, C] = + Writer((implicitly[Monoid[L]].op(fa.v._1, fb.v._1), f(fa.v._2, fb.v._2))) + override def map[A, B](fa: Writer[L, A])(f: A => B): Writer[L, B] = Writer((fa.v._1, f(fa.v._2))) + } +} diff --git a/src/main/scala/util/package.scala b/src/main/scala/util/package.scala new file mode 100644 index 0000000000000000000000000000000000000000..81f7365163f8ecacac41a0224f197d1b4c263863 --- /dev/null +++ b/src/main/scala/util/package.scala @@ -0,0 +1,10 @@ +import applicative.Monad + +import scala.language.higherKinds + +package object util { + implicit class MonadOps[F[_] : Monad, A](fa: F[A]) { + def flatMap[B](f: A => F[B]): F[B] = implicitly[Monad[F]].flatMap(fa)(f) + def map[B](f: A => B): F[B] = implicitly[Monad[F]].map(fa)(f) + } +} diff --git a/src/test/scala/readerwriter/RandomSpec.scala b/src/test/scala/readerwriter/RandomSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..9cb491670c000b3ad40cd0fdd83f394dd1941917 --- /dev/null +++ b/src/test/scala/readerwriter/RandomSpec.scala @@ -0,0 +1,18 @@ +package readerwriter + +import org.scalatest._ +import testutil.PendingIfUnimplemented + +class RandomSpec extends FlatSpec with Matchers with AppendedClues with PendingIfUnimplemented { + val initial = Simple(192837465L) + val (r1, i1) = initial.nextInt + val (r2, i2) = r1.nextInt + val (r3, i3) = r2.nextInt + + "threeInts" should "have the same result as passing state manually" in { + Randoms.threeInts.run(initial)._2 shouldBe ((i1, i2, i3)) + } + it should "result in the same rng state as passing state manually" in { + Randoms.threeInts.run(initial)._1 shouldBe r3 + } +} diff --git a/src/test/scala/readerwriter/ReaderSpec.scala b/src/test/scala/readerwriter/ReaderSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..b4500ffcbc5f08fe07581f39d8acf1bbae1cff3b --- /dev/null +++ b/src/test/scala/readerwriter/ReaderSpec.scala @@ -0,0 +1,31 @@ +package readerwriter + +import org.scalatest._ +import testutil.PendingIfUnimplemented +import java.time.LocalDate + +class ReaderSpec extends FlatSpec with Matchers with AppendedClues with PendingIfUnimplemented { + val exampleRequest = Request( + Some("Mister X"), + "de-DE", + "/hello", + Map(), + LocalDate.of(2019,6,26) + ) + + val noUserRequest = exampleRequest.copy(user = None) + + "formatUser" should "read the username or substitute \"anonymous\"" in { + Readers.formatUser.run(exampleRequest) shouldBe "Mister X" + Readers.formatUser.run(noUserRequest) shouldBe "anonymous" + } + + "formatTime" should "output the string representation of a request date" in { + Readers.formatTime.run(exampleRequest) shouldBe "2019-06-26" + } + + "sayBye" should "contain the correct user name and date" in { + Readers.sayBye.run(exampleRequest) shouldBe "Goodbye Mister X, today is 2019-06-26" + } +} + diff --git a/src/test/scala/readerwriter/WriterSpec.scala b/src/test/scala/readerwriter/WriterSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..8a51b3a37f86c0ad3f869b671b7ef8381664de98 --- /dev/null +++ b/src/test/scala/readerwriter/WriterSpec.scala @@ -0,0 +1,57 @@ +package readerwriter + +import java.time.LocalDate + +import org.scalatest._ +import testutil.PendingIfUnimplemented + +class WriterSpec extends FlatSpec with Matchers with AppendedClues with PendingIfUnimplemented { + "collatzDepth" should "keep a log of the search" in { + val (log, _) = Writers.collatzDepth(3).v + log shouldBe List( + "got 3, tripling plus one", + "got 10, halving", + "got 5, tripling plus one", + "got 16, halving", + "got 8, halving", + "got 4, halving", + "got 2, halving", + "got 1, doing nothing", + ) + } + + it should "return the right depth" in { + val (_, depth) = Writers.collatzDepth(3).v + depth shouldBe 7 + } + + "collatzSearch" should "keep a log, together with collatzDepth" in { + val (log, _) = Writers.collatzSearch(1, 5).v + log shouldBe List( + "testing 1", + "got 1, doing nothing", + "depth was 0", + "testing 2", + "got 2, halving", + "got 1, doing nothing", + "depth was 1", + "testing 3", + "got 3, tripling plus one", + "got 10, halving", + "got 5, tripling plus one", + "got 16, halving", + "got 8, halving", + "got 4, halving", + "got 2, halving", + "got 1, doing nothing", + "depth was 7", + "returning 3", + ) + } + + it should "find the smallest number with at least given depth" in { + val (_, res) = Writers.collatzSearch(1, 5).v + res shouldBe 3 + } +} +