Skip to content
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

Closed
5 tasks done
dsyme opened this issue Jun 14, 2017 · 50 comments
Closed
5 tasks done

Support let! .. and... for applicative functors #579

dsyme opened this issue Jun 14, 2017 · 50 comments

Comments

@dsyme
Copy link
Collaborator

dsyme commented Jun 14, 2017

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

Applicative functors [14,12] are weaker (and thus more common) abstraction than monads. The difference between applicative and monadic computations is that a monadic computation can perform different effects depending on values obtained earlier during the computation. On the other hand, the effects of an applicative computation are fully determined by its structure. In other words, it is not possible to choose which computation to run (using let! or do!) based on values obtained in previous let! bindings.

The following example demonstrates this using a web form abstraction called formlets [2]:

formlet { let! name = Formlet.textBox
          and gender = Formlet.dropDown ["Male"; "Female"]
          return name + " " + gender }

The computation describes two aspects – the rendering and the processing of entered values. The rendering phase uses the fixed structure to produce HTML with text-box and drop-down elements. In the processing phase, the values of name and gender are available and are used to calculate the result of the form. The structure of the form needs to be known without having access to specific values. The syntax uses parallel binding (let!. . . and. . . ), which binds a fixed number of independent computations. The rest of the computation cannot contain other (applicative) bindings. There are two equivalent definitions of applicative functors. We need two operations known from the less common style.

  • Merge of type F α×F β → F(α×β)`` represents composition of the structure (without considering specific values) and
  • Map of type F α×(α → β) → F β transforms the (pure) value.

The computation expression from the previous example is translated as follows:

formlet.Map   ( formlet.Merge(Formlet.textBox, Formlet.dropDown ["Male"; "Female"]), fun (name, gender)name + " " + gender )

The computations composed using parallel binding are combined using Merge. In formlets, this determines the structure used for HTML rendering. The rest of the computation is turned into a pure function passed to Map. Note that the translation allows uses beyond applicative functors. The let!. . . and. . . syntax can also be used with monads to write zip comprehensions [5].

Applicative functors were first introduced to support applicative programming style where monads are not needed. The idiom brackets notation [14] fits that purpose better. We find that computation
expressions provide a useful alternative for more complex code and fit better with the impure nature of F#.

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:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I would be willing to help implement and/or test this
@dsyme
Copy link
Collaborator Author

dsyme commented Jun 14, 2017

@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

@dsyme
Copy link
Collaborator Author

dsyme commented Jun 14, 2017

Note that the F# language already supports zip-like custom operations, #if-declared like this in Query.fsi:

#if SUPPORT_ZIP_IN_QUERIES
        /// <summary>
        /// When used in queries, this operator corresponds to the LINQ Zip operator.
        /// </summary>
        [<CustomOperation("zip",IsLikeZip=true)>] 
        member Zip : firstSource:QuerySource<'T1> * secondSource:QuerySource<'T2> * resultSelector:('T1 -> 'T2 -> 'Result) -> QuerySource<'Result>
#endif

So an alternative is to say that let! x1 = e1 and x2 = e2 in e3 desugars to builder.Zip(e1,e2,(fun x1 x2 -> e3)) if a Zip custom operation is supported. However, it's basically impossible to find custom builders which make use of this (can anyone find one?), so we could equally use some other encoding, and plausibly deprecate the above.

@CarstenKoenig
Copy link

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 <$>, <*>) with curried constructors for this (here is an example implementation https://github.com/CarstenKoenig/RemmiDemmi/blob/master/Definitionen.fsx)

Obviously it would fit exactly this here with a nicer syntax F#ers knows more or less.

@Savelenko
Copy link

A simple example involving everyone's favorite (and often easy to understand as a motivating example) Option<T> is collecting data pieces which are required to compose a larger piece of data.

An example is a message stream processing system where events/messages are like Name and Age, which are both required to construct a Person value. A message processor maintains state, where data pieces collected so far are represented as Option<Name> and Option<Age> and so on. Then there is a simple function person : Name -> Age -> Person, expressing the fact that all those pieces are required to make a Person.

The order in which events arrive is not defined, so the processor must wait until a Person can be made. This is elegantly expressed by

person <!> name <*> age

which produces a Some iff all data has been collected. It would be very nice to be able to write this in a CE sometimes, albeit in a "custom" one, as there is no predefined builder for Option<t>.

@Savelenko
Copy link

Another example would be Async, for which there is a CE builder in the standard library. "Await all" would look very intuitive in the let! = ... and ... guise.

@Rickasaurus
Copy link

Note that the F# language already supports zip-like custom operations, #if-declared like this in Query.fsi:

This is such a nice explanation of how to use the zip CE feature. I wish the msdn docs explained it so well.

@eulerfx
Copy link

eulerfx commented Jun 20, 2017

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

@eulerfx
Copy link

eulerfx commented Jun 20, 2017

@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
On that note however, this is another possible use case - ability to analyze a statically define async workflows. We'd need a new type to carry the structure, but can be useful. Haxl is one example, and yet another example is in the same paper Free Applicatives referenced above. It builds an embedded lang corresponding to operations on a file systems, namely reading and writing, and allows, for example, to count the number of operations in a workflow using static analysis.

@Savelenko
Copy link

@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 Async, if the notation desugares to Zip mentioned above (or a similar method), we could just use Async.StartChild there for each of the RHS of let! = ... and ....

@eulerfx
Copy link

eulerfx commented Jun 20, 2017

@Savelenko: I'll try to explain using the OP:

The computation describes two aspects – the rendering and the processing of entered values.

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 type Async<'a> ~= ('a -> unit) -> unit). Therefore, if you've a value of type Async<'a * 'b> that has been created using Async.StartChild as you suggest, it will run the two argument computation in parallel, and they are indeed independent of each other, however, it is impossible to know that by inspecting the value itself.

@Savelenko
Copy link

And why would it be interesting to know that? The goal is to construct a "parallel" Async using a convenient notation/syntax, not determine its structure once constructed. There is no rendering going on here, like with formlets.

@eulerfx
Copy link

eulerfx commented Jun 20, 2017

@Savelenko: you could, for example, wish to count the number of Async operations that are part of a workflow.

@tpetricek
Copy link
Member

tpetricek commented Jun 20, 2017

To clarify how this worked in the joinads proposal, you can either have just Map and Merge in which case you are only allowed one let! per workflow, so the following works:

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 Bind, then you're also allowed to use:

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.

@dsyme
Copy link
Collaborator Author

dsyme commented Jun 21, 2017

Just to note that I suppose we would use and! not and

@rspeele
Copy link

rspeele commented Jun 23, 2017

I would be thrilled with this addition.

My library Rezoom is like Facebook's Haxl, in that its apply batches requests for external data, usually SQL queries provided by Rezoom.SQL.

Currently I am overloading Bind for 2 and 3-tuples so let! a, b, c = planA, planB, planC binds multiple variables applicatively. This let! ... and syntax would be a very welcome improvement, since it gets hard to associate e.g. c with planC.

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 let! will get re-computed on each parse. This would solve that very nicely.

Finally, I could see it being useful for a builder that produces Result<'ok, ValidationErrors<'err>>, where ValidationErrors is defined something like:

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).

@Pauan
Copy link

Pauan commented Jun 24, 2017

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 foo and bar.

The new Observable outputs a + b, which is recomputed every time foo or bar outputs a new value.

e.g. if foo and bar have the following values over time:

foo: 1 ... 2 .... 3 .......... 4
bar: 5 ..... 6 ..... 7 ... 8 ...

Then the output Observable would have these values over time:

1 + 5 ... 2 + 5 ... 2 + 6 ... 3 + 6 ... 3 + 7 ... 3 + 8 ... 4 + 8

@granicz
Copy link

granicz commented Jun 24, 2017

I am not seeing how any of the above examples couldn't be written with multiple let!s instead of let! ... and! .... Anyone care to explain?

@rspeele
Copy link

rspeele commented Jun 24, 2017

The fundamental difference is that in:

let! x = exprX
let! y = exprY

The variable x is in scope for exprY (whether it's used there or not). In:

let! x = exprX
and! y = exprY

exprY would not be permitted to refer to x.

For the person implementing the computation expression, this means they can handle exprX and exprY simultaneously, instead of having to wait for whatever computation is represented by exprX to complete in order to start working on exprY.

let! x = exprX in etc desugars to builder.Bind(exprX, fun x -> etc), so as the implementer of Bind you have no way to dig into the fun x -> etc callback and start running anything in etc until you have an x to give it.

let! x = exprX and! y = exprY in etc would be something conceptually similar to: builder.BindMultiple(x, y, fun x y -> etc).

@granicz
Copy link

granicz commented Jun 24, 2017

Why not just write let! x, y = exprX, exprY in ...? I get your argument on "hard to tell what is bound to what" as the number of "independent" bindings increase, but I don't see it as a good enough reason to complicate the language for it, especially if CE authors can't get to define their own BindMultiple expressible in the type system.

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.

@rspeele
Copy link

rspeele commented Jun 24, 2017

The compiler could convert an arbitrary number of and ... chains into nested BindMultiple (see note 1) calls, whereas with the let! a,b,c... hack, the CE author must define overloads up to some limited arity.

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 let! and do!. If it included parsers more complex than pint and spaces1, it could be dramatically less efficient by say, rebuilding an operator precedence parser each time it parses a string. The author of FParsec recommends not using the computation expression for this reason, instead suggesting that users use functions like pipe2, pipe3, etc. to glue sequential parsers together. My FParsec-Pipes library abuses the type system to avoid the fixed-arity pipe combinators, but I would be happy to see that replaced with a better computation expr.

With regard to the formlet example, I'm not acquainted with that one, but I think the idea is that with and! you could combine multiple formlets on one screen instead of requiring the user to proceed to the next screen to see the other formlet.

  • Note 1: BindMultiple might actually be called Apply and have a slightly different type signature, but same basic idea.

  • Note 2: This example also suggests it might be useful to have something like an and do! expr as sugar for and! () = expr.

@granicz
Copy link

granicz commented Jun 24, 2017

How is parse {let! ... and! ... and! ...} (e.g. a single independent block) more efficient than a parse {let! ... let! ... let! ...}? Either way, if you are concerned about closure allocations in your parsing with computation expressions, matching with active patterns might be another option (here is article I wrote back in 2009), if I recall correctly that these provide some basic optimization for calling recognizers to circumvent some of that overhead. However, both parser combinators and active patterns will suffer from backtracking and neither will beat generated parsers (fsyacc, etc.).

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.
[2]: Denuziere, L., Rodriguez E., Granicz, A. “Piglets to the rescue: Declarative User Interface Specification with Pluggable View Models.” IFL 2013. paper here
[3]: Fowler, S., Denuziere, L., Granicz, A. Reactive Single-Page Applications with Dynamic Dataflow. PADL 2015. paper here

@Pauan
Copy link

Pauan commented Jun 24, 2017

@granicz For Observables, let! a = foo; let! b = bar is quite inefficient, because every time foo outputs a value, it has to unsubscribe from bar and then resubscribe to bar

Depending on the implementation of bar, this resubscription can even change the behavior of the program.

But with and! there is no resubscription: it simply subscribes to foo and bar once.


In WebSharper, let! corresponds to View.Bind, and and! corresponds to View.Map2

The documentation for View says to prefer Map2 over Bind, because it is more efficient (not just because of the closures) and it also behaves better with object identity.

Using and! is a lot nicer syntactically than using View.Map2 or <*>


The same is true with many of the other examples: when you use multiple let! the computation expression can only see the first let!, the remaining let! are hidden, therefore it cannot parallelize it: let! is inherently sequential.

But with and! the computation expression can see everything, which allows for a parallel or concurrent implementation.

@kurtschelfthout
Copy link
Member

Isn't the use of and confusing? Everywhere else, and is used for values or types that are interdependent, while here it would be used for values that explicitly aren't.

I can see how certain applications like those already mentioned would benefit from having a computation builder that does not implement Bind but does implement Map and Merge. On the other hand it's pretty easy, in my experience, to express those kinds of computations with a few operators instead. (I know some people are fundamentally against use of operators beyond +, but I am not one of them.)

@granicz
Copy link

granicz commented Jun 24, 2017

@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 let! ... and! ... syntax in addition to let! x, y, z = e1, e2, e3. We just have to make sure the language doesn't ever get a let rec! ... and! ... pair :) Thanks.

@rmunn
Copy link

rmunn commented Jun 27, 2017

@kurtschelfthout - That potential confusion is why I just thumbed-up Don Syme's comment about using and! instead of and. That should reduce the confusion, because now it fits in with the other "special" CE keywords. The mental model is: "let! is like let but with a special extra unwrapping step. And do! is like do but with a special operation. And likewise, and! is like and but with a special meaning." Here, the special meaning is "This acts like an applicative step" -- and yes, it does have different scope rules than and. But when I see the ! at the end of the keyword, I'm already trained to think, "Okay, this is similar to the other keyword but with CE-specific rules", so the scope change doesn't bother me -- as long as the keyword is and! rather than and.

@tpetricek
Copy link
Member

tpetricek commented Jun 27, 2017

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.

@panesofglass
Copy link

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 Task.WaitAll in the former with both Async.StartAsTask and Async.StartChildAsTask with similar results.

@kurtschelfthout
Copy link
Member

The mental model is: "let! is like let but with a special extra unwrapping step. And do! is like do but with a special operation. And likewise, and! is like and but with a special meaning."

"yes" is like "no" but with a special meaning ;)

and! is completely unlike and because it means something completely different - and is used to allow two expressions to refer to each other where they otherwise can't. and! would be used to disallow two expressions to refer to each other, where otherwise expressions would be able to refer to preceding expressions.

let!/let and do!/do are similar of course. But comparing that with and and the proposed and! seems to prove the point...

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 also?

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)
    }

@dsyme
Copy link
Collaborator Author

dsyme commented Aug 29, 2017

... and is used to allow two expressions to refer to each other where they otherwise can't. ...

I get what you're saying here. That said, historically ML-family languages (e.g. Edinburgh ML, OCaml) have supported let x = expr1 and y = expr2 in expr3 where expr2 couldn't reference x. So here and had exactly the meaning of simultaneous non-mutually-referential bindings. It was always the rec which introduced mutual reference - except in the case of mutually referential type definitions, which just used and :)

I'm not sure if the history matters though

@Lleutch
Copy link

Lleutch commented Oct 6, 2017

@tpetricek

I have seen that this Map and Merge will enfore that only one let! is done.
Does it mean that, in some ways it is possible to have linearity of variables checked statically in the scope of the CE ? (If I understand correctly)
It would also be a great thing to have scoped linearity in CE "for free" if we want that.

@gusty
Copy link

gusty commented Dec 24, 2017

I wonder what would be the default behavior for async applicatives.
Would they run in parallel?
I thought asyncs were designed to be explicitly run in parallel as there might be cases where you expect them to run sequentially, although I can't think of any at this time.
Maybe I got it wrong.

@dsyme
Copy link
Collaborator Author

dsyme commented Jan 12, 2018

I wonder what would be the default behavior for async applicatives. Would they run in parallel? I thought asyncs were designed to be explicitly run in parallel...

I don't think async would support Map/Merge by default

@gusty
Copy link

gusty commented Jan 12, 2018

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?

@dsyme
Copy link
Collaborator Author

dsyme commented Jan 12, 2018

Not sure what do you mean? They will not work in parallel by default?

You just won't be able to write async { let! x = ... and y = ... in .... }, it just won't typecheck because the relevant methods are missing

@gusty
Copy link

gusty commented Jan 12, 2018

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 seqAsync and/or parallelAsync builder. Is that what you mean?

@cmeeren
Copy link

cmeeren commented Apr 17, 2018

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 result CE. If you have defined relevant map (<!>) and apply (<*>) operators, then instead of

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 tupX for any needed arity X and instead do

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 tupX functions will save you from splitting up logic if you don't want to.

@Savelenko
Copy link

Savelenko commented Apr 17, 2018

The point of this proposal is that helper functions like tup2 above are not needed:

let innerFunction x1Raw x2Raw =
  result {
     let! x1Validated = validateX1 x1Raw
     and! x2Validated = validateX2 x2Raw
     // work with x1Validated and x2Validated
  }

@cmeeren
Copy link

cmeeren commented Apr 17, 2018

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. :-)

@amongonz
Copy link

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 let! could transform to a call to Map(Merge(...), ...) anytime the expression bound is not used in a following binding. For example:

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 let and do to be interleaved with the let! bindings, and you could use the incoming match! as the last binding. I wonder what semantics would interleaving do! have here.

Also, a builder supporting both Bind and Map/Merge would always use the optimal path without the user changing any code.

The main edge case with this syntax would be a normal let binding making use of a bound expression before the last let!. At that point, you either track that binding too or default to Bind.

@7sharp9
Copy link
Member

7sharp9 commented Mar 14, 2019

@dsyme Why is this one marked as started?
I cant find a reference to an implementation in this discussion?

@cartermp
Copy link
Member

dotnet/fsharp#5696

@7sharp9
Copy link
Member

7sharp9 commented Mar 15, 2019

I must admit from the wording described here I still struggle to understand the usefulness of this feature.

@cartermp
Copy link
Member

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

@emcake
Copy link

emcake commented Mar 23, 2019

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 let!..and! is valuable but I imagine there are a few various alternatives to applicatives in the wild right now, and others might want to know.

@7sharp9
Copy link
Member

7sharp9 commented Mar 23, 2019 via email

@emcake
Copy link

emcake commented Mar 23, 2019

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.

@nickcowle
Copy link

@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 let!..and! syntax:
https://gist.github.com/nickcowle/55a9f94e251a4aaf318abf208644053a

@cartermp
Copy link
Member

cartermp commented Nov 8, 2020

Closing out as completed for F# 5.

@cartermp cartermp closed this as completed Nov 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests