-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSaturdayNightTests.scala
More file actions
182 lines (159 loc) · 5.76 KB
/
Copy pathSaturdayNightTests.scala
File metadata and controls
182 lines (159 loc) · 5.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/*
* <p>This code is derived from a Github Gist written by Chris Marshall:
* https://gist.github.com/970717
* </p>
* <p>It is essentially a copy of a fork of the above, made by Rob Dickens:
* https://gist.github.com/1241855
* </p>
*/
import org.lafros.scala.Checking._
/**
* Part Zero : 10:15 Saturday Night
*/
object Sobriety extends Enumeration {
val Sober, Tipsy, Drunk, Paralytic, Unconscious = Value
}
object Gender extends Enumeration {
val Male, Female = Value
}
case class Person(
gender: Gender.Value,
age: Int,
clothes: Set[String],
sobriety: Sobriety.Value)
object people {
val Ken = Person(Gender.Male, 28, Set("Tie", "Shirt"), Sobriety.Tipsy)
val Dave = Person(Gender.Male, 41, Set("Tie", "Jeans"), Sobriety.Sober)
val Ruby = Person(Gender.Female, 25, Set("High Heels"), Sobriety.Tipsy)
}
/**
* Let's define a trait which will contain the checks that *all* nightclubs make! */
trait Nightclub {
type Ch = Checked[Person, String]
//First CHECK
def checkAge(p: Person): Ch =
if (p.age < 18) "Too Young!".toReason
else if (p.age > 40) "Too Old!".toReason
else p.toOkay
//Second CHECK
def checkClothes(p: Person): Ch =
if (p.gender == Gender.Male && !p.clothes("Tie"))
"Smarten Up!".toReason
else if (p.gender == Gender.Female && p.clothes("Trainers"))
"Wear high heels".toReason
else
p.toOkay
//Third CHECK
def checkSobriety(p: Person): Ch =
if (Set(Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious) contains p.sobriety)
"Sober Up!".toReason
else
p.toOkay
}
/**
* Part One : Clubbed to Death
*
* Now let's compose some validation checks */
object ClubbedToDeath extends Nightclub {
def costToEnter(p: Person): Checked[Double, String] = {
//PERFORM THE CHECKS USING Monadic "for comprehension" SUGAR
for {
a <- checkAge(p)
b <- checkClothes(a)
c <- checkSobriety(b)
amount = if (c.gender == Gender.Female) 0D else 5D
} yield amount
}
}
import org.scalatest.{FunSuite, matchers}
import matchers.ShouldMatchers
// Now let's see these in action
class ClubbedToDeathTest extends FunSuite with ShouldMatchers {
import people._
test("costToEnter") {
ClubbedToDeath.costToEnter(Dave) should equal(Reason("Too Old!"))
//scalaz.Validation[String,Double] = Failure(Too Old!)
ClubbedToDeath.costToEnter(Ken) should equal(Okay(5.0))
//scalaz.Validation[String,Double] = Success(5.0)
ClubbedToDeath.costToEnter(Ruby) should equal(Okay(0.0))
// scalaz.Validation[String,Double] = Success(0.0)
ClubbedToDeath.costToEnter(Ruby.copy(age = 17)) should equal(Reason("Too Young!"))
// scalaz.Validation[String,Double] = Failure(Too Young!)
ClubbedToDeath.costToEnter(Ken.copy(sobriety = Sobriety.Unconscious)) should
equal(Reason("Sober Up!"))
// scalaz.Validation[String,Double] = Failure(Sober Up!)
}
}
/**
* The thing to note here is how the Validations can be composed together in a
* for-comprehension. * Scala's type system is making sure that failures flow through your
* computation in a safe manner.
*/
/**
* Part Two : Club Tropicana
*
* Part One showed monadic composition, which from the perspective of Validation is *fail-fast*.
* That is, any failed check shortcircuits subsequent checks. This nicely models nightclubs in
* the real world, as anyone who has dashed home for a pair of smart shoes and returned, only to
* be told that your tie does not pass muster, will attest.
*
* But what about an ideal nightclub? One that tells you *everything* that is wrong with you.
*
*/
object ClubTropicana extends Nightclub {
def costToEnter(p: Person): Checked[Double, Iterable[String]] = {
p.fs(checkAge, checkClothes, checkSobriety).map { p =>
if (p.gender == Gender.Female) 0D else 7.5D
}
}
}
/**
*
* And the use? Dave tried the second nightclub after a few more drinks in the pub
*
*/
class ClubTropicanaTest extends FunSuite with ShouldMatchers {
import people._
test("costToEnter ClubTropicana") {
ClubTropicana.costToEnter(Dave.copy(sobriety = Sobriety.Paralytic)) should
equal(Reason(Iterable("Too Old!", "Sober Up!")))
// scalaz.Scalaz.ValidationNEL[String,Double] = Failure(NonEmptyList(Too Old!, Sober Up!))
ClubTropicana.costToEnter(Ruby) should equal(Okay(0.0))
// scalaz.Scalaz.ValidationNEL[String,Double] = Success(0.0)
}
}
/**
* So, what have we done? Well, with a *tiny change* (and no changes to the individual checks
* themselves), we have completely changed the behaviour to accumulate all errors, rather than
* halting at the first sign of trouble. Imagine trying to do this in Java, using exceptions,
* with ten checks.
*/
/**
* Part Three : Gay Bar
*
* And for those wondering how to do this with a *very long list* of checks. Use sequence:
* List[ValidationNEL[E, A]] ~> (via sequence) ~> ValidationNEL[E, List[A]]
*
* Here we go (unfortunately we need to use a type lambda on the call to sequence):
*/
object GayBar extends Nightclub {
def checkGender(p: Person): Checked[Person, String] =
if (p.gender != Gender.Male) "Men Only".toReason
else p.toOkay
def costToEnter(p: Person): Checked[Double, Iterable[String]] = {
val checks = List(checkAge _, checkClothes _, checkSobriety _, checkGender _)
p.fs(checks: _*).map(_.age + 1.5D)
}
}
class GayBarTest extends FunSuite with ShouldMatchers {
import GayBar._
test("costToEnter GayBar") {
costToEnter(Person(Gender.Male, 59, Set("Jeans"), Sobriety.Paralytic)) should
equal(Reason(Iterable("Too Old!", "Smarten Up!", "Sober Up!")))
// Failure(NonEmptyList(Too Old!, Smarten Up!, Sober Up!))
}
}
/**
* As always; the point is that our validation functions are "static";
* we do not need to change the way they have been coded because we want to combine them in different ways
*/