Skip to content

Instantly share code, notes, and snippets.

@oxbowlakes
Created May 13, 2011 15:14
Show Gist options
  • Save oxbowlakes/970717 to your computer and use it in GitHub Desktop.
Save oxbowlakes/970717 to your computer and use it in GitHub Desktop.
A Tale of 3 Nightclubs
/**
* 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)
/**
* 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)
}
}
// Now let's see these in action
object Test1 {
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 go clubbing!
ClubbedToDeath costToEnter Dave //res0: scalaz.Validation[String,Double] = Failure(Too Old!)
ClubbedToDeath costToEnter Ken //res1: scalaz.Validation[String,Double] = Success(5.0)
ClubbedToDeath costToEnter Ruby //res2: scalaz.Validation[String,Double] = Success(0.0)
ClubbedToDeath costToEnter (Ruby.copy(age = 17)) //res3: scalaz.Validation[String,Double] = Failure(Too Young!)
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 {
import Test1._
ClubTropicana costToEnter (Dave.copy(sobriety = Sobriety.Paralytic)) //res6: scalaz.Scalaz.ValidationNEL[String,Double] = Failure(NonEmptyList(Too Old!, Sober Up!))
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 map {(_ : (Person => Validation[String, Person])).apply(p).liftFailNel}).sequence[({type l[a]=ValidationNEL[String, a]})#l, Person] map {
case c :: _ => c.age + 1.5D
}
}
//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.traverse[({type l[a] = ValidationNEL[String, a]})#l, Person](_ andThen (_.liftFailNel) apply p) map { case c :: _ => c.age + 1.5D }
}
}
object Test3 {
import GayBar._
def main(args: Array[String]) {
costToEnter(Person(Gender.Male, 59, Set("Jeans"), Sobriety.Paralytic))) //Failure(NonEmptyList(Too Old!, Smarten Up!, Sober Up!))
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
*/
@selckin
Copy link

selckin commented Jun 18, 2011

Maybe someone else stumples on this and really doesn't understand the last sequence[*], here it is explained:

21:42:59 dcsobral | selckin: [] means it is a type parameter. They are usually inferred, but it is being passed explicitly in this case.
21:43:33 dcsobral | selckin: next, (...)#l means the "l" member of whatever is inside the parenthesis.
21:44:07 dcsobral | selckin: next, {...} is a structural type: a type defined by what members it contains, instead of a class or trait.
21:44:52 dcsobral | selckin: type l[a] = ... is the definition of a member, the type "l" which is parameterized by "a" -- note that "l" is what you return at the top
21:46:20 dcsobral | selckin: ValidationNEL[String, a] is just a common type. The trick here is that l has a single type parameter, whereas ValidationNEL has two. What this whole sentence does is pass a ValidationNEL with
| the first parameter filled by with String, and the second parameter unfilled (actually, inferred).
21:47:13 dcsobral | selckin: it's as if it said sequence[ValidationNEL[String, infer], Person]
21:47:22 selckin | dcsobral: excellent, thanks for the detailed explanation
21:48:04 dcsobral | no problem. It is ugly as hell, but it is a very nice trick, that has only recently gained more widespread usage.

@fmpwizard
Copy link

If you are copying/pasting, note that the line

object ClubbedToDeath extends Nighclub {
should be
object ClubbedToDeath extends Nightclub { //added a missing t on Nightclub

@ejc123
Copy link

ejc123 commented Jul 7, 2011

It appears that there's an extra ) after liftFailNel on line 155. Once I remove the second ) I get this error:

error: missing parameter type for expanded function ((x$1) => x$1(p).liftFailNel)

I think the compiler is trying to tell me that it can't determine the type of (_(p).liftFailNel) which I suspect is ValidationNEL[String, Person] but I'm not quite sure how to tell it that.

I tried both scala 2.8.0 and 2.8.1 with scalaz 5.0

@oxbowlakes
Copy link
Author

I have changed this so that it now compiles with scalaz 6 and scala 2.9. I have fixed the errors (think there must be a difference with REPL and non-REPL)

@robcd
Copy link

robcd commented Sep 26, 2011

Many thanks for this, and for the recent Skills Matter talk. See my fork for a slightly refactored version which prints each result and gets rid of the match not exhaustive warning.

@robcd
Copy link

robcd commented Apr 30, 2012

Chris, I seem to have found a way to do the above validation without the aid of Scalaz. Have just blogged about it here:

http://robsscala.blogspot.co.uk/2012/04/validation-without-scalaz.html

@ejc123
Copy link

ejc123 commented Apr 30, 2012 via email

@oxbowlakes
Copy link
Author

oxbowlakes commented Apr 30, 2012 via email

@oxbowlakes
Copy link
Author

oxbowlakes commented Apr 30, 2012 via email

@robcd
Copy link

robcd commented Apr 30, 2012

Hi. Yes it can. The check (or checkAndMap) method takes a variable number of arguments (check functions). So you do a p.check(checks: _*).

@robcd
Copy link

robcd commented Apr 30, 2012

Okay - all I've done is to provide a couple of implicit methods which can be called on a single value. But let me have a think!

@oxbowlakes
Copy link
Author

oxbowlakes commented May 2, 2012 via email

@robcd
Copy link

robcd commented May 8, 2012

Hi Chris,
I've had that think, and now added the applicative-functor support:
http://robsscala.blogspot.co.uk/2012/05/validating-multiple-values-at-once.html
The code is now more that a few lines long, but there is still only one new classname to mention in your own code.
Granted, it only applies to Either, but that alone can get you a long way.
Rob

@alexanderdean
Copy link

Hey @oxblowlakes, many thanks for publishing this gist. It's super helpful. I have a problem though - when I try to run it, I get:

value liftFailNel is not a member of scalaz.Validation[String,Name]
[error]     def mkPerson(name: String, age: Int) = (Name(name).liftFailNel ⊛ Age(age).liftFailNel){ (n, a) =>    
Person(n, a)}

I'm using Scalaz 7.0-SNAPSHOT. I've tried Tony's example code too and got the same problem... Any ideas what might be going on?

Edit: looks like liftFailNel is a Scalaz 6 thing - I'm trying out toValidationNEL as a replacement...

@justjoheinz
Copy link

Hi - I am not sure if I can submit a PR to gists, but at https://gist.github.com/justjoheinz/9184859 there is a version updated for scalaz 7.0.5

thanks for sharing - nice example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment