-
-
Save robcd/1241855 to your computer and use it in GitHub Desktop.
A Tale of 3 Nightclubs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Part Zero : 10:15 Saturday Night | |
* | |
* (In which we will see how to let the type system help you handle failure)... | |
* | |
* First let's define a domain. (All the following requires scala 2.9.x and scalaz 6.0) */ | |
import scalaz._ | |
import Scalaz._ | |
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 { | |
//First CHECK | |
def checkAge(p: Person): Validation[String, Person] = | |
if (p.age < 18) "Too Young!".fail | |
else if (p.age > 40) "Too Old!".fail | |
else p.success | |
//Second CHECK | |
def checkClothes(p: Person): Validation[String, Person] = | |
if (p.gender == Gender.Male && !p.clothes("Tie")) | |
"Smarten Up!".fail | |
else if (p.gender == Gender.Female && p.clothes("Trainers")) | |
"Wear high heels".fail | |
else | |
p.success | |
//Third CHECK | |
def checkSobriety(p: Person): Validation[String, Person] = | |
if (Set(Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious) contains p.sobriety) | |
"Sober Up!".fail | |
else | |
p.success | |
} | |
/** | |
* Part One : Clubbed to Death | |
* | |
* Now let's compose some validation checks */ | |
object ClubbedToDeath extends Nightclub { | |
def costToEnter(p: Person): Validation[String, Double] = { | |
//PERFORM THE CHECKS USING Monadic "for comprehension" SUGAR | |
for { | |
a <- checkAge(p) | |
b <- checkClothes(a) | |
c <- checkSobriety(b) | |
} yield (if (c.gender == Gender.Female) 0D else 5D) | |
} | |
} | |
trait Test { | |
def run(block: => Any) { | |
println(block) | |
} | |
} | |
// Now let's see these in action | |
object Test1 extends App with Test { | |
import people._ | |
// Let's go clubbing! | |
run(ClubbedToDeath costToEnter Dave) //res0: scalaz.Validation[String,Double] = Failure(Too Old!) | |
run(ClubbedToDeath costToEnter Ken) //res1: scalaz.Validation[String,Double] = Success(5.0) | |
run(ClubbedToDeath costToEnter Ruby) //res2: scalaz.Validation[String,Double] = Success(0.0) | |
run(ClubbedToDeath costToEnter (Ruby.copy(age = 17))) | |
//res3: scalaz.Validation[String,Double] = Failure(Too Young!) | |
run(ClubbedToDeath costToEnter (Ken.copy(sobriety = Sobriety.Unconscious))) | |
//res5: 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. | |
* | |
* Applicative functors to the rescue! | |
* | |
*/ | |
object ClubTropicana extends Nightclub { | |
def costToEnter(p: Person): ValidationNEL[String, Double] = { | |
// PERFORM THE CHECKS USING applicative functors, accumulating failure via a monoid (a | |
// NonEmptyList, or NEL) | |
(checkAge(p).liftFailNel |@| checkClothes(p).liftFailNel |@| checkSobriety(p).liftFailNel) { | |
case (_, _, c) => if (c.gender == Gender.Female) 0D else 7.5D | |
} | |
} | |
} | |
/** | |
* | |
* And the use? Dave tried the second nightclub after a few more drinks in the pub | |
* | |
*/ | |
object Test2 extends App with Test { | |
import people._ | |
run(ClubTropicana costToEnter (Dave.copy(sobriety = Sobriety.Paralytic))) | |
//res6: scalaz.Scalaz.ValidationNEL[String,Double] = Failure(NonEmptyList(Too Old!, Sober Up!)) | |
run(ClubTropicana costToEnter(Ruby)) | |
//res7: 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): Validation[String, Person] = | |
if (p.gender != Gender.Male) "Men Only".fail | |
else p.success | |
def costToEnter(p: Person): ValidationNEL[String, Double] = { | |
val checks = List(checkAge _, checkClothes _, checkSobriety _, checkGender _) | |
checks // : List[Validation[String, Person]] | |
.map(_(p).liftFailNel) // : List[ValidationNEL[String, Person]] | |
.sequence[({type M[A]=ValidationNEL[String, A]})#M, Person] | |
// : ValidationNEL[String, List[Person]] | |
.map { l => (l: @unchecked) match { | |
case c :: _ => c.age + 1.5D // : Function1[List[Person], Double] | |
} | |
} // : ValidationNEL[String, Double] | |
} | |
//Interestingly, as traverse is basically map + sequence, we can reduce this even further | |
def costToEnter2(p: Person): ValidationNEL[String, Double] = { | |
val checks = List(checkAge _, checkClothes _, checkSobriety _, checkGender _) | |
checks // : List[Validation[String, Person]] | |
.traverse[({type M[A] = ValidationNEL[String, A]})#M, Person](_(p).liftFailNel) | |
// : ValidationNEL[String, List[Person]] | |
.map { l => (l: @unchecked) match { | |
case c :: _ => c.age + 1.5D // : Function1[List[Person], Double] | |
} | |
} // : ValidationNEL[String, Double] | |
} | |
} | |
object Test3 extends App with Test { | |
import GayBar._ | |
run(costToEnter(Person(Gender.Male, 59, Set("Jeans"), Sobriety.Paralytic))) | |
//Failure(NonEmptyList(Too Old!, Smarten Up!, Sober Up!)) | |
run(costToEnter2(Person(Gender.Male, 59, Set("Jeans"), Sobriety.Paralytic))) | |
//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 | |
*/ |
To run using sbt,
-
paste code into
something.scala
-
create
build.sbt
containing the following:scalaVersion := "2.9.1"
libraryDependencies += "org.scalaz" %% "scalaz-core" % "6.0.3"
-
create
project/build.properties
containingsbt.version=0.10.1
-
enter
sbt run
at command line.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The above differs from the original in the following respects:
trait Test
havingrun
method that prints each resultApp with Test
, and print their results when runobject people
in order thatTest2
no longer dependent onTest1
costToEnter
andconstToEnter2
refactored slightly, hopefully to improve clarity, adding comments to show result at each stepmap
method in the above methods modified so as to get rid of 'match is not exhaustive!' warnings