Skip to content

Instantly share code, notes, and snippets.

@OnurGumus
Last active February 1, 2025 09:30
Show Gist options
  • Save OnurGumus/0dd61a29c2a90b9c3b4e81a60426c785 to your computer and use it in GitHub Desktop.
Save OnurGumus/0dd61a29c2a90b9c3b4e81a60426c785 to your computer and use it in GitHub Desktop.
AsyyncValidatedLens
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)
/// Isomorphism between 'a and 'b.
type Isomorphism<'a,'b> =
('a -> 'b) * ('b -> 'a)
/// Epimorphism between 'a and '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>)
/// Async Validated Lens from 'a -> 'b with error type 'e.
/// The getter remains synchronous while the setter returns an asynchronous Result.
type AsyncValidatedLens<'a,'b,'e> =
('a -> 'b) * ('b -> 'a -> Async<Result<'a,'e>>)
// ----------------------------------------------------------------------------
// Compose Module (with AsyncValidatedLens and ValidatedLens support)
// ----------------------------------------------------------------------------
[<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 AsyncValidatedLens.
static member (>->) (Lens, asyncValidatedLens: AsyncValidatedLens<'b,'c,'e>) =
fun (aLens: AsyncValidatedLens<'a,'b,'e>) ->
let (g1, s1) = aLens
let (g2, s2) = asyncValidatedLens
let get a = g2 (g1 a)
let set c a = async {
let! res = s2 c (g1 a)
match res with
| Ok b' -> return! s1 b' a
| Error e -> return Error e
}
(get, set) : AsyncValidatedLens<'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
// ------------------------------------------------------------------------
// AsyncValidatedLens composition and helpers
// ------------------------------------------------------------------------
[<RequireQualifiedAccess>]
module AsyncValidatedLens =
/// Compose two AsyncValidatedLens instances.
let compose (lens1: AsyncValidatedLens<'a,'b,'e>) (lens2: AsyncValidatedLens<'b,'c,'e>) : AsyncValidatedLens<'a,'c,'e> =
let (get1, set1) = lens1
let (get2, set2) = lens2
let get a = get2 (get1 a)
let set c a = async {
let! res = set2 c (get1 a)
match res with
| Ok b' -> return! set1 b' a
| Error e -> return Error e
}
(get, set)
/// Compose an AsyncValidatedLens with a standard Lens.
let composeAsyncValidatedLens (vLens: AsyncValidatedLens<'a,'b,'e>) (lens: Lens<'b,'c>) : AsyncValidatedLens<'a,'c,'e> =
let (get1, set1) = vLens
let (get2, set2) = lens
let get a = get2 (get1 a)
let set c a = async {
let b = get1 a
let b' = set2 c b
return! set1 b' a
}
(get, set)
/// Compose a standard Lens with an AsyncValidatedLens.
let composeLensAsyncValidated (lens: Lens<'a,'b>) (vLens: AsyncValidatedLens<'b,'c,'e>) : AsyncValidatedLens<'a,'c,'e> =
let (get1, set1) = lens // get1 : 'a -> 'b, set1 : 'b -> 'a -> 'a
let (get2, set2) = vLens // get2 : 'b -> 'c, set2 : 'c -> 'b -> Async<Result<'b, 'e>>
let get a = get2 (get1 a) // get : 'a -> 'c
let set c a = async {
let b = get1 a // extract the inner value from a
let! res = set2 c b // update the inner value asynchronously
match res with
| Ok b' -> return Ok (set1 b' a) // use set1 to reconstruct the outer structure
| Error e -> return Error e
}
(get, set)
/// Map the error of an AsyncValidatedLens using the provided function.
let mapError (f: 'e1 -> 'e2) (lens: AsyncValidatedLens<'a,'b,'e1>) : AsyncValidatedLens<'a,'b,'e2> =
let (get, set) = lens
let set' b a = async {
let! res = set b a
return Result.mapError f res
}
(get, set')
// ------------------------------------------------------------------------
// ValidatedLens composition and helpers
// ------------------------------------------------------------------------
[<RequireQualifiedAccess>]
module ValidatedLens =
/// Compose two ValidatedLens instances while mapping errors.
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)
/// Map 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')
// ----------------------------------------------------------------------------
// Optic Operators
// ----------------------------------------------------------------------------
[<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
static member (^.) (Get, (g, _): AsyncValidatedLens<'a,'b,'e>) =
fun (a: 'a) -> g a : 'b
/// 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>
static member (^=) (Set, (_, s): AsyncValidatedLens<'a,'b,'e>) =
fun (b: 'b) -> s b : 'a -> Async<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>
static member (^%) (Map, (g, s): AsyncValidatedLens<'a,'b,'e>) =
fun (f: 'b -> 'b) ->
(fun a -> async { return! s (f (g a)) a }) : 'a -> Async<Result<'a,'e>>
/// Modify a value using an optic.
let inline map optic f =
(Map ^% optic) f
// ----------------------------------------------------------------------------
// 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))
/// Lift a ValidatedLens into an AsyncValidatedLens with no asynchronous work.
let toAsyncValidated (lens: ValidatedLens<'a,'b,'e>) : AsyncValidatedLens<'a,'b,'e> =
let (get, set) = lens
get, (fun b a -> async { return set b a })
// ----------------------------------------------------------------------------
// 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))
// ----------------------------------------------------------------------------
// Predefined Optics
// ----------------------------------------------------------------------------
/// Various optics implemented for common types such as tuples,
/// lists and maps, along with an identity lens.
[<AutoOpen>]
module Optics =
// Lens for the identity function (does not change the focus of operation).
let id_ : Lens<'a,'a> =
(fun x -> x),
(fun x _ -> x)
/// Isomorphism between a boxed and unboxed type.
let box_<'a> : Isomorphism<obj,'a> =
unbox<'a>, box
/// Lens to the first item of a tuple.
let fst_ : Lens<('a * 'b),'a> =
fst,
(fun a t -> a, snd t)
/// Lens to the second item of a tuple.
let snd_ : Lens<('a * 'b),'b> =
snd,
(fun b t -> fst t, b)
[<RequireQualifiedAccess>]
module Array =
/// Isomorphism to an list.
let list_ : Isomorphism<'v[], 'v list> =
Array.toList,
Array.ofList
[<RequireQualifiedAccess>]
module Choice =
/// Prism to Choice1Of2.
let choice1Of2_ : Prism<Choice<_,_>, _> =
(fun x ->
match x with
| Choice1Of2 v -> Some v
| _ -> None),
(fun v x ->
match x with
| Choice1Of2 _ -> Choice1Of2 v
| _ -> x)
/// Prism to Choice2Of2.
let choice2Of2_ : Prism<Choice<_,_>, _> =
(fun x ->
match x with
| Choice2Of2 v -> Some v
| _ -> None),
(fun v x ->
match x with
| Choice2Of2 _ -> Choice2Of2 v
| _ -> x)
[<RequireQualifiedAccess>]
module Result =
/// Prism to Ok.
let ok_ : Prism<Result<_,_>, _> =
(fun x ->
match x with
| Ok v -> Some v
| _ -> None),
(fun v x ->
match x with
| Ok _ -> Ok v
| _ -> x)
/// Prism to Error.
let error_ : Prism<Result<_,_>, _> =
(fun x ->
match x with
| Error v -> Some v
| _ -> None),
(fun v x ->
match x with
| Error _ -> Error v
| _ -> x)
[<RequireQualifiedAccess>]
module List =
/// Prism to the head of a list.
let head_ : Prism<'v list, 'v> =
(function | h :: _ -> Some h
| _ -> None),
(fun v ->
function | _ :: t -> v :: t
| l -> l)
/// Prism to an indexed position in a list.
let pos_ (i: int) : Prism<'v list, 'v> =
#if NET35
(function | l when List.length l > i -> Some (List.nth l i)
#else
(function | l when List.length l > i -> Some (List.item i l)
#endif
| _ -> None),
(fun v l ->
List.mapi (fun i' x -> if i = i' then v else x) l)
/// Prism to the tail of a list.
let tail_ : Prism<'v list, 'v list> =
(function | _ :: t -> Some t
| _ -> None),
(fun t ->
function | h :: _ -> h :: t
| [] -> [])
/// Isomorphism to an array.
let array_ : Isomorphism<'v list, 'v[]> =
List.toArray,
List.ofArray
[<RequireQualifiedAccess>]
module Map =
/// Prism to a value associated with a key in a map.
let key_ (k: 'k) : Prism<Map<'k,'v>,'v> =
Map.tryFind k,
(fun v x ->
if Map.containsKey k x then Map.add k v x else x)
/// Lens to a value option associated with a key in a map.
let value_ (k: 'k) : Lens<Map<'k,'v>, 'v option> =
Map.tryFind k,
(fun v x ->
match v with
| Some v -> Map.add k v x
| _ -> Map.remove k x)
/// Weak Isomorphism to an array of key-value pairs.
let array_ : Isomorphism<Map<'k,'v>, ('k * 'v)[]> =
Map.toArray,
Map.ofArray
/// Weak Isomorphism to a list of key-value pairs.
let list_ : Isomorphism<Map<'k,'v>, ('k * 'v) list> =
Map.toList,
Map.ofList
[<RequireQualifiedAccess>]
module Option =
/// Prism to the value in an Option.
let value_ : Prism<'v option, 'v> =
id,
(fun v ->
function | Some _ -> Some v
| None -> None)
// ----------------------------------------------------------------------------
// Operators for Working with Optics (including async 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
// ----------------------------------------------------------------------------
// Sample Types and Usage
// ----------------------------------------------------------------------------
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, string> =
(fun addr -> addr.Street),
(fun street addr ->
let streetV = street ^. (ShortString.value_)
if streetV.Length < 200 then Ok ({ addr with Street = street })
else Error "Street name is too long.")
// Create an instance of Person.
let person : Person =
{| Address = "21st jump"; FirstName = "Dennis "; LastName = "Booker" |}
^= Person.value_ <| Unchecked.defaultof<_>
// Compose the ValidatedLenses using the >-> operator.
let homeAddressValidated = Lens.toValidated Person.homeAddress_
let personStreetValueLens = homeAddressValidated >-> Address.street_ >-> ShortString.value_
// Successful asynchronous update.
// Convert the validated lens to an async validated lens.
let asyncPersonStreetValueLens =
(homeAddressValidated >-> Address.street_ >-> ShortString.value_)
|> Lens.toAsyncValidated
// Use the async validated lens in an async block.
let asyncUpdate =
async {
return! "New Street" ^= asyncPersonStreetValueLens <| person
}
async {
let! result1 = asyncUpdate
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
} |> Async.StartImmediate
// Attempt to set an invalid street name (too long).
let result2 = Optic.set (homeAddressValidated >-> Address.street_ >-> ShortString.value_) "A Very Long Street Name" person
match result2 with
| Ok updatedPerson ->
printfn "Updated person: %A" updatedPerson
| Error errMsg ->
printfn "Error: %s" errMsg
// ----------------------------------------------------------------------------
// Example: Composing with error mapping
// ----------------------------------------------------------------------------
let composedLens =
Compose.ValidatedLens.composeMapErrors
(Lens.toValidated Person.homeAddress_)
(Address.street_ >-> ShortString.value_)
(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
// Use a new street value ("composed street") on the left of the ^= operator.
let composedPerson = "composed street" ^= composedLens <| person
printf "composed %A" composedPerson
// ----------------------------------------------------------------------------
// Additional Example: Asynchronous Validation Using AsyncValidatedLens
// ----------------------------------------------------------------------------
(*
In this example we define an asynchronous validated lens for ShortString.
Its setter simulates an asynchronous check (using Async.Sleep) before validating.
*)
module AsyncShortString =
/// An AsyncValidatedLens for ShortString that validates asynchronously.
let valueAsync : AsyncValidatedLens<ShortString, string, string> =
(fun (ShortString s) -> s),
(fun s _ -> async {
// Simulate asynchronous delay (e.g., calling an external service)
do! Async.Sleep 100
if s.Length <= 10 then
return Ok (ShortString s)
else
return Error "Async validation failed: string length must be 10 or less."
})
// Test the asynchronous validated lens.
let asyncShortUpdate =
async {
let ss = ShortString "Hello"
// Try updating with a valid value.
let! validResult = "AsyncTest" ^= AsyncShortString.valueAsync <| ss
// Try updating with an invalid value.
let! invalidResult = "This string is way too long" ^= AsyncShortString.valueAsync <| ss
return (validResult, invalidResult)
}
async{
let! (validUpdate, invalidUpdate) = asyncShortUpdate
printfn "\n--- Asynchronous ShortString Validation Example ---"
printfn "Valid update result: %A" validUpdate
printfn "Invalid update result: %A" invalidUpdate
}|> Async.StartImmediate
(* We know that Ages here will be a comma separated list of zero
or more integer ages. *)
type Record =
{ Ages: string }
static member Ages_ =
(fun x -> x.Ages), (fun ages x -> { x with Ages = ages })
(* An isomorphism to split our string to multiple strings *)
let stringstrings_ : Isomorphism<string,string list> =
(fun s -> List.ofArray (s.Split ',')),
(fun ss -> String.Join (",", ss))
(* An isomorphism from a string list -> int list *)
let stringsints_ : Isomorphism<string list,int list> =
(List.map Int32.Parse), (List.map string)
(* Lens<Record,int list> *)
let recordints_ =
Record.Ages_
>-> stringstrings_
>-> stringsints_
(* Prism<Record,int> *)
let recordfirstint_ = recordints_ >-> List.head_
let record =
{ Ages = "24,56,45,10" }
(* 56 *)
let oldest =
Optic.get recordints_ record |> List.max
(* { Ages = "25,57,46,11" } *)
let record' =
Optic.map recordints_ (List.map ((+) 1)) record
printf "%A" record'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment