All Downloads are FREE. Search and download functionalities are using the official Maven repository.

monocle.IsoExercises.scala Maven / Gradle / Ivy

/*
 * Copyright 2017-2020 47 Degrees Open Source 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package monoclelib

import monocle.Iso
import monocle.macros.GenIso
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalaexercises.definitions._

object IsoHelper {
  case class Person(name: String, age: Int)

  val personToTuple = Iso[Person, (String, Int)](p => (p.name, p.age)) {
    case (name, age) => Person(name, age)
  }

  def listToVector[A] = Iso[List[A], Vector[A]](_.toVector)(_.toList)

  def vectorToList[A] = listToVector[A].reverse

  val stringToList = Iso[String, List[Char]](_.toList)(_.mkString(""))

  case class MyString(s: String)
  case class Foo()
  case object Bar

}

/**
 * == Iso ==
 *
 * An [[http://julien-truffaut.github.io/Monocle/optics/iso.html `Iso`]] is an optic which converts elements of type `S` into elements of type `A` without loss.
 *
 * Consider a case class `Person` with two fields:
 *
 * {{{
 *   case class Person(name: String, age: Int)
 * }}}
 *
 * @param name iso
 */
object IsoExercises extends AnyFlatSpec with Matchers with Section {

  import IsoHelper._

  /**
   * `Person` is equivalent to a tuple `(String, Int)` and a tuple `(String, Int)` is equivalent to `Person`. So we can create an `Iso` between `Person` and `(String, Int)` using two total functions:
   *
   *  - `get: Person => (String, Int)`
   *  - `reverseGet (aka apply): (String, Int) => Person`
   *
   * {{{
   *   import monocle.Iso
   *   val personToTuple = Iso[Person, (String, Int)](p => (p.name, p.age)){case (name, age) => Person(name, age)}
   * }}}
   */
  def exercisePersonToTuple(res0: (String, Int), res1: Person) = {

    personToTuple.get(Person("Zoe", 25)) should be(res0)
    personToTuple.reverseGet(("Zoe", 25)) should be(res1)
  }

  /**
   * Or simply:
   */
  def exercisePersonToTupleApply(res0: Person) =
    personToTuple(("Zoe", 25)) should be(res0)

  /**
   * Another common use of `Iso` is between collection. `List` and `Vector` represent the same concept, they are both an ordered sequence of elements but they have different performance characteristics. Therefore, we can define an `Iso` between a `List[A]` and a `Vector[A]`:
   * {{{
   *   def listToVector[A] = Iso[List[A], Vector[A]](_.toVector)(_.toList)
   * }}}
   */
  def exerciseListToVector(res0: Vector[Int]) =
    listToVector.get(List(1, 2, 3)) should be(res0)

  /**
   * We can also `reverse` an `Iso` since it defines a symmetric transformation:
   * {{{
   *   def vectorToList[A] = listToVector[A].reverse
   *     // vectorToList: [A]=> monocle.PIso[Vector[A],Vector[A],List[A],List[A]]
   * }}}
   */
  def exerciseVectorToList(res0: List[Int]) =
    vectorToList.get(Vector(1, 2, 3)) should be(res0)

  /**
   * `Iso` are also convenient to lift methods from one type to another, for example a `String` can be seen as a `List[Char]` so we should be able to transform all functions `List[Char] => List[Char]` into `String => String`:
   * {{{
   *   val stringToList = Iso[String, List[Char]](_.toList)(_.mkString(""))
   * }}}
   */
  def exerciseStringToList(res0: String) =
    stringToList.modify(_.tail)("Hello") should be(res0)

  /**
   * = Iso Generation =
   *
   * We defined several macros to simplify the generation of `Iso` between a case class and its `Tuple` equivalent. All macros are defined in a separate module (see modules).
   * {{{
   *     case class MyString(s: String)
   *     case class Foo()
   *     case object Bar
   *
   *     import monocle.macros.GenIso
   * }}}
   *
   * First of all, `GenIso.apply` generates an `Iso` for `newtype` i.e. case class with a single type parameter:
   */
  def exerciseGenIsoApply(res0: String) =
    GenIso[MyString, String].get(MyString("Hello")) should be(res0)

  /**
   * Then, `GenIso.unit` generates an `Iso` for object or case classes with no field:
   *
   * {{{
   * GenIso.unit[Foo]
   * // res8: monocle.Iso[Foo,Unit] = monocle.PIso$$anon$10@280a5b3b
   * GenIso.unit[Bar.type]
   * // res9: monocle.Iso[Bar.type,Unit] = monocle.PIso$$anon$10@5520ac34
   * }}}
   *
   * Finally, `GenIso.fields` is a whitebox macro which generalise `GenIso.apply` to all case classes:
   */
  def exerciseGenIsoFields(res0: (String, Int)) =
    GenIso.fields[Person].get(Person("John", 42)) should be(res0)

  /**
   * 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 exerciseLaws(res0: Boolean, res1: Boolean) = {
    personToTuple.get(Person("Zoe", 25))
    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

    roundTripOneWay(personToTuple, Person("Zoey", 25)) should be(res0)

    roundTripOtherWay(personToTuple, ("Zoe", 52)) should be(res1)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy