-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support let! .. and... for applicative functors #579
Comments
@kurtschelfthout @tpetricek and others I would be very glad of input below giving further utilitarian examples where this would be useful. There are examples from both Scala and Haskell |
Note that the F# language already supports
So an alternative is to say that |
I think I have an application for this. I played with the idea of abstracting projections in event-sourcing into data. Basically the idea is the same you have in parser-combinators, the difference being that a monadic interface is not really useful if you want to fold the events only once. Right now I use custom defined operators (basically Haskells Obviously it would fit exactly this here with a nicer syntax F#ers knows more or less. |
A simple example involving everyone's favorite (and often easy to understand as a motivating example) An example is a message stream processing system where events/messages are like The order in which events arrive is not defined, so the processor must wait until a
which produces a |
Another example would be |
This is such a nice explanation of how to use the zip CE feature. I wish the msdn docs explained it so well. |
An example similar to the formlet scenario is from Free Applicatives for CLI option parsing, where it is useful to return a list of available options given the static structure of the parser: let readInt (s:string) =
match System.Int32.TryParse s with
| true,i -> Some i
| _ -> None
type Opt<'a> = Opt of name:string * defaultValue:'a option * read:(string -> 'a option)
with
static member Name<'a> (Opt(n,_,_) : Opt<'a>) = n
static member Read<'a> (Opt(_,_,r) : Opt<'a>) = r
static member Default<'a> (Opt(_,d,_) : Opt<'a>) = d
static member Map (f:'a -> 'b) (a:Opt<'a>) =
let (Opt(n,d,r)) = a in Opt (n,Option.map f d, r >> Option.map f)
type OptAp<'a> =
| PureOpt of 'a
| ApOpt of (Opt<obj -> 'a>) * OptAp<obj>
with
static member Map<'a, 'b> (f:'a -> 'b) (a:OptAp<'a>) : OptAp<'b> =
match a with
| PureOpt a -> PureOpt (f a)
| ApOpt (x,y) -> ApOpt (Opt.Map (fun g -> g >> f) x,y)
static member Size<'a> (a:OptAp<'a>) : int =
match a with
| PureOpt _ -> 1
| ApOpt (a,b) -> 1 + OptAp.Size b
static member AllOpts<'a> (a:OptAp<'a>) : string list =
match a with
| PureOpt x -> []
| ApOpt (a,b) -> [Opt.Name a] @ OptAp.AllOpts b
static member Ap<'b> (fa:OptAp<'a -> 'b>) (y:OptAp<'a>) : OptAp<'b> =
match fa with
| PureOpt f -> OptAp.Map f y
| ApOpt (h,x) ->
let h : Opt<obj -> 'b> =
Opt.Map (
fun f o ->
match o with
| :? (obj * 'a) as x -> let o,a = x in f o a
| _ -> failwith "unreachable") h
let x = OptAp.Map (fun o a -> box (o,a)) x
OptAp.ApOpt (h, OptAp.Ap x y)
static member Merge<'a, 'b> (a:OptAp<'a>) (b:OptAp<'b>) : OptAp<'a * 'b> =
OptAp.Ap (OptAp.Map (fun a b -> a,b) a) b
static member Default<'a> (o:OptAp<'a>) : 'a option =
match o with
| PureOpt a -> Some a
| ApOpt (a,b) ->
match (Opt.Default a),(OptAp.Default b) with
| Some f, Some x -> Some (f x)
| _ -> None
static member MatchOpt (opt:string) (value:string) (o:OptAp<'a>) : OptAp<'a> option =
match o with
| PureOpt _ -> None
| ApOpt (g,x) ->
if opt = "--" + Opt.Name g then Option.map (fun f -> OptAp.Map f x) (Opt.Read g value)
else Option.map (fun f -> ApOpt(g,f)) (OptAp.MatchOpt opt value x)
static member Run (p:OptAp<'a>) (args:string list) : 'a option =
match args with
| [] -> OptAp.Default p
| opt::value::args ->
match OptAp.MatchOpt opt value p with
| Some p' -> OptAp.Run p' args
| None -> None
| _ -> None
let one (o:Opt<'a>) : OptAp<'a> =
OptAp.ApOpt (Opt.Map (fun a _ -> a) o, PureOpt (Unchecked.defaultof<_>))
/// a type to parse
type User = User of un:string * fn:string * id:int
/// define an option parser for User
let user =
OptAp.Ap
(OptAp.Ap
(OptAp.Map (fun un fn id -> User(un,fn,id)) (one (Opt ("fullname", (Some ""), Some))))
(one (Opt ("fullname", (Some ""), Some))))
(one (Opt ("id", None, readInt)))
/// infixed
let (<@>) = OptAp.Map
let (<*>) = OptAp.Ap
let user2 =
(fun un fn id -> User(un,fn,id))
<@> one (Opt ("username", None, Some))
<*> one (Opt ("fullname", (Some ""), Some))
<*> one (Opt ("id", None, readInt))
/// would be
//let user = opt {
// let! un = Opt("username", (Some ""), Some)
// and fn = Opt("fullname", (Some ""), Some)
// and id = Opt("id", None, readInt)
// return User(un,dn,id) }
/// statically read all options
let options = OptAp.AllOpts user |
@Savelenko that would be a nice syntactic feature, however it due to the way that Async is defined, it isn't possible to analyze the static structure of an async workflow. An example in Desugaring Haskell's do-notation into applicative operations describes how Facebook's Haxl make it spossible. EDIT |
@eulerfx I am not sure what you mean. There is no need to analyze the static structure exactly because the proposed notation makes it explicit that constituent computations are independent of each other. In case of |
@Savelenko: I'll try to explain using the OP:
The rendering is possible because one is able to analyze the static structure of the formlet. This is the nice thing about an applicative as compared to a monad - to determine the structure of a monadic workflow, it must be evaluated. Async is defined monadically (as a specialized continuation monad type |
And why would it be interesting to know that? The goal is to construct a "parallel" |
@Savelenko: you could, for example, wish to count the number of Async operations that are part of a workflow. |
To clarify how this worked in the joinads proposal, you can either have just async {
let! a = oneWork
and b = twoWork
let r = a + b // Normal let is fine
return r } // Return becomes Map In this case, you can statically analyse the structure. If you add async {
let! a = oneWork
and b = twoWork
let! r = moreWork (a + b)
return r } Now you cannot statically analyse the structure, but the syntax is still useful e.g. for running things in parallel. |
Just to note that I suppose we would use |
I would be thrilled with this addition. My library Rezoom is like Facebook's Haxl, in that its Currently I am overloading FParsec would also benefit from this. Right now the FParsec documentation recommends against using computation expressions to define parsers, since the parser after the first Finally, I could see it being useful for a builder that produces type ValidationErrors<'err> =
| Invalid of 'err
| MultipleInvalid of 'err ValidationErrors * 'err ValidationErrors You could bind multiple results and merge their errors on failure, in order to present as many errors as possible to the user (telling them that their email and password are both invalid, for example, instead of having them fix the first error only to run into the second). |
Another area where this would be useful is with Observables (or things similar to Observables) It's common to want to subscribe to the values of multiple Observables: observable {
let! a = foo
and! b = bar
return a + b
} This returns a new Observable which will subscribe to both The new Observable outputs e.g. if foo: 1 ... 2 .... 3 .......... 4
bar: 5 ..... 6 ..... 7 ... 8 ... Then the output Observable would have these values over time:
|
I am not seeing how any of the above examples couldn't be written with multiple |
The fundamental difference is that in:
The variable
For the person implementing the computation expression, this means they can handle
|
Why not just write The formlet example is certainly not a good example, as it would be unreasonable to require independence between the inner formlets - as one usually wants the opposite, e.g. dependent formlets. |
The compiler could convert an arbitrary number of This is particularly handy for FParsec, where you might write: /// Parses 3 integers separated by whitespace, with optional trailing whitespace.
let threeInts =
parse {
let! i1 = pint
and! () = spaces1
and! i2 = pint
and! () = spaces1
and! i3 = pint
and! () = spaces
return (i1, i2, i3)
} (see note 2) This parser is less efficient if written with a chain of With regard to the formlet example, I'm not acquainted with that one, but I think the idea is that with
|
How is We have done quite a bit of work with formlets in the past, what you mention is a formlet vs flowlet, as defined in IFL 2010 our paper [1]. (It's a shame publishers cannibalized academic papers, I am happy to send a copy if you are interested.) No CE extension was required. You can find a formlet+flowlet example here - and hit Try Live on the top to see it in action, and the related documentation here and here. Since then we have also experimented with a reactive formlet library (WebSharper.UI.Next.Formlets, example here and here), and a fundamentally more powerful and developer-friendly library (WebSharper.Forms) around piglets [2], cheat sheets here, another formalism created in our research group. Both are based on UI.Next, WebSharper's reactive library, which in turn has other uses of CEs such as composing reactive views. You can find more examples at Try WebSharper. [1]: Bjornson, J., Tayanovskyy, A., Granicz, A. Composing Reactive GUIs in F# Using WebSharper. IFL 2010. |
@granicz For Observables, Depending on the implementation of But with In WebSharper, The documentation for View says to prefer Using The same is true with many of the other examples: when you use multiple But with |
Isn't the use of I can see how certain applications like those already mentioned would benefit from having a computation builder that does not implement |
@Pauan The ability to express domain-specific computation (e.g. inside CEs) over a group of independent bindings is certainly useful, but I just don't think that parser combinators and formlets are particularly good examples for this. However, other examples can be and indeed are. This thread also helped me appreciate the proposed |
@kurtschelfthout - That potential confusion is why I just thumbed-up Don Syme's comment about using |
I think the parsers example is valid one - arguably, that's a case where funky operators are established way of doing it, but this could give you a nice syntax for it - as for why applicative is better than monadic, see this SO question about this in Haskell and paper Deterministic, Error-Correcting Combinator Parsers. |
I've done a little experimentation, and it was fun to see use of a zip-like operator work as expected for asyncs and observables. I used |
"yes" is like "no" but with a special meaning ;)
This does not really detract from the usefulness of applicatives of course, even though I maintain they are easy enough to get via operators. But if we must have new syntax, how about let threeInts =
parse {
let! i1 = pint
also! () = spaces1
also! i2 = pint
also! () = spaces1
also! i3 = pint
also! () = spaces
return (i1, i2, i3)
} But this also shows some weirdness - do we need an applicative let and and an applicative do? let threeInts =
parse {
let! i1 = pint
alsodo! spaces1
alsolet! i2 = pint
alsodo! spaces1
alsolet! i3 = pint
alsodo! spaces
return (i1, i2, i3)
} |
I get what you're saying here. That said, historically ML-family languages (e.g. Edinburgh ML, OCaml) have supported I'm not sure if the history matters though |
I have seen that this |
I wonder what would be the default behavior for async applicatives. |
I don't think async would support Map/Merge by default |
Not sure what do you mean? They will not work in parallel by default? But then the next question is which (non-default) construction can we provide in order to run them in parallel without defeating the whole purpose of this syntactic sugaring? |
You just won't be able to write |
Oh, so you won't add new methods to the async builder in FSharp.Core? Only a new desugaring mechanism in the compiler? Then the end-user, or a library will provide a |
I'd really love for this to be in F#. Just want to share a quick "workaround" that just occurred to me. Say you're validating user input using my let innerFunction x1Validated x2Validated =
result {
// work with x1Validated and x2Validated
}
let outerFunction x1Raw x2Raw =
innerFunction
<!> validateX1 x1Raw
<*> validateX2 x2Raw which requires separating out the function that works with the validated values and using a wrapping function just to apply the arguments, you can define a helper method let tup2 x1 x2 =
x1, x2
let innerFunction x1Raw x2Raw =
result {
let! x1Validated, x2Validated = tup2 <!> validateX1 x1Raw <*> validateX2 x2Raw
// work with x1Validated and x2Validated
} The point here simply being that for many simple cases, the general-purpose |
The point of this proposal is that helper functions like let innerFunction x1Raw x2Raw =
result {
let! x1Validated = validateX1 x1Raw
and! x2Validated = validateX2 x2Raw
// work with x1Validated and x2Validated
} |
Yes I know, which is why I would love it. Just wanted to share the next best thing that occurred to me, which saved me from needlessly splitting up logic just to apply wrapped arguments. :-) |
I'm really excited about this proposal, but I have a couple of questions: First, what would be the type of the mapping function? I don't like the idea of allocating a tuple for each call to it. Second, do we really need a new syntax for this? A builder {
let! a = foo
let! b = bar
return (a, b)
}
// Could compile to:
builder.Map(
builder.Merge(foo, bar),
(fun (a, b) -> (a, b))) but this: builder {
let! a = foo
let! b = baz a // Note we use `a` here
return (a, b)
}
// Would compile to:
builder.Bind(foo, (fun a ->
builder.Map(baz a, (fun b -> (a, b)))) I admit this would be less explicit and more complex to implement, (maybe too hard/slow?). But the syntax would allow normal Also, a builder supporting both The main edge case with this syntax would be a normal |
@dsyme Why is this one marked as started? |
I must admit from the wording described here I still struggle to understand the usefulness of this feature. |
The RFC has a good number of examples: https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1063-support-letbang-andbang-for-applicative-functors.md |
So after a bit of playing it turns out you can actually do this within computation expression syntax right now: https://gist.github.com/emcake/d6456fb45a0995b4afef00628a4557ff I imagine this isn't intentional, but Don gives the game away at the start with IsLikeZip. I still think that specific syntax for |
Zip makes it explicit, I like that rather than the implicit bind and map
which is not obvious.
…On Sat, 23 Mar 2019 at 08:38, emcake ***@***.***> wrote:
So after a bit of playing it turns out you can actually do this within
computation expression syntax right now:
https://gist.github.com/emcake/d6456fb45a0995b4afef00628a4557ff
I imagne this isn't intentional, but Don gives the game away at the start
with IsLikeZip.
I don't thik this is a reason to not add specific syntax for let!..and!
but I imagine there are a few various alternatives to applicatives in the
wild right now, and others might want to know.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#579 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAj7yuFuaaEJhee5C9gtd41XYTdd91Sgks5vZef-gaJpZM4N5wY7>
.
|
Have updated the gist: https://gist.github.com/emcake/d6456fb45a0995b4afef00628a4557ff As it turns out one nice side effect of this is it plays very nicely with bind. |
@emcake that's really interesting - I've never seen that done before! With further abuse of computation expression builders, you can get slightly closer to the |
Closing out as completed for F# 5. |
Sitting at WG2.8 hearing talks on more theoretical things reminds me of this corner case of the F# language design which was effectively a small part of the joinads proposal #172 .
From the paper The F# Computations Expression Zoo
Pros and Cons
The advantages of making this adjustment to F# are outlined in the paper linked above
More examples are needed to expand the utility of this
The disadvantages of making this adjustment to F# are it adds complexity and potential overly simple encoding of obscure computational constructs.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): M
Related suggestions: #172
Affidavit (must be submitted)
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: