Last active
January 31, 2025 12:40
-
-
Save OnurGumus/3dba435729cf76fbfadb1b826a470085 to your computer and use it in GitHub Desktop.
Validates Lens 3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Aether | |
open System | |
// Optics | |
/// Lens from 'a -> 'b. | |
type Lens<'a,'b> = | |
('a -> 'b) * ('b -> 'a -> 'a) | |
/// Prism from 'a -> 'b. | |
type Prism<'a,'b> = | |
('a -> 'b option) * ('b -> 'a -> 'a) | |
// Morphisms | |
/// Isomorphism between 'a <> 'b. | |
type Isomorphism<'a,'b> = | |
('a -> 'b) * ('b -> 'a) | |
/// Epimorphism between 'a <> 'b. | |
type Epimorphism<'a,'b> = | |
('a -> 'b option) * ('b -> 'a) | |
/// Validated Lens from 'a -> 'b with error type 'e. | |
type ValidatedLens<'a,'b,'e> = | |
('a -> 'b) * ('b -> 'a -> Result<'a,'e>) | |
/// Validated Prism from 'a -> 'b with error type 'e. | |
type ValidatedPrism<'a,'b,'e> = | |
('a -> 'b option) * ('b -> 'a -> Result<'a,'e>) | |
[<RequireQualifiedAccess>] | |
module Compose = | |
/// Static overloads of the composition function for lenses (>->). | |
type Lens = | |
| Lens with | |
static member (>->) (Lens, (g2, s2): Lens<'b,'c>) = | |
fun ((g1, s1): Lens<'a,'b>) -> | |
(fun a -> g2 (g1 a)), | |
(fun c a -> s1 (s2 c (g1 a)) a) : Lens<'a,'c> | |
static member (>->) (Lens, (g2, s2): Prism<'b,'c>) = | |
fun ((g1, s1): Lens<'a,'b>) -> | |
(fun a -> g2 (g1 a)), | |
(fun c a -> s1 (s2 c (g1 a)) a) : Prism<'a,'c> | |
static member (>->) (Lens, (f, t): Isomorphism<'b,'c>) = | |
fun ((g, s): Lens<'a,'b>) -> | |
(fun a -> f (g a)), | |
(fun c a -> s (t c) a) : Lens<'a,'c> | |
static member (>->) (Lens, (f, t): Epimorphism<'b,'c>) = | |
fun ((g, s): Lens<'a,'b>) -> | |
(fun a -> f (g a)), | |
(fun c a -> s (t c) a) : Prism<'a,'c> | |
static member (>->) (Lens, validatedLens: ValidatedLens<'b,'c,'e>) = | |
fun (vLens: ValidatedLens<'a,'b,'e>) -> | |
let (g1, s1) = vLens | |
let (g2, s2) = validatedLens | |
let get a = g2 (g1 a) | |
let set c a = | |
s2 c (g1 a) | |
|> Result.bind (fun b' -> s1 b' a) | |
(get, set) : ValidatedLens<'a,'c,'e> | |
/// Compose a lens with an optic or morphism. | |
let inline lens l o = | |
(Lens >-> o) l | |
/// Static overloads of the composition function for prisms (>?>). | |
type Prism = | |
| Prism with | |
static member (>?>) (Prism, (g2, s2): Lens<'b,'c>) = | |
fun ((g1, s1): Prism<'a,'b>) -> | |
(fun a -> Option.map g2 (g1 a)), | |
(fun c a -> | |
Option.map (s2 c) (g1 a) | |
|> function | |
| Some b -> s1 b a | |
| _ -> a | |
) : Prism<'a,'c> | |
static member (>?>) (Prism, (g2, s2): Prism<'b,'c>) = | |
fun ((g1, s1): Prism<'a,'b>) -> | |
(fun a -> Option.bind g2 (g1 a)), | |
(fun c a -> | |
Option.map (s2 c) (g1 a) | |
|> function | |
| Some b -> s1 b a | |
| _ -> a | |
) : Prism<'a,'c> | |
static member (>?>) (Prism, (f, t): Isomorphism<'b,'c>) = | |
fun ((g, s): Prism<'a,'b>) -> | |
(fun a -> Option.map f (g a)), | |
(fun c a -> s (t c) a) : Prism<'a,'c> | |
static member (>?>) (Prism, (f, t): Epimorphism<'b,'c>) = | |
fun ((g, s): Prism<'a,'b>) -> | |
(fun a -> Option.bind f (g a)), | |
(fun c a -> s (t c) a) : Prism<'a,'c> | |
static member (>?>) (Prism, validatedPrism: ValidatedPrism<'b,'c,'e>) = | |
fun (prism: Prism<'a,'b>) -> | |
let (g1, s1) = prism | |
let (g2, s2) = validatedPrism | |
let get a = | |
Option.bind g2 (g1 a) | |
let set c a = | |
match g1 a with | |
| Some b -> | |
s2 c b | |
|> Result.map (fun b' -> s1 b' a) | |
| None -> | |
// Return an error indicating the path is invalid. | |
Error (unbox<'e> "Invalid path") | |
(get, set) : ValidatedPrism<'a,'c,'e> | |
/// Compose a prism with an optic or morphism. | |
let inline prism p o = | |
(Prism >?> o) p | |
module ValidatedLens = | |
/// Compose two ValidatedLens instances. | |
let compose (lens1: ValidatedLens<'a,'b,'e>) (lens2: ValidatedLens<'b,'c,'e>) : ValidatedLens<'a,'c,'e> = | |
let (get1, set1) = lens1 | |
let (get2, set2) = lens2 | |
let get a = get2 (get1 a) | |
let set c a = | |
set2 c (get1 a) | |
|> Result.bind (fun b' -> set1 b' a) | |
(get, set) | |
/// Compose a ValidatedLens with a standard Lens. | |
let composeValidatedLens (vLens: ValidatedLens<'a,'b,'e>) (lens: Lens<'b,'c>) : ValidatedLens<'a,'c,'e> = | |
let (get1, set1) = vLens | |
let (get2, set2) = lens | |
let get a = get2 (get1 a) | |
let set c a = | |
let b = get1 a | |
let b' = set2 c b | |
set1 b' a | |
(get, set) | |
/// Compose a standard Lens with a ValidatedLens. | |
let composeLensValidated (lens: Lens<'a,'b>) (vLens: ValidatedLens<'b,'c,'e>) : ValidatedLens<'a,'c,'e> = | |
let (get1, set1) = lens | |
let (get2, set2) = vLens | |
let get a = get2 (get1 a) | |
let set c a = | |
set2 c (get1 a) | |
|> Result.map (fun b' -> set1 b' a) | |
(get, set) | |
let composeMapErrors | |
(lens1: ValidatedLens<'a,'b,'e1>) | |
(lens2: ValidatedLens<'b,'c,'e2>) | |
(mapError1: 'e1 -> string) | |
(mapError2: 'e2 -> string) | |
: ValidatedLens<'a,'c,string> = | |
let (get1, set1) = lens1 | |
let (get2, set2) = lens2 | |
let get a = get2 (get1 a) | |
let set c a = | |
match set2 c (get1 a) with | |
| Ok b' -> | |
match set1 b' a with | |
| Ok a' -> Ok a' | |
| Error e1 -> Error (mapError1 e1) | |
| Error e2 -> Error (mapError2 e2) | |
(get, set) | |
let composeLensPrismMapErrors | |
(lens: ValidatedLens<'a,'b,'e1>) | |
(prism: ValidatedPrism<'b,'c,'e2>) | |
(mapError1: 'e1 -> 'e) | |
(mapError2: 'e2 -> 'e) | |
: ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = lens | |
let (get2, set2) = prism | |
let get a = | |
get2 (get1 a) | |
let set c a = | |
match set2 c (get1 a) with | |
| Ok b' -> | |
match set1 b' a with | |
| Ok a' -> Ok a' | |
| Error e1 -> Error (mapError1 e1) | |
| Error e2 -> Error (mapError2 e2) | |
(get, set) | |
/// Maps the error of a ValidatedLens using the provided function. | |
let mapError | |
(f: 'e1 -> 'e2) | |
(lens: ValidatedLens<'a,'b,'e1>) | |
: ValidatedLens<'a,'b,'e2> = | |
let (get, set) = lens | |
let set' b a = | |
set b a |> Result.mapError f | |
(get, set') | |
module ValidatedPrism = | |
/// Compose two ValidatedPrism instances. | |
let compose (prism1: ValidatedPrism<'a,'b,'e>) (prism2: ValidatedPrism<'b,'c,'e>) : ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = prism1 | |
let (get2, set2) = prism2 | |
let get a = | |
Option.bind get2 (get1 a) | |
let set c a = | |
match get1 a with | |
| Some b -> | |
set2 c b | |
|> Result.bind (fun b' -> set1 b' a) | |
| None -> | |
// Return an error indicating the path is invalid. | |
Error (unbox<'e> "Invalid path") | |
(get, set) | |
/// Compose a ValidatedPrism with a standard Prism. | |
let composeValidatedPrism (vPrism: ValidatedPrism<'a,'b,'e>) (prism: Prism<'b,'c>) : ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = vPrism | |
let (get2, set2) = prism | |
let get a = | |
Option.bind get2 (get1 a) | |
let set c a = | |
match get1 a with | |
| Some b -> | |
let b' = set2 c b | |
set1 b' a | |
| None -> | |
// Return an error indicating the path is invalid. | |
Error (unbox<'e> "Invalid path") | |
(get, set) | |
/// Compose a standard Prism with a ValidatedPrism. | |
let composePrismValidated (prism: Prism<'a,'b>) (vPrism: ValidatedPrism<'b,'c,'e>) : ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = prism | |
let (get2, set2) = vPrism | |
let get a = | |
Option.bind get2 (get1 a) | |
let set c a = | |
match get1 a with | |
| Some b -> | |
set2 c b | |
|> Result.map (fun b' -> set1 b' a) | |
| None -> | |
// Return an error indicating the path is invalid. | |
Error (unbox<'e> "Invalid path") | |
(get, set) | |
let composeMapErrors | |
(prism1: ValidatedPrism<'a,'b,'e1>) | |
(prism2: ValidatedPrism<'b,'c,'e2>) | |
(mapError1: 'e1 -> 'e) | |
(mapError2: 'e2 -> 'e) | |
: ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = prism1 | |
let (get2, set2) = prism2 | |
let get a = | |
match get1 a with | |
| Some b -> | |
match get2 b with | |
| Some c -> Some c | |
| None -> None | |
| None -> None | |
let set c a = | |
match get1 a with | |
| Some b -> | |
match set2 c b with | |
| Ok b' -> | |
match set1 b' a with | |
| Ok a' -> Ok a' | |
| Error e1 -> Error (mapError1 e1) | |
| Error e2 -> Error (mapError2 e2) | |
| None -> | |
// Return an error indicating the path is invalid. | |
Error (unbox<'e> "Invalid path") | |
(get, set) | |
let composePrismLensMapErrors | |
(prism: ValidatedPrism<'a,'b,'e1>) | |
(lens: ValidatedLens<'b,'c,'e2>) | |
(mapError1: 'e1 -> 'e) | |
(mapError2: 'e2 -> 'e) | |
: ValidatedPrism<'a,'c,'e> = | |
let (get1, set1) = prism | |
let (get2, set2) = lens | |
let get a = | |
match get1 a with | |
| Some b -> Some (get2 b) | |
| None -> None | |
let set c a = | |
match get1 a with | |
| Some b -> | |
match set2 c b with | |
| Ok b' -> | |
match set1 b' a with | |
| Ok a' -> Ok a' | |
| Error e1 -> Error (mapError1 e1) | |
| Error e2 -> Error (mapError2 e2) | |
| None -> | |
Error (unbox<'e> "Invalid path") | |
(get, set) | |
let mapError | |
(f: 'e1 -> 'e2) | |
(prism: ValidatedPrism<'a,'b,'e1>) | |
: ValidatedPrism<'a,'b,'e2> = | |
let (get, set) = prism | |
let set' b a = | |
set b a |> Result.mapError f | |
(get, set') | |
[<RequireQualifiedAccess>] | |
module Optic = | |
/// Static overloads of the optic get function (^.). | |
type Get = | |
| Get with | |
static member (^.) (Get, (g, _): Lens<'a,'b>) = | |
fun (a: 'a) -> | |
g a : 'b | |
static member (^.) (Get, (g, _): Prism<'a,'b>) = | |
fun (a: 'a) -> | |
g a : 'b option | |
static member (^.) (Get, (g, _): ValidatedLens<'a,'b,'e>) = | |
fun (a: 'a) -> | |
g a : 'b | |
static member (^.) (Get, (g, _): ValidatedPrism<'a,'b,'e>) = | |
fun (a: 'a) -> | |
g a : 'b option | |
/// Get a value using an optic. | |
let inline get optic target = | |
(Get ^. optic) target | |
/// Static overloads of the optic set function (^=). | |
type Set = | |
| Set with | |
static member (^=) (Set, (_, s): Lens<'a,'b>) = | |
fun (b: 'b) -> | |
s b : 'a -> 'a | |
static member (^=) (Set, (_, s): Prism<'a,'b>) = | |
fun (b: 'b) -> | |
s b : 'a -> 'a | |
static member (^=) (Set, (_, s): ValidatedLens<'a,'b,'e>) = | |
fun (b: 'b) -> | |
s b : 'a -> Result<'a,'e> | |
static member (^=) (Set, (_, s): ValidatedPrism<'a,'b,'e>) = | |
fun (b: 'b) -> | |
s b : 'a -> Result<'a,'e> | |
/// Set a value using an optic. | |
let inline set optic value = | |
(Set ^= optic) value | |
/// Static overloads of the optic map function (%=). | |
type Map = | |
| Map with | |
static member (^%) (Map, (g, s): Lens<'a,'b>) = | |
fun (f: 'b -> 'b) -> | |
(fun a -> s (f (g a)) a) : 'a -> 'a | |
static member (^%) (Map, (g, s): Prism<'a,'b>) = | |
fun (f: 'b -> 'b) -> | |
(fun a -> Option.map f (g a) |> function | Some b -> s b a | |
| _ -> a) : 'a -> 'a | |
static member (^%) (Map, (g, s): ValidatedLens<'a,'b,'e>) = | |
fun (f: 'b -> 'b) -> | |
(fun a -> | |
s (f (g a)) a | |
) : 'a -> Result<'a,'e> | |
static member (^%) (Map, (g, s): ValidatedPrism<'a,'b,'e>) = | |
fun (f: 'b -> 'b) -> | |
(fun a -> | |
match g a with | |
| Some b -> s (f b) a | |
| None -> Ok a | |
) : 'a -> Result<'a,'e> | |
/// Modify a value using an optic. | |
let inline map optic f = | |
(Map ^% optic) f | |
/// Functions for creating or using lenses. | |
[<RequireQualifiedAccess>] | |
module Lens = | |
/// Converts an isomorphism into a lens. | |
let ofIsomorphism ((f, t): Isomorphism<'a,'b>) : Lens<'a,'b> = | |
f, (fun b _ -> t b) | |
/// Lift a standard Lens into a ValidatedLens with no validation. | |
let toValidated (lens: Lens<'a,'b>) : ValidatedLens<'a,'b,'e> = | |
let (get, set) = lens | |
get, (fun b a -> Ok (set b a)) | |
/// Functions for creating or using prisms. | |
[<RequireQualifiedAccess>] | |
module Prism = | |
/// Converts an epimorphism into a prism. | |
let ofEpimorphism ((f, t): Epimorphism<'a,'b>) : Prism<'a,'b> = | |
f, (fun b _ -> t b) | |
/// Lift a standard Prism into a ValidatedPrism with no validation. | |
let toValidated (prism: Prism<'a,'b>) : ValidatedPrism<'a,'b,'e> = | |
let (get, set) = prism | |
get, (fun b a -> Ok (set b a)) | |
[<AutoOpen>] | |
module Optics = | |
let id_ : Lens<'a,'a> = | |
(fun x -> x), | |
(fun x _ -> x) | |
let fst_ : Lens<('a * 'b),'a> = | |
fst, | |
(fun a t -> a, snd t) | |
let snd_ : Lens<('a * 'b),'b> = | |
snd, | |
(fun b t -> fst t, b) | |
/// Operators for working with optics, including validated optics. | |
module Operators = | |
/// Compose a lens with an optic or morphism. | |
let inline (>->) l o = | |
Compose.lens l o | |
/// Compose a prism with an optic or morphism. | |
let inline (>?>) p o = | |
Compose.prism p o | |
/// Get a value using an optic. | |
let inline (^.) target optic = | |
Optic.get optic target | |
/// Set a value using an optic. | |
let inline (^=) value optic = | |
Optic.set optic value | |
/// Modify a value using an optic. | |
let inline (^%) f optic = | |
Optic.map optic f | |
open Operators | |
// Define the types. | |
type ShortString = ShortString of string | |
type Address = { Street: ShortString } | |
type Person = Person of {| HomeAddress: Address; FirstName: ShortString; LastName : ShortString |} | |
let resultOrValue r = | |
match r with | |
| Ok x -> x | |
| _ -> failwith "validation" | |
module ShortString = | |
// ValidatedLens for ShortString.Value_. | |
let value_ : ValidatedLens<ShortString, string, string> = | |
(fun (ShortString s) -> s), | |
(fun s _ -> | |
if s.Length <= 10 then Ok (ShortString s) | |
else Error "String length must be 10 characters or less.") | |
module Person = | |
// Lens for Person.HomeAddress. | |
let homeAddress_ : Lens<Person, Address> = | |
(fun (Person p) -> p.HomeAddress), | |
(fun addr (Person p) -> Person {| p with HomeAddress = addr |}) | |
let value_ : Lens<Person,{|Address: string; FirstName: string; LastName : string|}> = | |
(fun (Person p) -> | |
{| | |
Address = p.HomeAddress.Street ^. (ShortString.value_) ; | |
FirstName = p.FirstName ^. (ShortString.value_) ; | |
LastName = p.LastName ^. (ShortString.value_) ; | |
|}), | |
(fun data p -> | |
let firstName = | |
data.FirstName ^= ShortString.value_ <| Unchecked.defaultof<_> |> resultOrValue | |
let lastName = data.LastName ^= ShortString.value_ <| Unchecked.defaultof<_> |> resultOrValue | |
let street = data.Address ^= ShortString.value_ <| Unchecked.defaultof<_> |> resultOrValue | |
Person({| HomeAddress = {Street = street}; FirstName = firstName; LastName = lastName |})) | |
module Address = | |
// ValidatedLens for Address.Street. | |
let street_ : ValidatedLens<Address, ShortString,_> = | |
(fun addr -> addr.Street), | |
(fun street addr -> | |
// Include validation logic if needed. | |
// For example, ensure the street is not empty. | |
//let street2 = street ^= ShortString.value_ | |
let streetV= street ^. (ShortString.value_) | |
if streetV.Length < 200 then | |
Ok({ addr with Street = street }) | |
else | |
Error "1" | |
) | |
// Create an instance of Person. | |
let person :Person = {| Address ="21st jump"; FirstName = "Dennis "; LastName ="Booker" |} ^= Person.value_ <| Unchecked.defaultof<_> | |
//{ HomeAddress = { Street = ShortString "Old St" } } | |
// Compose the ValidatedLenses using the >-> operator. | |
let homeAddressValidated = Lens.toValidated Person.homeAddress_ | |
let x = (homeAddressValidated >-> Address.street_ >-> ShortString.value_) | |
// Successful update. | |
let result1 = | |
"New Street" | |
^= (homeAddressValidated >-> Address.street_ >-> ShortString.value_) | |
<| person | |
match result1 with | |
| Ok updatedPerson -> | |
let personValue = (updatedPerson ^. (Person.value_)) | |
printfn "Updated person: %A %A %A" personValue.FirstName personValue.LastName personValue.Address | |
| Error errMsg -> | |
printfn "Error: %s" errMsg | |
// Attempt to set an invalid street name (too long). | |
let result2 = Optic.set (homeAddressValidated >-> Address.street_ >-> ShortString.value_) "A Very Long Street Name" person | |
let resultR = person ^. (homeAddressValidated >-> Address.street_ >-> ShortString.value_) | |
let personStreetValueLens = homeAddressValidated >-> Address.street_ >-> ShortString.value_ | |
match result2 with | |
| Ok updatedPerson -> | |
printfn "Updated person: %A" updatedPerson | |
| Error errMsg -> | |
printfn "Error: %s" errMsg | |
// Attempt to set an invalid street name (empty string). | |
let result3 = Optic.set personStreetValueLens "" person | |
match result3 with | |
| Ok updatedPerson -> | |
printfn "Updated person: %A" updatedPerson | |
| Error errMsg -> | |
printfn "Error: %s" errMsg | |
let composedLens = Compose.ValidatedLens.composeMapErrors | |
(Lens.toValidated Person.homeAddress_) | |
( Address.street_) | |
(fun e1 -> sprintf "Library A error: %A" e1) | |
(fun e2 -> sprintf "Library B error: %A" e2) | |
let vcomposed = Compose.ValidatedLens.mapError (fun x -> 1 ) composedLens | |
let composedPerson = (person ^. (Person.homeAddress_)).Street ^= composedLens <| person | |
printf "composed %A" composedPerson |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment