Last active
August 29, 2015 14:03
-
-
Save eulerfx/d675cb4e5ff43e106a8f to your computer and use it in GitHub Desktop.
Imperative-style update syntax with F# lenses and state/costate.
This file contains 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
type MerchantSku = { | |
name : string | |
price : decimal | |
} with | |
static member Name = Lens.create (fun x -> x.name) (fun v x -> { x with name = v}) | |
static member Price = Lens.create (fun x -> x.price) (fun v x -> { x with price = v}) | |
let sku = { name = "Shaving Cream" ; price = 10m } | |
let updatedSku = sku |> State.eval (state { | |
do! MerchantSku.Name := "Cool Shaving Cream" | |
do! MerchantSku.Price += 1000m | |
return! State.get | |
}) |
This file contains 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
/// The state monad - a transition from a state, to a value and a new state. | |
type State<'s, 'a> = State of ('s -> 'a * 's) | |
/// The state monad. | |
module State = | |
let inline create f : State<'s, 'a> = State f | |
let run s (state:State<'s, 'a>) = let (State(run)) = state in run s | |
let eval (state:State<'s, 'a>) s = run s state |> fst | |
let exec (state:State<'s, 'a>) s = run s state |> snd | |
let unit a : State<'s, 'a> = create <| fun s -> (a,s) | |
let get<'s> : State<'s, 's> = create <| fun s -> (s,s) | |
let set s : State<'s, unit> = create <| fun _ -> ((),s) | |
let map f (state:State<'s, 'a>) : State<'s, 'b> = | |
create <| fun s -> | |
let (a,s1) = run s state | |
(f a, s1) | |
let bind f (state:State<'s, 'a>) : State<'s, 'b> = | |
create <| fun s -> | |
let (a,s1) = run s state | |
run s1 (f a) | |
let map2 (s1:State<'s, 'a>) (s2:State<'s, 'b>) (f:'a -> 'b -> 'c) : State<'s, 'c> = | |
bind (fun a -> map (fun b -> f a b) s2) s1 | |
let sequenceList (ss:State<'s, 'a> list) : State<'s, 'a list> = | |
List.foldBack (fun s acc -> map2 s acc (fun x xs -> x::xs)) ss (unit (List.empty)) | |
let sequenceSeq (ss:State<'s, 'a> seq) : State<'s, 'a seq> = | |
create <| fun s -> | |
let s = ref s | |
let ss = | |
ss | |
|> Seq.map (fun x -> | |
let (a,s') = x |> run !s | |
s := s' | |
a | |
) | |
|> Seq.toArray | |
(ss |> Seq.ofArray,!s) | |
type StateBuilder() = | |
member x.Bind(s, f) = bind f s | |
member x.Return(value) = unit value | |
member x.ReturnFrom(value) = value | |
member x.Yield(value) = unit value | |
member x.Zero() = unit() | |
member x.Combine(s1:State<'S,unit>, s2:State<'S,'a>) = map2 s1 s2 (fun _ s -> s) | |
member x.For(xs:seq<'a>, f:'a -> State<'S, 'a>) = xs |> Seq.map f | |
let state = new State.StateBuilder() | |
/// CoState comonad (dual of State monad). | |
type CoState<'a, 'b> = 'a * ('a -> 'b) | |
module CoState = | |
let inline create a set : CoState<'a, 'b> = (a,set) | |
let get (s:CoState<'a, 'b>) = let (a,_) = s in a | |
let set (s:CoState<'a, 'b>) = let (_,set) = s in set | |
let map f (s:CoState<'a, 'b>) : CoState<'a, 'c> = | |
let (a,set) = s in | |
(a, f << set) | |
let cobind (f:CoState<'a, 'b> -> 'c) (s:CoState<'a, 'b>) : CoState<'a, 'c> = | |
let (a,set) = s in | |
(a, fun a -> f (a,set)) | |
let cojoin (s:CoState<'a, 'b>) : CoState<'a, CoState<'a, 'b>> = | |
cobind (Operators.id) s | |
/// A lens is a function from a root object 'a to a CoState structure which contains the field 'b and a function to update the field. | |
type Lens<'a, 'b> = 'a -> CoState<'b, 'a> | |
module Lens = | |
let create (get:'a -> 'b) (set:'b -> 'a -> 'a) : Lens<'a, 'b> = | |
fun a -> CoState.create (get a) (fun b -> set b a) | |
let get a (l:Lens<'a, 'b>) : 'b = l a |> CoState.get | |
let set b a (l:Lens<'a, 'b>) : 'a = l a |> CoState.set <| b | |
let update (f:'b -> 'b) (a:'a) (l:Lens<'a, 'b>) : 'a = | |
let (b,set) = l a | |
set (f b) | |
let id<'a> : Lens<'a, 'a> = create id (fun b _ -> b) | |
let compose (f:Lens<'b, 'c>) (g:Lens<'a, 'b>) : Lens<'a, 'c> = | |
create | |
(fun a -> let b = get a g in get b f) | |
(fun c a -> g |> set (f |> set c (g |> get a)) a) | |
let st (l:Lens<'a, 'b>) : State<'a, 'b> = State (fun a -> get a l, a) | |
let foldList (xs:list<Lens<'a, 'a>>) : Lens<'a, 'a> = List.foldBack compose xs id<'a> | |
let product (l1:Lens<'a, 'b>) (l2:Lens<'c, 'd>) : Lens<'a * 'c, 'b * 'd> = | |
create | |
(fun (a,c) -> (get a l1, get c l2)) | |
(fun (b,d) (a,c) -> (set b a l1),(set d c l2)) | |
let coproduct (l1:Lens<'a, 'b>) (l2:Lens<'c, 'b>) : Lens<Choice<'a, 'c>, 'b> = | |
create | |
(function Choice1Of2 a -> get a l1 | Choice2Of2 c -> get c l2) | |
(fun b -> function Choice1Of2 a -> set b a l1 |> Choice1Of2 | Choice2Of2 c -> set b c l2 |> Choice2Of2) | |
let codiag<'a> : Lens<Choice<'a, 'a>, 'a> = | |
let id = id<'a> in | |
coproduct id id | |
let fst<'a, 'b> : Lens<'a * 'b, 'a> = create (fst) (fun a (_,b) -> (a,b)) | |
let snd<'a, 'b> : Lens<'a * 'b, 'b> = create snd (fun b (a,_) -> (a,b)) | |
let mapL (k:'k) : Lens<Map<'k, 'v>, Option<'v>> = | |
create (Map.tryFind k) | |
(fun v m -> match v with Some v -> Map.add k v m | None -> Map.remove k m) | |
let arrayL i : Lens<'a[], 'a> = | |
create (fun arr -> arr.[i]) | |
(fun a arr -> arr.[i] <- a ; arr) | |
[<AutoOpen>] | |
module LensOps = | |
let (:=) (l:Lens<'a, 'b>) b : State<'a, unit> = | |
State.create <| fun a -> ((), Lens.set b a l) | |
let inline (+=) (l:Lens<'a, 'b>) b : State<'a, unit> = | |
State.create <| fun a -> | |
let w = (Lens.get a l) + b | |
((), Lens.set w a l) | |
// reference: Asymmetric Lenses in Scala (https://dl.dropboxusercontent.com/u/7810909/media/doc/lenses.pdf) |
I like that! I will incorporate this into FSharp.Data.JsonValue together with zippers and make a PR to address this fsprojects/FSharp.Data#638
Hmm... Why not make it so that one could write state sku { ... }
?
Addition: Although you'd probably want to use some other name, say update sku { ... }
, to make it clearer and leave state
intact.
@VesaKarvonen I'm not sure I follow. Would sku
be the name of the workflow builder? I do agree however that state
should be aliased to update
or something of the sort.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Small thing, but if you do
then you can do:
Might make for a slightly cleaner lens dsl.