Skip to content

Commit d8a8ac5

Browse files
authored
Merge pull request #157 from abo64/exercise-lense-person
exercise lense-person
2 parents d69d50b + f54ab50 commit d8a8ac5

File tree

6 files changed

+269
-1
lines changed

6 files changed

+269
-1
lines changed

config.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"parallel-letter-frequency",
6666
"acronym",
6767
"zipper",
68-
"forth"
68+
"forth",
69+
"lens-person"
6970
],
7071
"exercises": [
7172
{
@@ -445,6 +446,12 @@
445446
"slug": "forth",
446447
"difficulty": 1,
447448
"topics": [
449+
]
450+
},
451+
{
452+
"slug": "lens-person",
453+
"difficulty": 1,
454+
"topics": [
448455
]
449456
}
450457
],

exercises/lens-person/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Lens Person
2+
3+
Use lenses to manipulate (get, set, modify) immutable objects.
4+
5+
Updating fields of nested (immutable) objects is kind of annoying in Scala. One solution
6+
is to use [lenses](https://wiki.haskell.org/Lens).
7+
Implement several class attribute accessing functions using lenses, you may use any library you want. One good choice could be [Monocle](https://julien-truffaut.github.io/Monocle/].
8+
The test suite also allows you to avoid lenses alltogether so you can experiment with
9+
different approaches.
10+
11+
12+
The Scala exercises assume an SBT project scheme. The exercise solution source
13+
should be placed within the exercise directory/src/main/scala. The exercise
14+
unit tests can be found within the exercise directory/src/test/scala.
15+
16+
To run the tests simply run the command `sbt test` in the exercise directory.
17+
18+
For more detailed info about the Scala track see the [help
19+
page](http://help.exercism.io/getting-started-with-scala.html).

exercises/lens-person/build.sbt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
scalaVersion := "2.11.8"
2+
3+
libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.5" % "test"
4+
5+
libraryDependencies ++= Seq(
6+
"com.github.julien-truffaut" % "monocle-core_2.11" % "1.2.2",
7+
"com.github.julien-truffaut" % "monocle-macro_2.11" % "1.2.2"
8+
)
9+
10+
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.6"
11+
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import java.time.LocalDate
2+
import LensPerson._
3+
4+
trait ToBeImplemented {
5+
6+
// Implement these.
7+
8+
val bornStreet: Born => String
9+
10+
val setCurrentStreet: String => Person => Person
11+
12+
val setBirthMonth: Int => Person => Person
13+
14+
// Transform both birth and current street names.
15+
val renameStreets: (String => String) => Person => Person
16+
}
17+
18+
object LensPerson extends ToBeImplemented with MonocleSolution /* with ScalazSolution */ {
19+
20+
case class Person(_name: Name, _born: Born, _address: Address)
21+
22+
case class Name(_foreNames: String /*Space separated*/ , _surName: String)
23+
24+
// Value of java.time.LocalDate.toEpochDay
25+
type EpochDay = Long
26+
27+
case class Born(_bornAt: Address, _bornOn: EpochDay)
28+
29+
case class Address(_street: String, _houseNumber: Int,
30+
_place: String /*Village / city*/ , _country: String)
31+
32+
// Valid values of Gregorian are those for which 'java.time.LocalDate.of'
33+
// returns a valid LocalDate.
34+
case class Gregorian(_year: Int, _month: Int, _dayOfMonth: Int)
35+
}
36+
37+
trait MonocleSolution extends ToBeImplemented {
38+
import monocle.PIso
39+
import monocle.macros.GenLens
40+
41+
// Person Lenses
42+
val personLens = GenLens[Person]
43+
val name = personLens(_._name)
44+
val born = personLens(_._born)
45+
val address = personLens(_._address)
46+
47+
// Name Lenses
48+
val nameLens = GenLens[Name]
49+
val foreNames = nameLens(_._foreNames)
50+
val surName = nameLens(_._surName)
51+
52+
// Born Lenses
53+
val bornLens = GenLens[Born]
54+
val bornAt = bornLens(_._bornAt)
55+
val bornOn = bornLens(_._bornOn)
56+
57+
// Address Lenses
58+
val street = GenLens[Address](_._street)
59+
60+
// Gregorian Lenses
61+
val gregorianLens = GenLens[Gregorian]
62+
val dayOfMonth = gregorianLens(_._dayOfMonth)
63+
val month = gregorianLens(_._month)
64+
val year = gregorianLens(_._year)
65+
66+
val bornStreet: Born => String =
67+
bornAt ^|-> street get
68+
69+
val setCurrentStreet: String => Person => Person =
70+
address ^|-> street set
71+
72+
val isoEpocheDayGregorian: PIso[EpochDay, EpochDay, Gregorian, Gregorian] = {
73+
def epochDayToGregorian(ed: EpochDay): Gregorian = {
74+
val ld = LocalDate.ofEpochDay(ed)
75+
Gregorian(ld.getYear, ld.getMonth.getValue, ld.getDayOfMonth)
76+
}
77+
def gregorianToEpochDay(g: Gregorian): EpochDay =
78+
LocalDate.of(g._year, g._month, g._dayOfMonth).toEpochDay
79+
80+
PIso(epochDayToGregorian _)(gregorianToEpochDay _)
81+
}
82+
83+
val setBirthMonth: Int => Person => Person =
84+
born ^|-> bornOn ^<-> isoEpocheDayGregorian ^|-> month set
85+
86+
val renameStreets: (String => String) => Person => Person =
87+
f => ((born ^|-> bornAt ^|-> street modify f) .compose (address ^|-> street modify f))
88+
}
89+
90+
trait ScalazSolution extends ToBeImplemented {
91+
import scalaz.Lens
92+
93+
// Person Lenses
94+
val name = Lens.lensu[Person, Name](
95+
(person, name) => person.copy(_name = name), (_._name))
96+
val born = Lens.lensu[Person, Born](
97+
(person, born) => person.copy(_born = born), (_._born))
98+
val address = Lens.lensu[Person, Address](
99+
(person, address) => person.copy(_address = address), (_._address))
100+
101+
// Name Lenses
102+
val foreNames = Lens.lensu[Name, String](
103+
(name, foreNames) => name.copy(_foreNames = foreNames), (_._foreNames))
104+
val surName = Lens.lensu[Name, String](
105+
(name, surName) => name.copy(_surName = surName), (_._surName))
106+
107+
// Born Lenses
108+
val bornAt = Lens.lensu[Born, Address](
109+
(born, address) => born.copy(_bornAt = address), (_._bornAt))
110+
val bornOn = Lens.lensu[Born, EpochDay](
111+
(born, epochDay) => born.copy(_bornOn = epochDay), (_._bornOn))
112+
113+
// Address Lenses
114+
val street = Lens.lensu[Address, String](
115+
(address, street) => address.copy(_street = street), (_._street))
116+
117+
// Gregorian Lenses
118+
val dayOfMonth = Lens.lensu[Gregorian, Int](
119+
(gregorian, dayOfMonth) => gregorian.copy(_dayOfMonth = dayOfMonth), (_._dayOfMonth))
120+
val month = Lens.lensu[Gregorian, Int](
121+
(gregorian, month) => gregorian.copy(_month = month), (_._month))
122+
val year = Lens.lensu[Gregorian, Int](
123+
(gregorian, year) => gregorian.copy(_year = year), (_._year))
124+
125+
126+
val bornStreet: Born => String =
127+
bornAt >=> street get
128+
129+
val setCurrentStreet: String => Person => Person =
130+
s => address >=> street set(_, s)
131+
132+
def epochDayToGregorian(ed: EpochDay): Gregorian = {
133+
val ld = LocalDate.ofEpochDay(ed)
134+
Gregorian(ld.getYear, ld.getMonth.getValue, ld.getDayOfMonth)
135+
}
136+
def gregorianToEpochDay(g: Gregorian): EpochDay =
137+
LocalDate.of(g._year, g._month, g._dayOfMonth).toEpochDay
138+
139+
val setBirthMonth: Int => Person => Person =
140+
m => born >=> bornOn =>= setMonth(m)
141+
142+
val setMonth: Int => EpochDay => EpochDay =
143+
newMonth => oldEpochDay => {
144+
val Gregorian(year, _, day) = epochDayToGregorian(oldEpochDay)
145+
val newGregorian = Gregorian(year, newMonth, day)
146+
gregorianToEpochDay(newGregorian)
147+
}
148+
149+
val renameStreets: (String => String) => Person => Person =
150+
f => ((born >=> bornAt >=> street =>= f) .compose (address >=> street =>= f))
151+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import java.time.LocalDate
2+
3+
object LensPerson {
4+
case class Person(_name: Name, _born: Born, _address: Address)
5+
6+
case class Name(_foreNames: String /*Space separated*/ , _surName: String)
7+
8+
// Value of java.time.LocalDate.toEpochDay
9+
type EpochDay = Long
10+
11+
case class Born(_bornAt: Address, _bornOn: EpochDay)
12+
13+
case class Address(_street: String, _houseNumber: Int,
14+
_place: String /*Village / city*/ , _country: String)
15+
16+
// Valid values of Gregorian are those for which 'java.time.LocalDate.of'
17+
// returns a valid LocalDate.
18+
case class Gregorian(_year: Int, _month: Int, _dayOfMonth: Int)
19+
20+
// Implement these.
21+
22+
val bornStreet: Born => String = ???
23+
24+
val setCurrentStreet: String => Person => Person = ???
25+
26+
val setBirthMonth: Int => Person => Person = ???
27+
28+
// Transform both birth and current street names.
29+
val renameStreets: (String => String) => Person => Person = ???
30+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import org.scalatest.{FunSuite, Matchers}
2+
import java.time.LocalDate
3+
import LensPerson._
4+
5+
class LensPersonTest extends FunSuite with Matchers {
6+
val testPerson =
7+
Person(
8+
_name = Name(
9+
_foreNames = "Jane Joanna",
10+
_surName = "Doe"),
11+
_born = Born(
12+
_bornAt = Address(
13+
_street = "Longway",
14+
_houseNumber = 1024,
15+
_place = "Springfield",
16+
_country = "United States"),
17+
_bornOn = toEpochDay(1984, 4, 12)),
18+
_address = Address(
19+
_street = "Shortlane",
20+
_houseNumber = 2,
21+
_place = "Fallmeadow",
22+
_country = "Canada"))
23+
24+
def toEpochDay(year: Int, month: Int, dayOfMonth: Int) =
25+
LocalDate.of(year, month, dayOfMonth).toEpochDay
26+
27+
test("bornStreet") {
28+
bornStreet(testPerson._born) should be ("Longway")
29+
}
30+
31+
test("setCurrentStreet") {
32+
pending
33+
(setCurrentStreet("Middleroad")(testPerson))._address._street should be ("Middleroad")
34+
}
35+
36+
test("setBirthMonth") {
37+
pending
38+
setBirthMonth(9)(testPerson)._born._bornOn should be (toEpochDay(1984, 9, 12))
39+
}
40+
41+
test("renameStreets birth") {
42+
pending
43+
renameStreets(_.toUpperCase)(testPerson)._born._bornAt._street should be ("LONGWAY")
44+
}
45+
46+
test("renameStreets current") {
47+
pending
48+
renameStreets(_.toUpperCase)(testPerson)._address._street should be ("SHORTLANE")
49+
}
50+
}

0 commit comments

Comments
 (0)