diff --git a/README.md b/README.md index 873d0c85bb0a30dbb013fc481a5114a055090028..0efc9867441063c6dcff25492099004967eb3c4f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ usually marked with `???`. | 4: Laziness | [`laziness`](src/main/scala/laziness/) | `testOnly laziness.*` | 5: Algebras, Laws, and Monoids | [`algebra`](src/main/scala/algebra/) | `testOnly algebra.*` | 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) ## Usage tips: To keep your local solutions to the exercises when pulling from the repository, diff --git a/src/main/scala/applicative/Applicative.scala b/src/main/scala/applicative/Applicative.scala new file mode 100644 index 0000000000000000000000000000000000000000..baf28032adb822c6b0d70e5d44f1f9e97e71af3e --- /dev/null +++ b/src/main/scala/applicative/Applicative.scala @@ -0,0 +1,34 @@ +package applicative + +import scala.language.higherKinds + +trait Applicative[F[_]] { + def unit[A](a: A): F[A] + + def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = map2(ff, fa)((f, a) => f(a)) + + // implement map using unit and ap, but not flatMap/flatten + def map[A, B](fa: F[A])(f: A => B): F[B] = ??? + + // implement map2 using unit and ap + // hint: f.curried converts `(A,B) => C` to `A => (B => C)` + def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = ??? + + + // implement unit first + // then write the signature for ap and map2 + // then implement one of them + def compose[G[_]](implicit G: Applicative[G]): Applicative[Lambda[a => F[G[a]]]] = { + val F = this + new Applicative[Lambda[a => F[G[a]]]] { + + def unit[A](a: A): F[G[A]] = ??? + //note: when you are done implementing this, the test for ap/map2 will result in a StackOverflow until you implement one of them + + + // override def ap... = ??? + // or + // override def map2... = ??? + } + } +} diff --git a/src/main/scala/applicative/Monad.scala b/src/main/scala/applicative/Monad.scala new file mode 100644 index 0000000000000000000000000000000000000000..1455cae42461a088f7f524f3f731ff2c0cc178a1 --- /dev/null +++ b/src/main/scala/applicative/Monad.scala @@ -0,0 +1,11 @@ +package applicative + +import scala.language.higherKinds + +trait Monad[F[_]] extends Applicative[F] { + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = flatten(map(fa)(f)) + + def flatten[A](fa: F[F[A]]): F[A] = flatMap(fa)(identity) + override def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)(f andThen unit) + override def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = flatMap(fa)(a => map(fb)(b => f(a,b))) +} diff --git a/src/main/scala/applicative/Validated.scala b/src/main/scala/applicative/Validated.scala new file mode 100644 index 0000000000000000000000000000000000000000..879f3735106a4f936f1c785382b24e9498794269 --- /dev/null +++ b/src/main/scala/applicative/Validated.scala @@ -0,0 +1,12 @@ +package applicative + +sealed trait Validated[+E, +A] +case class Valid[+A](a:A) extends Validated[Nothing, A] +case class Invalid[+E](head: E, tail: List[E] = List()) extends Validated[E, Nothing] + +object Validated { + implicit def validatedApplicative[E]: Applicative[Validated[E, +?]] = new Applicative[Validated[E,+?]] { + def unit[A](a: A) = Valid(a) + // add map2 or ap here + } +} diff --git a/src/test/scala/applicative/ApplicativeSpec.scala b/src/test/scala/applicative/ApplicativeSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..58468199fd9bd6cf11c1c7b7ffe8c4aed2e83976 --- /dev/null +++ b/src/test/scala/applicative/ApplicativeSpec.scala @@ -0,0 +1,49 @@ +package applicative + +import org.scalatest._ +import testutil.PendingIfUnimplemented + +class ApplicativeSpec extends FlatSpec with Matchers with AppendedClues with PendingIfUnimplemented { + implicit val optionApplicative = new Applicative[Option] { + override def unit[A](a: A): Option[A] = Some(a) + override def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] = ff.flatMap(f => fa.map(f)) + } + implicit val listApplicative = new Applicative[List] { + override def unit[A](a: A): List[A] = List(a) + override def ap[A, B](ff: List[A => B])(fa: List[A]): List[B] = ff.flatMap(f => fa.map(f)) + } + + "map" should "work the same when implemented via ap" in { + val f: Int => Int = i => i * 3 + val g: String => String = s => "Hello " + s + val h: Int => String = _.toHexString + + val someint = Some(2) + val somestring = Some("world") + + optionApplicative.map(someint)(f) shouldBe someint.map(f) + optionApplicative.map(somestring)(g) shouldBe somestring.map(g) + optionApplicative.map(None)(g) shouldBe None.map(g) + optionApplicative.map(None)(g) shouldBe None.map(g) + } + + "map2" should "combine two monadic values" in { + optionApplicative.map2(Some(1), Some(2))((a, b) => a + b) shouldBe Some(3) + optionApplicative.map2(Option.empty[Int], Some(2))((a, b) => a + b) shouldBe None + optionApplicative.map2(Some(2), Option.empty[Int])((a, b) => a + b) shouldBe None + } + + "compose" should "create nested element with unit" in { + val ola: Applicative[Lambda[a => Option[List[a]]]] = optionApplicative.compose(listApplicative) + + ola.unit(5) shouldBe Some(List(5)) + } + + it should "implement map2 or ap correctly" in { + val ola: Applicative[Lambda[a => Option[List[a]]]] = optionApplicative.compose(listApplicative) + + val optList = Some(List(1, 2, 3, 4)) + + ola.map(optList)(_ + 10) shouldBe Some(List(11, 12, 13, 14)) + } +} diff --git a/src/test/scala/applicative/ValidatedSpec.scala b/src/test/scala/applicative/ValidatedSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..835cdcdff9254c9c2f595e2a963df0f902e443fc --- /dev/null +++ b/src/test/scala/applicative/ValidatedSpec.scala @@ -0,0 +1,26 @@ +package applicative + +import org.scalatest._ +import testutil.PendingIfUnimplemented + +class ValidatedSpec extends FlatSpec with Matchers with AppendedClues with PendingIfUnimplemented { + "map2" should "accumulate errors" in { + type VSI = Validated[String, Int] + val valid1: VSI = Valid(1) + val valid2: VSI = Valid(2) + val invalidA: VSI = Invalid("A") + val invalidB: VSI = Invalid("B") + val invalidAB: VSI = Invalid("A", List("B")) + val invalidABAB: VSI = Invalid("A", List("B", "A", "B")) + + val strErrorApplicative = implicitly[Applicative[Validated[String, +?]]] + + ??? //remove, when you have overridden map2 or ap + + strErrorApplicative.map2(valid1, valid2)((a, b) => a + b) shouldBe Valid(3) + strErrorApplicative.map2(invalidA, valid2)((a, b) => a + b) shouldBe invalidA + strErrorApplicative.map2(valid1, invalidB)((a, b) => a + b) shouldBe invalidB + strErrorApplicative.map2(invalidA, invalidB)((a, b) => a + b) shouldBe invalidAB + strErrorApplicative.map2(invalidAB, invalidAB)((a, b) => a + b) shouldBe invalidABAB + } +}