Skip to content

Commit

Permalink
Add Prism Section
Browse files Browse the repository at this point in the history
  • Loading branch information
valydia committed Mar 2, 2017
1 parent 430efce commit c0b0f8a
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 2 deletions.
19 changes: 19 additions & 0 deletions src/main/scala/monocle/IsoExercises.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,24 @@ object IsoExercises extends FlatSpec with Matchers with Section {

}

/**
* Be aware that whitebox macros are not supported by all IDEs.
*
* == Laws ==
*
* An `Iso` must satisfy all properties defined in `IsoLaws` from the core module. You can check the validity of your own `Iso` using `IsoTests` from the law module.
*
* In particular, an Iso must verify that `get` and `reverseGet` are inverse. This is done via `roundTripOneWay` and `roundTripOtherWay` laws:
*
* {{{
* def roundTripOneWay[S, A](i: Iso[S, A], s: S): Boolean =
* i.reverseGet(i.get(s)) == s
*
* def roundTripOtherWay[S, A](i: Iso[S, A], a: A): Boolean =
* i.get(i.reverseGet(a)) == a
* }}}
*/
def conclusion(): Unit = ()


}
21 changes: 21 additions & 0 deletions src/main/scala/monocle/LensExercises.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,25 @@ object LensExercises extends FlatSpec with Matchers with Section {
GenLens[Person](_.address.streetName).set("Iffley Road")(john) should be(res0)
}

/**
* == Laws ==
*
* A `Lens` must satisfy all properties defined in `LensLaws` from the core module. You can check the validity of your own `Lenses` using `LensTests` from the law module.
*
* In particular, a Lens must respect the `getSet` law which states that if you get a value `A` from `S` and set it back in, the result is an object identical to the original one. A side effect of this law is that set must only update the `A` it points to, for example it cannot increment a counter or modify another value.
*
* {{{
* def getSet[S, A](l: Lens[S, A], s: S): Boolean =
* l.set(l.get(s))(s) == s
* }}}
*
* On the other hand, the `setGet` law states that if you `set` a `value`, you always `get` the same value back. This law guarantees that `set` is actually updating a value `A` inside of `S`.
*
* {{{
* def setGet[S, A](l: Lens[S, A], s: S, a: A): Boolean =
* l.get(l.set(a)(s)) == a
* }}}
*/
def conclusion(): Unit = ()

}
5 changes: 3 additions & 2 deletions src/main/scala/monocle/MonocleLib.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package monocleex

import monocle.IsoExercises
import monocle.{PrismExercises, IsoExercises}
import org.scalaexercises.definitions._

/** Monocle is an optics library for Scala (and Scala.js) strongly inspired by Haskell Lens.
Expand All @@ -16,7 +16,8 @@ object MonocleLib extends Library {

override def sections = List(
IsoExercises,
LensExercises
LensExercises,
PrismExercises
)
override def logoPath = "monocle"
}
194 changes: 194 additions & 0 deletions src/main/scala/monocle/PrismExercises.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package monocle

import org.scalatest._
import org.scalaexercises.definitions._


object PrismHelper {
sealed trait Json
case object JNull extends Json
case class JStr(v: String) extends Json
case class JNum(v: Double) extends Json
case class JObj(v: Map[String, Json]) extends Json

val jStr = Prism.partial[Json, String]{case JStr(v) => v}(JStr)

import monocle.std.double.doubleToInt // Prism[Double, Int] defined in Monocle

val jNum: Prism[Json, Double] = Prism.partial[Json, Double]{case JNum(v) => v}(JNum)

val jInt: Prism[Json, Int] = jNum composePrism doubleToInt

import monocle.macros.GenPrism
val rawJNum: Prism[Json, JNum] = GenPrism[Json, JNum]
}

/** == Prism ==
*
* A [[http://julien-truffaut.github.io/Monocle/optics/prism.html `Prism`]] is an optic used to select part of a `Sum` type (also known as `Coproduct`), e.g. `sealed trait` or `Enum`.
*
* `Prisms` have two type parameters generally called `S` and `A`: `Prism[S, A]` where `S` represents the `Sum` and `A` a part of the Sum.
*
* Let’s take a simplified `Json` encoding:
*
* {{{
* sealed trait Json
* case object JNull extends Json
* case class JStr(v: String) extends Json
* case class JNum(v: Double) extends Json
* case class JObj(v: Map[String, Json]) extends Json
* }}}
*
* We can define a `Prism` which only selects `Json` elements built with a `JStr` constructor by supplying a pair of functions:
* - `getOption: Json => Option[String]`
* - `reverseGet (aka apply): String => Json`
*
* {{{
* import monocle.Prism
*
* val jStr = Prism[Json, String]{
* case JStr(v) => Some(v)
* case _ => None
* }(JStr)
* }}}
*
* It is common to create a `Prism` by pattern matching on constructor, so we also added `partial` which takes a `PartialFunction`:
*
* {{{
* val jStr = Prism.partial[Json, String]{case JStr(v) => v}(JStr)
* }}}
*
* @param name prism
*/
object PrismExercises extends FlatSpec with Matchers with Section {

import PrismHelper._

/**
* We can use the supplied `getOption` and `apply` methods as constructor and pattern matcher for `JStr`:
*/
def exerciseGetOptionAndApply(res0: JStr, res1: Option[String], res2: Option[String]) = {

jStr("hello") should be(res0)

jStr.getOption(JStr("Hello")) should be(res1)

jStr.getOption(JNum(3.2)) should be(res2)
}

/**
* A `Prism` can be used in a pattern matching position:
*
* {{{
* def isLongString(json: Json): Boolean = json match {
* case jStr(v) => v.length > 100
* case _ => false}
* }}}
*
* We can also use `set` and `modify` to update a `Json` only if it is a `JStr`:
*/
def exerciseSetAndModify(res0: JStr, res1: JStr) = {

jStr.set("Bar")(JStr("Hello")) should be(res0)

jStr.modify(_.reverse)(JStr("Hello")) should be(res1)

}

/**
* If we supply another type of `Json`, `set` and `modify` will be a no operation:
*/
def exerciseSetAndModify2(res0: JNum, res1: JNum) = {

jStr.set("Bar")(JNum(10)) should be(res0)

jStr.modify(_.reverse)(JNum(10)) should be(res1)

}

/**
* If we care about the success or failure of the update, we can use `setOption` or `modifyOption`:
*/
def exerciseModifyOption(res0: Option[JStr], res1: Option[JNum]) = {

jStr.modifyOption(_.reverse)(JStr("Hello")) should be(res0)

jStr.modifyOption(_.reverse)(JNum(10)) should be(res1)

}

/**
* As all other optics `Prisms` compose together:
*
* {{{
* import monocle.std.double.doubleToInt // Prism[Double, Int] defined in Monocle
*
* val jNum: Prism[Json, Double] = Prism.partial[Json, Double]{case JNum(v) => v}(JNum)
*
* val jInt: Prism[Json, Int] = jNum composePrism doubleToInt
* }}}
*/
def exerciseCompose(res0: JNum, res1: Option[Int], res2: Option[Int], res3: Option[String]) = {

jInt(5) should be(res0)

jInt.getOption(JNum(5.0)) should be(res1)

jInt.getOption(JNum(5.2)) should be(res2)

jInt.getOption(JStr("Hello")) should be(res3)

}


/** == Prism Generation ==
*
* Generating `Prisms` for subclasses is fairly common, so we added a macro to simplify the process. All macros are defined in a separate module (see [[http://julien-truffaut.github.io/Monocle/modules.html modules]]).
*
* {{{
* import monocle.macros.GenPrism
*
* val rawJNum: Prism[Json, JNum] = GenPrism[Json, JNum]
* }}}
*/
def exercisePrismGeneration(res0: Option[JNum], res1: Option[JNum]) = {

rawJNum.getOption(JNum(4.5)) should be(res0)

rawJNum.getOption(JStr("Hello")) should be(res1)

}

/**
* If you want to get a `Prism[Json, Double]` instead of a `Prism[Json, JNum]`, you can compose `GenPrism` with `GenIso` (see `Iso` [[http://julien-truffaut.github.io/Monocle/optics/iso.html documentation]]):
*
* {{{
* import monocle.macros.GenIso
*
* val jNum: Prism[Json, Double] = GenPrism[Json, JNum] composeIso GenIso[JNum, Double]
* val jNull: Prism[Json, Unit] = GenPrism[Json, JNull.type] composeIso GenIso.unit[JNull.type]
* }}}
*
* A [[https://github.com/julien-truffaut/Monocle/issues/363 ticket]] currently exists to add a macro to merge these two steps together.
*
* == Prism Laws ==
*
* A `Prism` must satisfy all properties defined in `PrismLaws` from the core module. You can check the validity of your own `Prisms` using `PrismTests` from the law module.
*
* In particular, a `Prism` must verify that `getOption` and `reverseGet` allow a full round trip if the Prism matches i.e. if `getOption` returns a `Some`.
*
* {{{
* def partialRoundTripOneWay[S, A](p: Prism[S, A], s: S): Boolean =
* p.getOption(s) match {
* case None => true // nothing to prove
* case Some(a) => p.reverseGet(a) == s
* }
*
* def partialRoundTripOneWay[S, A](p: Prism[S, A], a: A): Boolean =
* p.getOption(p.reverseGet(a)) == Some(a)
* }}}
*/
def conclusion(): Unit = ()


}
69 changes: 69 additions & 0 deletions src/test/scala/monocle/PrismSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package monocle


import monocle.PrismHelper.{Json, JNum, JStr}
import org.scalaexercises.Test
import org.scalatest.FunSuite
import org.scalatest.prop.Checkers

import org.scalacheck.Shapeless._
import shapeless.HNil


class PrismSpec extends FunSuite with Checkers {

test("exercise get option apply") {
check(
Test.testSuccess(
PrismExercises.exerciseGetOptionAndApply _,
JStr("hello") :: Option("Hello") :: Option.empty[String] :: HNil
)
)
}

test("exercise set and modify") {
check(
Test.testSuccess(
PrismExercises.exerciseSetAndModify _,
JStr("Bar") :: JStr("olleH") :: HNil
)
)
}

test("exercise set and modify 2") {
check(
Test.testSuccess(
PrismExercises.exerciseSetAndModify2 _,
JNum(10) :: JNum(10) :: HNil
)
)
}

test("exercise modifyOption") {
check(
Test.testSuccess(
PrismExercises.exerciseModifyOption _,
Option(JStr("olleH")) :: Option.empty[JNum] :: HNil
)
)
}

test("exercise compose") {
check(
Test.testSuccess(
PrismExercises.exerciseCompose _,
JNum(5.0) :: Option(5) :: Option.empty[Int] :: Option.empty[String] :: HNil
)
)
}

test("exercise prism generation") {
check(
Test.testSuccess(
PrismExercises.exercisePrismGeneration _,
Option(JNum(4.5)) :: Option.empty[JNum] :: HNil
)
)
}

}

0 comments on commit c0b0f8a

Please sign in to comment.