-
Notifications
You must be signed in to change notification settings - Fork 146
/
ValidationExample.cs
252 lines (212 loc) · 9.13 KB
/
ValidationExample.cs
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.FSharp.Core;
using Microsoft.FSharp.Collections;
using NUnit.Framework;
namespace FSharpx.CSharpTests.ValidationExample {
// ported from original in Scalaz: https://gist.github.com/970717
// First let's define a domain.
enum Sobriety { Sober, Tipsy, Drunk, Paralytic, Unconscious }
enum Gender { Male, Female }
class Person {
private readonly Gender gender;
private readonly int age;
private readonly FSharpSet<string> clothes;
private readonly Sobriety sobriety;
public Gender Gender {
get { return gender; }
}
public int Age {
get { return age; }
}
public FSharpSet<string> Clothes {
get { return clothes; }
}
public Sobriety Sobriety {
get { return sobriety; }
}
public Person(Gender gender, int age, FSharpSet<string> clothes, Sobriety sobriety) {
this.gender = gender;
this.age = age;
this.clothes = clothes;
this.sobriety = sobriety;
}
}
// Let's define the checks that *all* nightclubs make!
class Club {
public static readonly Func<Person, FSharpChoice<Person, FSharpList<string>>>
CheckAge =
p => {
if (p.Age < 18)
return FSharpChoice.Error<Person>("Too young!");
if (p.Age > 40)
return FSharpChoice.Error<Person>("Too old!");
return FSharpChoice.Ok(p);
};
public static readonly Func<Person, FSharpChoice<Person, FSharpList<string>>>
CheckClothes =
p => {
if (p.Gender == Gender.Male && !p.Clothes.Contains("Tie"))
return FSharpChoice.Error<Person>("Smarten up!");
if (p.Gender == Gender.Female && p.Clothes.Contains("Trainers"))
return FSharpChoice.Error<Person>("Wear high heels!");
return FSharpChoice.Ok(p);
};
public static readonly Func<Person, FSharpChoice<Person, FSharpList<string>>>
CheckSobriety =
p => {
if (new[] { Sobriety.Drunk, Sobriety.Paralytic, Sobriety.Unconscious }.Contains(p.Sobriety))
return FSharpChoice.Error<Person>("Sober up!");
return FSharpChoice.Ok(p);
};
}
// Now let's compose some validation checks
class ClubbedToDeath {
// PERFORM THE CHECKS USING Monadic SUGAR (LINQ)
public static FSharpChoice<decimal, FSharpList<string>> CostToEnter(Person p) {
return from a in Club.CheckAge(p)
from b in Club.CheckClothes(a)
from c in Club.CheckSobriety(b)
select c.Gender == Gender.Female ? 0m : 5m;
}
}
// Now let's see these in action
[TestFixture]
class Test1 {
public static readonly Person Ken = new Person(
gender: Gender.Male,
age: 28,
clothes: FSharpSet.Create("Tie", "Shirt"),
sobriety: Sobriety.Tipsy);
public static readonly Person Dave = new Person(
gender: Gender.Male,
age: 41,
clothes: FSharpSet.Create("Tie", "Jeans"),
sobriety: Sobriety.Sober);
public static readonly Person Ruby = new Person(
gender: Gender.Female,
age: 25,
clothes: FSharpSet.Create("High heels"),
sobriety: Sobriety.Tipsy);
// let's go clubbing!
[Test]
public void Part1() {
var costDave = ClubbedToDeath.CostToEnter(Dave);
Assert.AreEqual(FSharpChoice.Error<decimal>("Too old!"), costDave);
var costKen = ClubbedToDeath.CostToEnter(Ken);
Assert.AreEqual(FSharpChoice.Ok(5m), costKen);
var costRuby = ClubbedToDeath.CostToEnter(Ruby);
Assert.AreEqual(FSharpChoice.Ok(0m), costRuby);
var Ruby17 = new Person(
age: 17,
clothes: Ruby.Clothes,
sobriety: Ruby.Sobriety,
gender: Ruby.Gender);
var costRuby17 = ClubbedToDeath.CostToEnter(Ruby17);
Assert.AreEqual(FSharpChoice.Error<decimal>("Too young!"), costRuby17);
var KenUnconscious = new Person(
age: Ken.Age,
clothes: Ken.Clothes,
gender: Ken.Gender,
sobriety: Sobriety.Unconscious);
var costKenUnconscious = ClubbedToDeath.CostToEnter(KenUnconscious);
Assert.AreEqual(FSharpChoice.Error<decimal>("Sober up!"), costKenUnconscious);
/**
* The thing to note here is how the Validations can be composed together in a computation expression.
* The 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!
*
*/
class ClubTropicana {
//PERFORM THE CHECKS USING applicative functors, accumulating failure via a monoid
// using LINQ sugar
public static FSharpChoice<decimal, FSharpList<string>> CostToEnter(Person p) {
return from c in Club.CheckAge(p)
join x in Club.CheckClothes(p) on 1 equals 1
join y in Club.CheckSobriety(p) on 1 equals 1
select c.Gender == Gender.Female ? 0m : 7.5m;
}
// or using regular functions:
static readonly Func<Person, Person, Person, decimal> CostByGender =
(p, x, y) => p.Gender == Gender.Female ? 0m : 7.5m;
public static FSharpChoice<decimal, FSharpList<string>> CostToEnter2(Person p) {
return CostByGender.Curry().ReturnValidation()
.ApValidation(Club.CheckAge(p))
.ApValidation(Club.CheckClothes(p))
.ApValidation(Club.CheckSobriety(p));
}
}
// And the use? Dave tried the second nightclub after a few more drinks in the pub
[TestFixture]
class Test2 {
[Test]
public void Part2() {
var daveParalytic = new Person(
age: Test1.Dave.Age,
clothes: Test1.Dave.Clothes,
gender: Test1.Dave.Gender,
sobriety: Sobriety.Paralytic);
var costDaveParalytic = ClubTropicana.CostToEnter(daveParalytic);
Assert.AreEqual(FSharpChoice.Errors<decimal>("Too old!", "Sober up!"), costDaveParalytic);
var costRuby = ClubTropicana.CostToEnter2(Test1.Ruby);
Assert.AreEqual(FSharpChoice.Ok(0m), costRuby);
}
/**
*
* 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 using exceptions, with ten checks.
*
*/
}
/**
*
* Part Three : Gay bar
*
* And for those wondering how to do this with a *very long list* of checks.
*
*/
class GayBar {
public static readonly Func<Person, FSharpChoice<Person, FSharpList<string>>>
CheckGender =
p => {
if (p.Gender == Gender.Male)
return FSharpChoice.Ok(p);
return FSharpChoice.Error<Person>("Men only");
};
public static FSharpChoice<decimal, FSharpList<string>> CostToEnter(Person p) {
return
FSharpList.Create(Club.CheckAge, Club.CheckClothes, Club.CheckSobriety, CheckGender)
.SelectMValidation(check => check(p))
.Select(x => x[0].Age + 1.5m);
}
}
[TestFixture]
class Test3 {
[Test]
public void Part3() {
var person = new Person(
gender: Gender.Male,
age: 59,
clothes: FSharpSet.Create("Jeans"),
sobriety: Sobriety.Paralytic);
var cost = GayBar.CostToEnter(person);
Assert.AreEqual(FSharpChoice.Errors<decimal>("Too old!", "Smarten up!", "Sober up!"), cost);
}
}
}