From 98e5c7423929ea4aa2d8c2d026d624ba37070ef4 Mon Sep 17 00:00:00 2001 From: Devin Lyons Date: Tue, 8 Oct 2024 10:13:07 -0800 Subject: [PATCH] Use FSharpPlus. --- .../FSharp.Data.Validation.Samples.fsproj | 2 +- .../packages.lock.json | 17 +- samples/GettingStarted/GettingStarted.fsproj | 2 +- samples/GettingStarted/packages.lock.json | 17 +- .../FSharp.Data.Validation.Giraffe.fsproj | 2 +- .../FSharp.Data.Validation.fsproj | 4 +- src/FSharp.Data.Validation/NonEmptyList.fs | 252 ------------------ src/FSharp.Data.Validation/Proof.fs | 10 +- src/FSharp.Data.Validation/VCtx.fs | 49 ++-- 9 files changed, 63 insertions(+), 292 deletions(-) delete mode 100644 src/FSharp.Data.Validation/NonEmptyList.fs diff --git a/samples/FSharp.Data.Validation.Samples/FSharp.Data.Validation.Samples.fsproj b/samples/FSharp.Data.Validation.Samples/FSharp.Data.Validation.Samples.fsproj index adf17de..b5537dc 100644 --- a/samples/FSharp.Data.Validation.Samples/FSharp.Data.Validation.Samples.fsproj +++ b/samples/FSharp.Data.Validation.Samples/FSharp.Data.Validation.Samples.fsproj @@ -16,7 +16,7 @@ - + diff --git a/samples/FSharp.Data.Validation.Samples/packages.lock.json b/samples/FSharp.Data.Validation.Samples/packages.lock.json index 224d9fa..777e69c 100644 --- a/samples/FSharp.Data.Validation.Samples/packages.lock.json +++ b/samples/FSharp.Data.Validation.Samples/packages.lock.json @@ -4,14 +4,23 @@ "net8.0": { "FSharp.Core": { "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "fbv1UwJ2LXVcFCt+GGDPu0sIYA5C6gdDvAupDj3iLQF3clRkua/6J33f+FiGQa8P1tEa+zmz3wrjoTnXZ1UiYg==" + "requested": "[6.0.6, )", + "resolved": "6.0.6", + "contentHash": "oSbTFqPtsp+EGkWAHwYIYYHyXLJRnbzVwcmM06kmXNLdWsp3P6XZViPSJJ2qUQS8feNK+nyPA0qtpNZXANGB7w==" + }, + "FSharpPlus": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "He6WVVTZpeMQNBlFHniemTSCWITKgwLIgmznNt2kw0U9G/xWSx7USdssGXd2FGmcpk2FlHInYZNwUw848tI0SQ==", + "dependencies": { + "FSharp.Core": "6.0.6" + } }, "fsharp.data.validation": { "type": "Project", "dependencies": { - "FSharp.Core": "[6.0.0, )" + "FSharp.Core": "[6.0.6, )", + "FSharpPlus": "[1.4.0, )" } } } diff --git a/samples/GettingStarted/GettingStarted.fsproj b/samples/GettingStarted/GettingStarted.fsproj index 5f216c1..119639e 100644 --- a/samples/GettingStarted/GettingStarted.fsproj +++ b/samples/GettingStarted/GettingStarted.fsproj @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/samples/GettingStarted/packages.lock.json b/samples/GettingStarted/packages.lock.json index 224d9fa..777e69c 100644 --- a/samples/GettingStarted/packages.lock.json +++ b/samples/GettingStarted/packages.lock.json @@ -4,14 +4,23 @@ "net8.0": { "FSharp.Core": { "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "fbv1UwJ2LXVcFCt+GGDPu0sIYA5C6gdDvAupDj3iLQF3clRkua/6J33f+FiGQa8P1tEa+zmz3wrjoTnXZ1UiYg==" + "requested": "[6.0.6, )", + "resolved": "6.0.6", + "contentHash": "oSbTFqPtsp+EGkWAHwYIYYHyXLJRnbzVwcmM06kmXNLdWsp3P6XZViPSJJ2qUQS8feNK+nyPA0qtpNZXANGB7w==" + }, + "FSharpPlus": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "He6WVVTZpeMQNBlFHniemTSCWITKgwLIgmznNt2kw0U9G/xWSx7USdssGXd2FGmcpk2FlHInYZNwUw848tI0SQ==", + "dependencies": { + "FSharp.Core": "6.0.6" + } }, "fsharp.data.validation": { "type": "Project", "dependencies": { - "FSharp.Core": "[6.0.0, )" + "FSharp.Core": "[6.0.6, )", + "FSharpPlus": "[1.4.0, )" } } } diff --git a/src/FSharp.Data.Validation.Giraffe/FSharp.Data.Validation.Giraffe.fsproj b/src/FSharp.Data.Validation.Giraffe/FSharp.Data.Validation.Giraffe.fsproj index 411bd9b..876ccbb 100644 --- a/src/FSharp.Data.Validation.Giraffe/FSharp.Data.Validation.Giraffe.fsproj +++ b/src/FSharp.Data.Validation.Giraffe/FSharp.Data.Validation.Giraffe.fsproj @@ -20,7 +20,7 @@ - + diff --git a/src/FSharp.Data.Validation/FSharp.Data.Validation.fsproj b/src/FSharp.Data.Validation/FSharp.Data.Validation.fsproj index 8ac6e72..ec7fdba 100644 --- a/src/FSharp.Data.Validation/FSharp.Data.Validation.fsproj +++ b/src/FSharp.Data.Validation/FSharp.Data.Validation.fsproj @@ -9,7 +9,6 @@ - @@ -31,6 +30,7 @@ - + + \ No newline at end of file diff --git a/src/FSharp.Data.Validation/NonEmptyList.fs b/src/FSharp.Data.Validation/NonEmptyList.fs deleted file mode 100644 index 4d71c6f..0000000 --- a/src/FSharp.Data.Validation/NonEmptyList.fs +++ /dev/null @@ -1,252 +0,0 @@ -namespace FSharp.Data.Validation - -type NonEmptyList<'a> = - | Single of 'a - | Cons of 'a * NonEmptyList<'a> - -[] -module NonEmptyList = - let singleton x = Single x - let cons x xs = Cons (x, xs) - let (><) y x = cons y (singleton x) - let (>-) x xs = cons x xs - - // Convert a NonEmptyList to a List - let rec toList xs = - match xs with - | Single x -> [x] - | Cons (x, xs') -> List.Cons (x, toList xs') - - // Convert a List to NonEmptyList - let rec ofList x xs = - match xs with - | [] -> Single x - | h :: t -> x >- (ofList h t) - - // Combine two NonEmptyLists, putting the first before the second - let rec append xs ys = - match xs with - | Single x -> Cons (x, ys) - | Cons (x, xs') -> Cons (x, append xs' ys) - - // Combine a List and a NonEmptyList, putting the first before the second - // Results in a NonEmptyList - let appendList xs ys = - match xs with - | [] -> ys - | h :: t -> append (ofList h t) ys - - // Combine two NonEmptyLists, putting the second before the first - let rec prepend xs ys = - match ys with - | Single y -> Cons (y, xs) - | Cons (y, ys') -> Cons (y, prepend ys' xs) - - // Combine a List and a NonEmptyList, putting the second before the first - // Results in a NonEmptyList - let prependList xs ys = - match xs with - | [] -> ys - | h :: t -> prepend (ofList h t) ys - - // Get the number of items in the NonEmptyList - let rec length xs = - match xs with - | Single _ -> 1 - | Cons (_, xs') -> 1 + length xs' - - // Get the first item in the NonEmptyList - let head xs = - match xs with - | Single x -> x - | Cons (x, _) -> x - - // Get the last item in the NonEmptyList - let rec last xs = - match xs with - | Single x -> x - | Cons (_, xs') -> last xs' - - // Get the first n items in the NonEmptyList - // If n is greater than the count, returns the original NonEmptyList - // n must be greater than zero in order to return a result - let take i xs = - if i < 1 then None - else - match List.truncate i (toList xs) with - | [] -> None // theoretically impossible - | h :: t -> Some (ofList h t) - - // Get the first items in the NonEmptyList that meet the provided criteria - let takeWhile f xs = - match List.takeWhile f (toList xs) with - | [] -> None - | h :: t -> Some (ofList h t) - - // Get all items in the NonEmptyList except for the last one - let init xs = - match xs with - | Single _ -> None - | _ -> take (length xs - 1) xs - - // Get all items in the NonEmptyList except for the first one - let tail xs = - match xs with - | Single _ -> None - | Cons (_, xs') -> Some xs' - - // Remove the first n items in the NonEmptyList - // n must be greater than zero and less than the count in order to return a result - // Technically this function can throw exceptions (List.skip), but the initial checks will avoid that - let skip i xs = - let xs' = toList xs - if i < 0 then None - elif i >= (List.length xs') then None - else - match List.skip i xs' with - | [] -> None // theoretically impossible - | h :: t -> Some (ofList h t) - - // Remove the first items in the NonEmptyList that meet the provided criteria - let skipWhile f xs = - match List.skipWhile f (toList xs) with - | [] -> None - | h :: t -> Some (ofList h t) - - // Apply the provided function to each item in the NonEmptyList - let rec map f xs = - match xs with - | Single x -> Single (f x) - | Cons (x, xs') -> Cons (f x, map f xs') - - // Apply the provided function to each element of the collection, threading an - // accumulator through the computation, returning a final result - let fold f a xs = List.fold f a (toList xs) - - // Determine if the provided item is in the NonEmptyList - let contains x xs = List.contains x (toList xs) - - // Get the provided item from the NonEmptyList if it exists - let rec find x xs = - match xs with - | Single x' -> if x = x' then (Some x') else None - | Cons (x', xs') -> if x = x' then (Some x') else find x xs' - - // Determine if any item in the NonEmptyList meets the provided criteria - let exists f xs = - match List.where f (toList xs) with - | [] -> false - | _ -> true - - // Determine if all items in the NonEmptyList meet the provided criteria - let forall f xs = List.forall f (toList xs) - - // Gets the items in the NonEmptyList that meet the provided criteria - let where f xs = - match List.where f (toList xs) with - | [] -> None - | h :: t -> Some (ofList h t) - - // Get the item at the provided index (index zero) from the NonEmptyList - // If the index is less than zero, get the item at the beginning of the NonEmptyList - // If the index is greater than the (count - 1), get the item at the end of the NonEmptyList - // Technically this function can throw exceptions (List.item), but the initial checks will avoid that - let item i xs = - let e = length xs - 1 - let i' = if i < 0 then 0 elif i > e then e else i - List.item i' (toList xs) - - // Add the provided item to the NonEmptyList at the provided index (index zero) - // If the index is less than zero, the item will go at the beginning of the NonEmptyList - // If the index is greater than the (count - 1), the item will go at the end of the NonEmptyList - // Technically this function can throw exceptions (List.insertAt), but the initial checks will avoid that - let insertAt i x xs = - let e = length xs - 1 - let i' = if i < 0 then 0 elif i > e then e else i - match List.insertAt i' x (toList xs) with - | [] -> Single x // theoretically impossible - | h :: t -> ofList h t - - // Remove the item at the provided index (index zero) from the NonEmptyList - // If the index is less than zero, the item at the beginning of the NonEmptyList will be removed - // If the index is greater than the (count - 1), the item at the end of the NonEmptyList will be removed - // If the NonEmptyList is a singleton, it will be returned as is - // Technically this function can throw exceptions (List.removeAt), but the initial checks will avoid that - let removeAt i xs = - match xs with - | Single _ -> xs - | _ -> - let e = length xs - 1 - let i' = if i < 0 then 0 elif i > e then e else i - match List.removeAt i' (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t - - // Replace the item at the provided index (index zero) in the NonEmptyList - // If the index is less than zero, the item at the beginning of the NonEmptyList will be replaced - // If the index is greater than the (count - 1), the item at the end of the NonEmptyList will be replaced - // If the NonEmptyList is a singleton, the single value will be replaced - // Technically this function can throw exceptions (List.updateAt), but the initial checks will avoid that - let updateAt i x xs = - match xs with - | Single _ -> Single x - | _ -> - let e = length xs - 1 - let i' = if i < 0 then 0 elif i > e then e else i - match List.updateAt i' x (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t - - // Split the NonEmptyList into two NonEmptyLists, the first of which the provided constraint is true and the second is false - // No result signifies that all the items in the NonEmptyList meet the constraint in the same way - let partition f xs = - match List.partition f (toList xs) with - | ([], _) -> None - | (_, []) -> None - | (h :: t, h' :: t') -> Some (ofList h t, ofList h' t') - - // Split the NonEmptyList at the provided index (index zero) - // If the index is less than zero, the item at the beginning of the NonEmptyList will become a singleton - // If the index is greater than the (count - 1), the item at the end of the NonEmptyList will become a singleton - // No result signifies that the NonEmptyList is a singleton - // Technically this function can throw exceptions (List.splitAt), but the initial checks will avoid that - let splitAt i xs = - match xs with - | Single _ -> None - | _ -> - let e = length xs - 1 - let i' = if i < 0 then 0 elif i > e then e else i - match List.splitAt i' (toList xs) with - | ([], _) -> None // theoretically impossible - | (_, []) -> None // theoretically impossible - | (h :: t, h' :: t') -> Some (ofList h t, ofList h' t') - - // Get the items in the NonEmptyList in reverse order - // Technically this function can throw exceptions (List.head, List.tail), but the source NonEmptyList will avoid that - let reverse xs = - let xs' = toList xs |> List.rev - ofList (List.head xs') (List.tail xs') - - // Sort the NonEmptyList with Operators.compare - let sort xs = - match List.sort (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t - - // Sort the NonEmptyList with the keys given in the projection - let sortBy f xs = - match List.sortBy f (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t - - // Removes all duplicate items in the NonEmptyList - let distinct xs = - match List.distinct (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t - - // Removes all duplicate items in the NonEmptyList, determined by the provided function - let distinctBy f xs = - match List.distinctBy f (toList xs) with - | [] -> xs // theoretically impossible - | h :: t -> ofList h t diff --git a/src/FSharp.Data.Validation/Proof.fs b/src/FSharp.Data.Validation/Proof.fs index b0a016f..17d80a1 100644 --- a/src/FSharp.Data.Validation/Proof.fs +++ b/src/FSharp.Data.Validation/Proof.fs @@ -5,7 +5,7 @@ open System.Text.Json open System.Text.Json.Serialization [)>] -type ValidationFailures<'F> = +type ValidationFailures<'F> = { Failures: 'F list Fields: FailureMap<'F> } and ValidationFailuresConverter<'F>() = @@ -23,7 +23,8 @@ and ValidationFailuresConverter<'F>() = | 0 -> str | 1 -> str.ToLower() | _ -> sprintf "%c%s" (Char.ToLowerInvariant(str[0])) (str.Substring(1)) - override this.Read(reader: byref, typ, opts) = base.Read(&reader, typ, opts) + override this.Read(reader: byref, typ, opts) = + JsonSerializer.Deserialize>(&reader, opts) override this.Write(writer, fs, opts) = writer.WriteStartObject() @@ -70,7 +71,8 @@ and ProofConverter<'F, 'A>() = | 0 -> str | 1 -> str.ToLower() | _ -> sprintf "%c%s" (Char.ToLowerInvariant(str[0])) (str.Substring(1)) - override this.Read(reader: byref, typ, opts) = base.Read(&reader, typ, opts) + override this.Read(reader: byref, typ, opts) = + JsonSerializer.Deserialize>(&reader, opts) override this.Write(writer, proof, opts) = match proof with | Valid a -> JsonSerializer.Serialize(writer, a, opts) @@ -109,7 +111,7 @@ module Proof = match p2 with | Valid _ -> Invalid (gfs, lfs) | Invalid (gfs', lfs') -> Invalid (gfs @ gfs', Utilities.mergeFailures lfs lfs') - + let toValidationFailures p = match p with | Valid a -> None diff --git a/src/FSharp.Data.Validation/VCtx.fs b/src/FSharp.Data.Validation/VCtx.fs index 0f7754e..cb9a90c 100644 --- a/src/FSharp.Data.Validation/VCtx.fs +++ b/src/FSharp.Data.Validation/VCtx.fs @@ -1,6 +1,9 @@ namespace FSharp.Data.Validation -open System.Linq.Expressions + open System +open System.Linq.Expressions + +open FSharpPlus.Data type VCtx<'F, 'A> = internal @@ -83,7 +86,7 @@ type VCtxBuilder() = match mn with | None -> this.WithValue(c, b) | Some n -> this.WithField(c, n, b) - + /// Performs some given validation using a 'Field' from a given selector. [] member this.WithField(c:VCtx<'F, 'A>, selector:Expression>) = @@ -91,7 +94,7 @@ type VCtxBuilder() = let mn = mkName exp.Member.Name let v = selector.Compile().Invoke() this.WithField(c, mn, v) - + /// Performs some given validation using a 'Field' from a given selector and value. [] member this.WithField(c:VCtx<'F, 'A>, selector:Expression>, b:'B) = @@ -134,40 +137,40 @@ type VCtxBuilder() = member this.ValidateEach(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> VCtx<'F, ValueCtx<'B>>): VCtx<'F, ValueCtx>> = this.Bind(c, fun v1 -> let xs = ValueCtx.getValue v1 - let ys = xs |> Seq.mapi (fun i x -> + let ys = xs |> Seq.mapi (fun i x -> match fn i x with | ValidCtx v2 -> ValidCtx (Element (i, (ValueCtx.getValue v2))) - | DisputedCtx (gfs,lfs,v2) -> + | DisputedCtx (gfs,lfs,v2) -> let v2' = Element (i, ValueCtx.getValue v2) let gfs',lfs' = VCtx.applyFailures v2' (List.empty, Map.empty) (gfs, lfs) DisputedCtx (gfs', lfs', v2') - | RefutedCtx (gfs,lfs) -> + | RefutedCtx (gfs,lfs) -> let v2' = Element (i, ()) let gfs',lfs' = VCtx.applyFailures v2' (List.empty, Map.empty) (gfs, lfs) RefutedCtx (gfs', lfs') ) let appendToCtx d d' = d |> ValueCtx.map (fun zs -> Seq.append zs [ValueCtx.getValue d']) - (ValidCtx (ValueCtx.setValue v1 Seq.empty), ys) ||> Seq.fold (fun acc x -> + (ValidCtx (ValueCtx.setValue v1 Seq.empty), ys) ||> Seq.fold (fun acc x -> match (acc, x) with - | ValidCtx a, ValidCtx b -> + | ValidCtx a, ValidCtx b -> ValidCtx (appendToCtx a b) - | ValidCtx a, DisputedCtx (gfs',lfs',b) -> + | ValidCtx a, DisputedCtx (gfs',lfs',b) -> let gfs2,lfs2 = VCtx.applyFailures v1 (List.empty, Map.empty) (gfs',lfs') DisputedCtx (gfs2,lfs2,appendToCtx a b) - | ValidCtx a, RefutedCtx (gfs',lfs') -> + | ValidCtx a, RefutedCtx (gfs',lfs') -> RefutedCtx (VCtx.applyFailures v1 (List.empty, Map.empty) (gfs',lfs')) - | DisputedCtx (gfs,lfs,a), ValidCtx b -> + | DisputedCtx (gfs,lfs,a), ValidCtx b -> DisputedCtx (gfs,lfs,appendToCtx a b) - | DisputedCtx (gfs,lfs,a), DisputedCtx (gfs',lfs',b) -> + | DisputedCtx (gfs,lfs,a), DisputedCtx (gfs',lfs',b) -> let gfs2,lfs2 = VCtx.applyFailures v1 (gfs,lfs) (gfs',lfs') DisputedCtx (gfs2,lfs2,appendToCtx a b) - | DisputedCtx (gfs,lfs,_), RefutedCtx (gfs',lfs') -> + | DisputedCtx (gfs,lfs,_), RefutedCtx (gfs',lfs') -> RefutedCtx (VCtx.applyFailures v1 (gfs,lfs) (gfs',lfs')) - | RefutedCtx (gfs,lfs), ValidCtx _ -> + | RefutedCtx (gfs,lfs), ValidCtx _ -> RefutedCtx (gfs,lfs) - | RefutedCtx (gfs,lfs), DisputedCtx (gfs',lfs',b) -> + | RefutedCtx (gfs,lfs), DisputedCtx (gfs',lfs',b) -> RefutedCtx (VCtx.applyFailures v1 (gfs,lfs) (gfs',lfs')) - | RefutedCtx (gfs,lfs), RefutedCtx (gfs',lfs') -> + | RefutedCtx (gfs,lfs), RefutedCtx (gfs',lfs') -> RefutedCtx (VCtx.applyFailures v1 (gfs,lfs) (gfs',lfs')) ) ) @@ -231,7 +234,7 @@ type VCtxBuilder() = /// If the result of all elements are `Ok b`, validation continues with the new value. [] member this.RefuteEachWith(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> Result<'B, 'F>): VCtx<'F, ValueCtx>> = - this.ValidateEach(c, fun i a -> + this.ValidateEach(c, fun i a -> match fn i a with | Ok b -> ValidCtx (Global b) | Error f -> RefutedCtx ([f], Map.empty) @@ -270,7 +273,7 @@ type VCtxBuilder() = /// If the result of all elements are `Valid b`, validation continues with the new value. [] member this.RefuteEachWithProof(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> Proof<'F, 'B>): VCtx<'F, ValueCtx>> = - this.ValidateEach(c, fun i a -> + this.ValidateEach(c, fun i a -> match fn i a with | Valid b -> ValidCtx (Global b) | Invalid (gfs,lfs) -> RefutedCtx (gfs,lfs) @@ -319,7 +322,7 @@ type VCtxBuilder() = this.Bind(c, fun v -> match fn (ValueCtx.getValue v) with | [] -> this.Return(v) - | h :: t -> this.DisputeMany(v, NonEmptyList.ofList h t) + | xs -> this.DisputeMany(v, NonEmptyList.ofList xs) ) /// Performs a validation on each member of a list using a given function and handles the result. @@ -327,7 +330,7 @@ type VCtxBuilder() = /// Otherwise, validation continues normally. [] member this.DisputeAnyWith(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> 'F option): VCtx<'F, ValueCtx>> = - this.DisputeAnyWithMany(c, fun i a -> + this.DisputeAnyWithMany(c, fun i a -> match fn i a with | None -> [] | Some f -> [f] @@ -345,7 +348,7 @@ type VCtxBuilder() = /// Otherwise, validation continues normally. [] member this.DisputeAnyWithMany(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> 'F list): VCtx<'F, ValueCtx>> = - this.ValidateEach(c, fun i a -> + this.ValidateEach(c, fun i a -> match fn i a with | [] -> ValidCtx (Global a) | fs -> DisputedCtx (fs,Map.empty,Global a) @@ -363,7 +366,7 @@ type VCtxBuilder() = /// Otherwise, no failures are added and validation continues normally. [] member this.DisputeAllWith(c:VCtx<'F, ValueCtx<#seq<'A>>>, fn:int -> 'A -> 'F option): VCtx<'F, ValueCtx<#seq<'A>>> = - this.DisputeAllWithMany(c, fun i a -> + this.DisputeAllWithMany(c, fun i a -> match fn i a with | None -> [] | Some f -> [f] @@ -412,7 +415,7 @@ type VCtxBuilder() = /// Otherwise, validation continues normally. [] member this.DisputeAnyWithFact(c:VCtx<'F, ValueCtx<#seq<'A>>>, f:'F, fn:int -> 'A -> bool): VCtx<'F, ValueCtx>> = - this.DisputeAnyWith(c, fun i a -> + this.DisputeAnyWith(c, fun i a -> match fn i a with | true -> None | false -> Some f