🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of SergiiVlasiuk

SergiiVlasiuk's solution

to Lens Person in the Scala Track

Published at Sep 01 2019 · 0 comments
Instructions
Test suite
Solution

Use lenses to update nested records (specific to languages with immutable data).

Updating fields of nested records is kind of annoying in Haskell. One solution is to use lenses. Implement several record accessing functions using lenses, you may use any library you want. The test suite also allows you to avoid lenses altogether so you can experiment with different approaches.

The Scala exercises assume an SBT project scheme. The exercise solution source should be placed within the exercise directory/src/main/scala. The exercise unit tests can be found within the exercise directory/src/test/scala.

To run the tests simply run the command sbt test in the exercise directory.

For more detailed info about the Scala track see the help page.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

LensPersonTest.scala

import org.scalatest.{FunSuite, Matchers}
import java.time.LocalDate
import LensPerson._

/** @version created manually **/
class LensPersonTest extends FunSuite with Matchers {
  val testPerson =
    Person(
        _name = Name(
            _foreNames = "Jane Joanna",
            _surName = "Doe"),
        _born = Born(
            _bornAt = Address(
                _street = "Longway",
                _houseNumber = 1024,
                _place = "Springfield",
                _country = "United States"),
            _bornOn = toEpochDay(1984, 4, 12)),
        _address = Address(
            _street = "Shortlane",
            _houseNumber = 2,
            _place = "Fallmeadow",
            _country = "Canada"))

  def toEpochDay(year: Int, month: Int, dayOfMonth: Int) =
    LocalDate.of(year, month, dayOfMonth).toEpochDay

  test("bornStreet") {
    bornStreet(testPerson._born) should be ("Longway")
  }

  test("setCurrentStreet") {
    pending
    (setCurrentStreet("Middleroad")(testPerson))._address._street should be ("Middleroad")
  }

  test("setBirthMonth") {
    pending
    setBirthMonth(9)(testPerson)._born._bornOn should be (toEpochDay(1984, 9, 12))
  }

  test("renameStreets birth") {
    pending
    renameStreets(_.toUpperCase)(testPerson)._born._bornAt._street should be ("LONGWAY")
  }

  test("renameStreets current") {
    pending
    renameStreets(_.toUpperCase)(testPerson)._address._street should be ("SHORTLANE")
  }
}
import java.time.LocalDate

import monocle.macros.GenLens

import scala.language.postfixOps

object LensPerson {

  case class Person(_name: Name, _born: Born, _address: Address)

  case class Name(_foreNames: String /*Space separated*/ , _surName: String)

  // Value of java.time.LocalDate.toEpochDay
  type EpochDay = Long

  case class Born(_bornAt: Address, _bornOn: EpochDay)

  case class Address(_street: String, _houseNumber: Int,
                     _place: String /*Village / city*/ , _country: String)

  // Valid values of Gregorian are those for which 'java.time.LocalDate.of'
  // returns a valid LocalDate.
  case class Gregorian(_year: Int, _month: Int, _dayOfMonth: Int)

  // Implement these.

  /*
    // START of my solution is
    lazy val bornStreet: Born => String = _._bornAt._street
    lazy val setCurrentStreet: String => Person => Person =
      street => person => person
        .copy(_address = person._address
          .copy(_street = street))
    lazy val setBirthMonth: Int => Person => Person =
      month => person => person
        .copy(_born = person._born
          .copy(_bornOn = LocalDate.ofEpochDay(person._born._bornOn).withMonth(month).toEpochDay))
    // Transform both birth and current street names.
    lazy val renameStreets: (String => String) => Person => Person =
      fun => person => person
        .copy(_born = person._born
          .copy(_bornAt = person._born._bornAt
            .copy(_street = fun(person._born._bornAt._street))),
          _address = person._address
            .copy(fun(person._address._street)))
    // END of my solution is
  */
  //solution borrowed from Wows. I like it because it shows how to use project dependency (monocle-core, monocle-macro)
  private val addressLens = GenLens[Person](_._address)
  private val bornLens = GenLens[Person](_._born)
  private val bornAddressLens = GenLens[Born](_._bornAt)
  private val streetLens = GenLens[Address](_._street)
  private val birthDateLens = GenLens[Person](_._born._bornOn)

  val bornStreet: Born => String = bornAddressLens composeLens streetLens get
  val setCurrentStreet: String => Person => Person =
    s => p => (addressLens composeLens streetLens).modify(_ => s)(p)
  val setBirthMonth: Int => Person => Person = month => p =>
    birthDateLens.modify(day => {
      val date = LocalDate.ofEpochDay(day)
      LocalDate.of(date.getYear, month, date.getDayOfMonth).toEpochDay
    })(p)
  // Transform both birth and current street names.
  val renameStreets: (String => String) => Person => Person = f =>
    p => {
      val person = (bornLens composeLens bornAddressLens composeLens streetLens).modify(s => f(s))(p)
      (addressLens composeLens streetLens).modify(s => f(s))(person)
    }
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?