Skip to content

Commit fb5156e

Browse files
Merge pull request #440 from fdornak/validation_result
Validation.Result uses Result instead of Choice
2 parents b2b6526 + 5a6b7df commit fb5156e

File tree

8 files changed

+416
-2
lines changed

8 files changed

+416
-2
lines changed

src/FSharpx.Extras/CSharpCompat.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ open FSharpx.Functional
1010
open FSharpx
1111
open Microsoft.FSharp.Control.WebExtensions
1212

13+
#nowarn "FS0044"
14+
1315
[<assembly:Extension>]
1416
do()
1517

src/FSharpx.Extras/ComputationExpressions/Validation.fs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace FSharpx
22

3+
[<System.Obsolete("This module is deprecated. Use Validation.Result instead.")>]
34
module Validation =
45
open FSharpx.Collections
56
open Choice
@@ -84,3 +85,80 @@ module Validation =
8485
let inline mapM f x = sequence (List.map f x)
8586

8687
let inline mapMIgnore f x = sequenceIgnore (List.map f x)
88+
89+
module Result =
90+
open FSharpx
91+
92+
let apa append x f =
93+
match f,x with
94+
| Ok f, Ok x -> Ok (f x)
95+
| Error e, Ok _ -> Error e
96+
| Ok _, Error e -> Error e
97+
| Error e1, Error e2 -> Error (append e1 e2)
98+
99+
/// Sequential application, parameterized by semigroup
100+
let inline apm (m: _ ISemigroup) = apa (curry m.Combine)
101+
102+
type CustomValidation<'T>(semigroup: 'T ISemigroup) =
103+
/// Sequential application
104+
member this.ap x = apm semigroup x
105+
106+
/// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right.
107+
member this.lift2 f a b = Ok f |> this.ap a |> this.ap b
108+
109+
/// Sequence actions, discarding the value of the first argument.
110+
member this.apr b a = this.lift2 (fun _ z -> z) a b
111+
112+
/// Sequence actions, discarding the value of the second argument.
113+
member this.apl b a = this.lift2 (fun z _ -> z) a b
114+
115+
member this.seqValidator f =
116+
let inline cons a b = this.lift2 (flip List.cons) a b
117+
Seq.map f >> Seq.fold cons (Ok [])
118+
119+
member this.sequence s =
120+
let inline cons a b = this.lift2 List.cons a b
121+
List.foldBack cons s (Ok [])
122+
123+
member this.sequenceIgnore s = this.sequence s |> Result.map ignore
124+
125+
member this.mapM f x = this.sequence (List.map f x)
126+
127+
member this.mapMIgnore f x = this.sequenceIgnore (List.map f x)
128+
129+
130+
type NonEmptyListSemigroup<'T>() =
131+
interface ISemigroup<'T NonEmptyList> with
132+
member x.Combine(a,b) = NonEmptyList.append a b
133+
134+
type NonEmptyListValidation<'T>() =
135+
inherit CustomValidation<'T NonEmptyList>(NonEmptyListSemigroup<'T>())
136+
137+
/// Sequential application
138+
let inline ap x = apa NonEmptyList.append x
139+
140+
/// Sequential application
141+
let inline (<*>) f x = ap x f
142+
143+
/// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right.
144+
let inline lift2 f a b = Ok f <*> a <*> b
145+
146+
/// Sequence actions, discarding the value of the first argument.
147+
let inline ( *>) x y = lift2 (fun _ z -> z) x y
148+
149+
/// Sequence actions, discarding the value of the second argument.
150+
let inline ( <*) x y = lift2 (fun z _ -> z) x y
151+
152+
let seqValidator f =
153+
let inline cons a b = lift2 (flip List.cons) a b
154+
Seq.map f >> Seq.fold cons (Ok [])
155+
156+
let inline sequence s =
157+
let inline cons a b = lift2 List.cons a b
158+
List.foldBack cons s (Ok [])
159+
160+
let inline sequenceIgnore s = sequence s |> Result.map ignore
161+
162+
let inline mapM f x = sequence (List.map f x)
163+
164+
let inline mapMIgnore f x = sequenceIgnore (List.map f x)

tests/FSharpx.Tests/ChoiceTests.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ let ChoiceFolding() =
5959
actions |> Choice.foldM folder startingPosition
6060

6161
match finalPosition with
62-
| Validation.Success (x,y) ->
62+
| Choice1Of2 (x,y) ->
6363
printfn "final position: %f,%f" x y
64-
| Validation.Failure error ->
64+
| Choice2Of2 error ->
6565
printfn "error: %s" error
6666
Assert.Fail("should not have failed: {0}", error)
6767

tests/FSharpx.Tests/FSharpx.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
<Compile Include="ListIterateeTest.fs" />
4646
<Compile Include="BinaryIterateeTest.fs" />
4747
<Compile Include="ValidationTests.fs" />
48+
<Compile Include="ValidationResultTests.fs" />
4849
<Compile Include="ValidationExample.fs" />
50+
<Compile Include="ValidationResultExample.fs" />
4951
<Compile Include="ZipListTests.fs" />
5052
<Compile Include="OptionTests.fs" />
5153
<Compile Include="ChoiceTests.fs" />

tests/FSharpx.Tests/ValidationExample.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ open FSharpx
99
open FSharpx.Functional
1010
open FSharpx.Collections
1111
open FSharpx.Choice
12+
#nowarn "FS0044"
1213
open FSharpx.Validation
1314

1415
// First let's define a domain.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
module FSharpx.Tests.ValidationResultExample
2+
3+
// ported from original in Scalaz: https://gist.github.com/970717
4+
// copy of ValidationExample adjusted for Validation.Result
5+
6+
open FSharpx.Result
7+
open NUnit.Framework
8+
open FsUnitTyped
9+
10+
open FSharpx.Collections
11+
open FSharpx.Validation.Result
12+
13+
// First let's define a domain.
14+
15+
type Sobriety = Sober | Tipsy | Drunk | Paralytic | Unconscious
16+
17+
type Gender = Male | Female
18+
19+
type Person = {
20+
Gender: Gender
21+
Age: int
22+
Clothes: string Set
23+
Sobriety: Sobriety
24+
}
25+
26+
// Let's define the checks that *all* nightclubs make!
27+
module Club =
28+
let checkAge (p: Person) =
29+
if p.Age < 18 then
30+
Error "Too young!"
31+
elif p.Age > 40 then
32+
Error "Too old!"
33+
else
34+
Ok p
35+
36+
let checkClothes (p: Person) =
37+
if p.Gender = Male && not (p.Clothes.Contains "Tie") then
38+
Error "Smarten up!"
39+
elif p.Gender = Female && p.Clothes.Contains "Trainers" then
40+
Error "Wear high heels"
41+
else
42+
Ok p
43+
44+
let checkSobriety (p: Person) =
45+
match p.Sobriety with
46+
| Drunk | Paralytic | Unconscious -> Error "Sober up!"
47+
| _ -> Ok p
48+
49+
// Now let's compose some validation checks
50+
51+
module ClubbedToDeath =
52+
open Club
53+
// PERFORM THE CHECKS USING Monadic "computation expression" SUGAR
54+
let either = ResultBuilder()
55+
let costToEnter p =
56+
either {
57+
let! a = checkAge p
58+
let! b = checkClothes a
59+
let! c = checkSobriety b
60+
return
61+
match c.Gender with
62+
| Female -> 0m
63+
| Male -> 5m
64+
}
65+
66+
// or composing functions:
67+
68+
let costToEnter2 =
69+
let costByGender (p: Person) =
70+
match p.Gender with
71+
| Female -> 0m
72+
| Male -> 5m
73+
let checkAll = checkAge >=> checkClothes >=> checkSobriety // kleisli composition
74+
checkAll >> Result.map costByGender
75+
76+
// Now let's see these in action
77+
78+
let Ken = { Person.Gender = Male; Age = 28; Clothes = set ["Tie"; "Shirt"]; Sobriety = Tipsy }
79+
let Dave = { Person.Gender = Male; Age = 41; Clothes = set ["Tie"; "Jeans"]; Sobriety = Sober }
80+
let Ruby = { Person.Gender = Female; Age = 25; Clothes = set ["High heels"]; Sobriety = Tipsy }
81+
82+
// let's go clubbing!
83+
84+
[<Test>]
85+
let part1() =
86+
ClubbedToDeath.costToEnter Dave |> shouldEqual (Error "Too old!")
87+
ClubbedToDeath.costToEnter Ken |> shouldEqual (Ok 5m)
88+
ClubbedToDeath.costToEnter Ruby |> shouldEqual (Ok 0m)
89+
ClubbedToDeath.costToEnter { Ruby with Age = 17 } |> shouldEqual (Error "Too young!")
90+
ClubbedToDeath.costToEnter { Ken with Sobriety = Unconscious } |> shouldEqual (Error "Sober up!")
91+
92+
(**
93+
* The thing to note here is how the Validations can be composed together in a computation expression.
94+
* The type system is making sure that failures flow through your computation in a safe manner.
95+
*)
96+
97+
(**
98+
* Part Two : Club Tropicana
99+
*
100+
* Part One showed monadic composition, which from the perspective of Validation is *fail-fast*.
101+
* That is, any failed check shortcircuits subsequent checks. This nicely models nightclubs in the
102+
* real world, as anyone who has dashed home for a pair of smart shoes and returned, only to be
103+
* told that your tie does not pass muster, will attest.
104+
*
105+
* But what about an ideal nightclub? One that tells you *everything* that is wrong with you.
106+
*
107+
* Applicative functors to the rescue!
108+
*
109+
*)
110+
111+
module ClubTropicana =
112+
open Club
113+
let failToList x = Result.mapError NonEmptyList.singleton x
114+
let costByGender (p: Person) =
115+
match p.Gender with
116+
| Female -> 0m
117+
| Male -> 7.5m
118+
119+
//PERFORM THE CHECKS USING applicative functors, accumulating failure via a monoid
120+
121+
let costToEnter p =
122+
costByGender <!> (checkAge p |> failToList) *> (checkClothes p |> failToList) *> (checkSobriety p |> failToList)
123+
124+
125+
// And the use? Dave tried the second nightclub after a few more drinks in the pub
126+
[<Test>]
127+
let part2() =
128+
ClubTropicana.costToEnter { Dave with Sobriety = Paralytic }
129+
|> shouldEqual (Error (NonEmptyList.create "Too old!" ["Sober up!"]))
130+
131+
ClubTropicana.costToEnter Ruby |> shouldEqual (Ok 0m)
132+
133+
(**
134+
*
135+
* So, what have we done? Well, with a *tiny change* (and no changes to the individual checks themselves),
136+
* we have completely changed the behaviour to accumulate all errors, rather than halting at the first sign
137+
* of trouble. Imagine trying to do this using exceptions, with ten checks.
138+
*
139+
*)
140+
141+
(**
142+
*
143+
* Part Three : Gay bar
144+
*
145+
* And for those wondering how to do this with a *very long list* of checks.
146+
*
147+
*)
148+
149+
module GayBar =
150+
open Club
151+
let checkGender (p: Person) =
152+
match p.Gender with
153+
| Male -> Ok p
154+
| _ -> Error "Men only"
155+
156+
let costToEnter p =
157+
[checkAge; checkClothes; checkSobriety; checkGender]
158+
|> mapM (fun check -> check p |> Result.mapError NonEmptyList.singleton)
159+
|> Result.map (function x::_ -> decimal x.Age + 1.5m | [] -> failwith "costToEnter")
160+
161+
[<Test>]
162+
let part3() =
163+
GayBar.costToEnter { Person.Gender = Male; Age = 59; Clothes = set ["Jeans"]; Sobriety = Paralytic }
164+
|> shouldEqual (Error (NonEmptyList.create "Too old!" ["Smarten up!"; "Sober up!"]))
165+
166+
GayBar.costToEnter { Person.Gender = Male; Age = 25; Clothes = set ["Tie"]; Sobriety = Sober } |> shouldEqual (Ok 26.5m)
167+

0 commit comments

Comments
 (0)