Skip to content

Instantly share code, notes, and snippets.

@eulerfx
Last active August 29, 2015 14:03
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eulerfx/d675cb4e5ff43e106a8f to your computer and use it in GitHub Desktop.
Save eulerfx/d675cb4e5ff43e106a8f to your computer and use it in GitHub Desktop.
Imperative-style update syntax with F# lenses and state/costate.
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
})
/// 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)
@Rickasaurus
Copy link

Small thing, but if you do

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
    member x.Run(value) = value |> exec

then you can do:

 let updatedSku2 = sku |> (state {
    do! MerchantSku.Name := "Cool Shaving Cream"
    do! MerchantSku.Price += 1000m
 })

Might make for a slightly cleaner lens dsl.

@eulerfx
Copy link
Author

eulerfx commented Jul 15, 2014

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

@polytypic
Copy link

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.

@eulerfx
Copy link
Author

eulerfx commented Jul 16, 2014

@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