public
Last active

A Tale of 3 Nightclubs

  • Download Gist
3nightclubs.scala
Scala
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 183 184
/**
* 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
*/

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.

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

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

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)

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.

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

Awesome! Thanks for sharing.

On Mon, Apr 30, 2012 at 10:48 AM, Rob Dickens
reply@reply.github.com
wrote:

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

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


Reply to this email directly or view it on GitHub:
https://gist.github.com/970717

Can you do sequence? Obviously you can use Either's right-projection in a
monadic way

On Mon, Apr 30, 2012 at 4:48 PM, Rob Dickens <
reply@reply.github.com

wrote:

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

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


Reply to this email directly or view it on GitHub:
https://gist.github.com/970717

Your implementation of the ApplicativeBuilder is also restricted. My
example is actually a bit rubbish because it does not really demonstrate
the following:

ValidationNel[X, A] |@| ValidationNel[X, B] apply (f: (A, B) => C) :

ValidationNel[X, C]

That is, the great thing is that the successful part of the validations is
being passed into the function application. In my example, the success case
is actually just the original person. The utility of this can be seen when
(for example) parsing a file full of stuff:

(parseNumber("Qty") |@| parseDecimal("Price") |@|

parseInstrument("Investment"))(Trade.apply)

Where Trade is a case class:

case class Trade(qty: Int, px: Double, inv: Investment)

So you can say:

(lines map f).sequence //lines is Stream[String], f is the function
defined above using applicative

Which is just (of course)

lines traverse f

Chris

On Mon, Apr 30, 2012 at 4:48 PM, Rob Dickens <
reply@reply.github.com

wrote:

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

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


Reply to this email directly or view it on GitHub:
https://gist.github.com/970717

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

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!

There's another point here, Rob. The *exact *same "applicative" pattern can
be used in a number of contexts and behaves as you would expect. For
example:

Option[A] |@| Option[B] apply (f: (A, B) => C) ~~> Option[C]
Promise[A] |@| Promise[B] apply (f: (A, B) => C) ~~> Promise[C]
List[A] |@| List[B] apply (f: (A, B) => C) ~~> List[C]

..obviously I could go on. But we also get other behaviour in scalaz which
is useful. For example, <+> gives us orElse, again consistently between
types:

Option[A] <+> Option[A] ~~> OptionA
Validation[X, A] <+> Validation[X, A] ~~> ValidationX, A

I totally agree on a general level about not wanting a big dependency for a
small feature - I apply the same rationale myself. But that is where scalaz
wins hands-down, I think. There's a ton of great stuff in there

Chris

On Mon, Apr 30, 2012 at 6:12 PM, Rob Dickens <
reply@reply.github.com

wrote:

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!


Reply to this email directly or view it on GitHub:
https://gist.github.com/970717

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

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...

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!

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.