Skip to content

Consider incorporating FSharp.Core.Fluent into FSharp.Core  #1073

Open

Description

@dsyme

I'd like to open a discussion about incorporating FSharp.Core.Fluent into FSharp.Core. Or we could consider adding it as a second F# DLL referenced by default, with the option of turning that off.

https://fsprojects.github.io/FSharp.Core.Fluent/

image

Pros and Cons

The advantages of making this adjustment to F# are

  • reduced barrier to entry for beginners - xs.sum() relies only on dot notation, c.f. xs |> Seq.sum requires knowlesdge of |> and Seq, and knoweledge that input collections support Seq/IEnumerable programming
  • more discoverable code for simple things like summing a list
  • it's often more succinct

The disadvantages of making this adjustment to F# are

  • fluent collection programming has some downsides for clarity, performance
  • loss of pipeline debugging
  • multiple ways to do things
  • complicates library authorship
  • introduces camelCase naming to methods and dot notation
  • more type annotations are required

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

Activity

  1. cartermp commented on Sep 9, 2021

    @cartermp
    Member

    I'm generally in favor of doing this, especially if debugging improvements come in with this motivating it. I would prefer that if this is done, it's just a part of FSharp.Core. Not a separate assembly.

    This is clearly in violation of this principle:

    whether this gives multiple ways to achieve the same thing

    But, ehh, we've violated that one before. Yay for guidelines and not rules, eh?

    I think it's worth going through who the intended audience is and why they would benefit from this.

  2. cannorin commented on Sep 9, 2021

    @cannorin

    I generally love the idea (adding popular module functions as extension methods) but not a fan of camelCase method names, especially in the standard library. It may encourage people to write their methods in camelCase, which I think is not very good.

  3. sudqijawabreh commented on Sep 9, 2021

    @sudqijawabreh

    I think this well help with autocomplete since you write dot and then you can discover what operations you can apply.

  4. rosalogia commented on Sep 9, 2021

    @rosalogia

    I'm generally in favor of doing this, especially if debugging improvements come in with this motivating it. I would prefer that if this is done, it's just a part of FSharp.Core. Not a separate assembly.

    I'm curious what kind of debugging improvements you foresee being motivated by this change? If you're right about it motivating debugging improvements, I suddenly have a strong reason to agree with this suggestion.

    As it stands, the reasoning seems somewhat weak to me. I worry about the similarity/simplicity of the syntax taking attention away from the loss of pipeline debugging and performance that might result in overuse of this syntax. It would be a bit of a shame if newcomers to the language/paradigm make a habit of using a syntax that necessarily carries with it these cons when the alternative pipelining syntax is not so much more complicated.

    Have others generally found that beginners struggle with adapting to pipelining? Or that developers in general struggle with discovery of module functions because of pipelining? Perhaps my perspective is skewed 😋

  5. 7sharp9 commented on Sep 9, 2021

    @7sharp9
    Member

    Another con of fluent is more annotations are normally required, which sort of spoils the flow having to go back and add an annotation.

  6. vzarytovskii commented on Sep 9, 2021

    @vzarytovskii

    It would be nice but as @cartermp said:

    This is clearly in violation of this principle:

    whether this gives multiple ways to achieve the same thing

    Which may be confusing for users.

    Also, question about official (and existing) docs and guides, will they favour one over another (fluent vs classic)? Shall we have an official "suggestion" what to use specifically?

  7. cartermp commented on Sep 9, 2021

    @cartermp
    Member

    @rosalogia

    I'm curious what kind of debugging improvements you foresee being motivated by this change?

    Since we now have pipeline debugging/stepping, it's a clear advantage to use them when diagnosing code. So imagine code like this:

    m1(asdf).m2(fun x -> (* some code *)).m3(asdf2)

    Where you can step into each piece of the call chain and subsequent statements/expressions within them. @dsyme sounds motivated to make that possible/better compared to today. It's an orthogonal improvement, but with this suggestion, a lot more impactful.

  8. rosalogia commented on Sep 9, 2021

    @rosalogia

    @cartermp makes sense; thanks for elaborating!

  9. pbiggar commented on Sep 9, 2021

    @pbiggar

    I don't like this at all! :)

    multiple ways to do things

    One of the things I've learned about programming languages (and software in general) is that when you have multiple ways of doing things, to only do that if the new ways can be done linearly.

    The Fluent namespace is a linear change - people can choose to import it, and nobody expects every library and API to support it.

    Moving it into FSharp.Core elevates it, saying "Fluent is a 2nd way of doing this thing". Tutorials and docs now need to cover it. People adding new top-level functions now need to make Fluent methods for them or else they're not as "first class" as the builtins (that is, they'll need to break up their tidy Fluent chains with an "ugly" function application or pipe). People now ask why there aren't Fluent versions of APIs, and packages get PRs adding Fluent versions of every function.

    I personally like functions and using piping to compose them. But, if you/the community thinks Fluent is better, I would suggest changing the language to have Fluent be the one true way of doing things. One way is better than two ways.

    If switching to fluent doesn't sound good, I would suggest instead improving functions so that they don't have the disadvantages you perceive. For example, for

    more discoverable code for simple things like summing a list

    VSCode (etc) could be made suggest a function with the appropriate type when you press '.'; after the completion, it would replace the . with a |>

    conceptually simpler options for beginner code, xs.sum() relies only on dot notation, c.f. xs |> Seq.sum requires knowlesdge of |> and Seq

    I do not believe that it is easier for beginners to learn two things than one thing (or to learn one thing only to learn that no-one does that - of course if people start to do that, now you have two things). The question of when to use functions vs methods is already challenging and not well addressed, this change would make it worse, imo.

    TL;DR: pick a lane 😊

  10. cartermp commented on Sep 9, 2021

    @cartermp
    Member

    VSCode (etc) could be made suggest a function with the appropriate type when you press '.'; after the completion, it would replace the . with a |>

    I actually have the beginnings of something along those lines (intellisense for pipelines) here for any F# editor, but it's super early stage and will involve some tricky stuff that's currently beyond me to do in my spare time. Any help appreciated 😄

    This is good feedback though, and I'd love to have more written out regarding this to offer more

    I think it's worth going through who the intended audience is and why they would benefit from this.

    I more immediately think of python developers working with collections for the first time. Maybe they would appreciate FSharp.Core.Fluent as a style, or maybe they would be satisfied with better tooling with pipelines.

  11. stweb commented on Sep 9, 2021

    @stweb

    I wonder if there's a way to support better autocomplete FSAC tooling for the F# syntax instead - I would prefer that over added language complexity. This could be triggered by typing |> -- maybe also by newline.

  12. pbiggar commented on Sep 9, 2021

    @pbiggar

    more immediately think of python developers working with collections for the first time. Maybe they would appreciate FSharp.Core.Fluent as a style,

    Another way to look at this is that when people see that .sum works, they would expect that the autocomplete options they are offered are the complete set. This would probably lead them to conclude that the standard library is very sparse.

  13. ursenzler commented on Sep 9, 2021

    @ursenzler

    I can see why this is tempting.
    However, I'm not too fond of this suggestion. As mentioned above, expectations would quickly come that everything that can be done via a pipeline call is available via Fluent-style. Otherwise, there will be style breaks in the code: a.foo.bar |> zoo (if zoo is not available in Fluent-style). That would make code much harder to read - in addition to having two styles for the same.
    The result is added needed effort for library authors (coding, documenting) and is hard to keep consistent.
    If both would be available, I'd probably almost always use the pipeline variant because I think fewer type annotations would be needed.

  14. BentTranberg commented on Sep 9, 2021

    @BentTranberg

    Alright, this adds multiple ways of doing things, but as a library that already exists, and not as a language syntax change. This makes the "multiple" argument mote in my view. Everybody can simply use the library right now. Isn't this what we're doing all the time anyway, taking advantage of F#'s power as an algebraic language to express things in less space and more elegantly?

    In fact I see all the Cons as the other side of Pros. Some downsides doesn't mean it's bad. Loss of pipeline debugging, yes, but the alternative is there, and debugging can be improved. Library authorship must necessarily be affected. Why not introduce camelCase naming to methods and dot notation?

  15. 13 remaining items

  16. piaste commented on Sep 13, 2021

    @piaste

    Very much not a fan. The simplicity and uniformity of F# has often been remarked upon as one of its strongest points, by outside observers as well.

    This would compromise that simplicity in order to compensate for a weakness in the tooling - namely, that |> doesn't yet offer Intellisense suggestions. Using fluent style is already a thing for C# compatibility, but it's a subpar experience (type inference, the verbose fun x -> instead of x =>) and raising it to a core library makes the language look worse.

    It's totally possible to design a functional language around the fluent style - look at Kotlin, which has the it keyword and several scope functions to improve the readability of fluent chains. But F# has none of those (I believe a more ambitious "make fluent style first-class in F#" suggestion, which included such improvements, would be controversial but it would look a lot better).

    On the other side, F# has partial application, free-standing custom operators, and (at the tooling level) pipeline debugging and code lenses to support the pipeline style of coding; Kotlin lacks those, and so I would similarly oppose the introduction of |> in Kotlin.

    I also agree with the criticism that third-party libraries would suddenly be expected to provide two aliases for every function, one in each style, and it would be extremely confusing for a new programmer to find that it's neither automatic nor a given.

  17. wallymathieu commented on Sep 13, 2021

    @wallymathieu

    Perhaps a lib generator that does the code gen for certain rules? So that you given a F# style module API get extensions?

  18. Happypig375 commented on Sep 13, 2021

    @Happypig375
    Contributor

    Let's just add automatic IntelliSense for Seq module functions when we type |> after a seq type.

  19. cartermp commented on Sep 13, 2021

    @cartermp
    Member

    @Happypig375 much easier said than done :)

  20. Happypig375 commented on Sep 13, 2021

    @Happypig375
    Contributor

    @cartermp Still this would be more viable than adding fluent methods :)

  21. cartermp commented on Sep 13, 2021

    @cartermp
    Member

    What do you mean by viable? Incorporating FSharp.Core.Fluent is very easy to do. Proper, discoverable, portable IntelliSense for pipelines is very hard to do.

  22. JordanMarr commented on Sep 13, 2021

    @JordanMarr

    Adding IntelliSense for pipelines seems like it would be nearly impossible since anything with the right shape could be piped in.
    But I think part of the beauty of pipelining is that it's not constrained to only a handful of Linq methods.
    Besides, learning to use the Seq, List and Array modules is likely in every F# day-one guide anyway, and it's also very intuitive IMO.

  23. pbiggar commented on Sep 13, 2021

    @pbiggar

    Adding IntelliSense for pipelines seems like it would be nearly impossible since anything with the right shape could be piped in.

    I'm not sure I follow. If you know the type of the expression passed into the pipe, you would look for a function or var which takes that argument in it's final position.

    We do this in Darklang, it required custom code for pipes but wasnt a challenging implementation.

  24. JordanMarr commented on Sep 13, 2021

    @JordanMarr

    Sure, but what scope would you expect it to search? Through your project maybe, but it could literally be anything in the entire framework, so it would have to index everything by the last position argument type. Seems kind of unlikely to me (and unnecessary).

  25. dsyme commented on Sep 13, 2021

    @dsyme
    CollaboratorAuthor

    Sure, but what scope would you expect it to search? Through your project maybe, but it could literally be anything in the entire framework, so it would have to index everything by the last position argument type. Seems kind of unlikely to me (and unnecessary).

    I think it is reasonable to search only one level, that is offer Thing.thing whith the required type match

    A challenge might be that all Seq and List offerings would be valid.

  26. cartermp commented on Sep 13, 2021

    @cartermp
    Member

    Adding IntelliSense for pipelines seems like it would be nearly impossible since anything with the right shape could be piped in.

    Not impossible, but hard. There are several challenges, in increasing order of difficulty:

    • Adjusting how we determine a "completion context" for each kind of pipelining you can do (|>, ||>, |||>, >>)
    • Understanding which is the "last slot" in a given applicable item (function, ctor, method, etc.)
    • Getting the right kind of information about the thing being piped into something else (and constituent parts of a tuple to splat into functions)

    Part of the reason why it's hard is that at the point where completion lists are generated, we aren't working with information that makes it easy to pull apart that information. But assuming that gets done, the next step is a relatively lengthy process of carefully applying the right filtering based on the information above and if the last slot(s) in the candidate list can "match", for some definition of "match" that will include:

    • Exact type matches
    • Generics
    • Subtyping rules

    This would likely to go through a few release cycles and usage in the "real world" before it gets to a steady state. I have a very basic prototype that doesn't do this filtering and it's kinda useful, but since it just gives back the full list you still pretty much need to know what you want to do next by typing it out for the list to filter. Each kind of filtering slowly gets you to an ideal kind of list and it would take a few cycles to get that behavior to feel right.

    There's some quirks to that, like if you have a generic value and you pipe. No reasonable filtering can be applied for the first one, but the function you pipe into could then influence filtering for the next one. I don't know if people would feel like that's weird or not.

    So really it's a complicated thing that would take time to get "right". I think it should happen, which is why I have a branch that sets up basic stuff working and a framework for adding things...but if anyone thinks that this is anywhere close to approaching a similar implementation effort as the suggestion here, they'd be very much mistaken.

  27. JordanMarr commented on Sep 13, 2021

    @JordanMarr

    Sure, but what scope would you expect it to search? Through your project maybe, but it could literally be anything in the entire framework, so it would have to index everything by the last position argument type. Seems kind of unlikely to me (and unnecessary).

    I think it is reasonable to search only one level, that is offer Thing.thing whith the required type match

    A challenge might be that all Seq and List offerings would be valid.

    Maybe you could have a short list of the most commonly piped core modules (like List, Array, Seq) that could be displayed at the top of completion list if they matched. Just showing the relevant modules (either List and Seq or 'Array and 'Seq) would be a nice jumping off point to show in the completion list instead of showing the many individual functions.

    Below that, you could show individual functions that match within a reasonable scope (one level up, or maybe only within the currently opened modules).

  28. JordanMarr commented on Sep 13, 2021

    @JordanMarr

    Not impossible, but hard. There are several challenges, in increasing order of difficulty:

    That all sounds like quite a challenge, and then there would also be performance considerations. Would my poor laptop sound like it was about to launch into space the first time I opened the completion list? 🚀

  29. cartermp commented on Sep 13, 2021

    @cartermp
    Member

    No. All the data used to gather a list is already brought into scope in F# editor tooling anyways. You can see what the "full" list is just by doing ctrl+space on a blank line in F# code -- that's every single item available to you at that point in your program unfiltered

  30. dsyme commented on Sep 13, 2021

    @dsyme
    CollaboratorAuthor

    @cartermp

    for some definition of "match" that will include:

    We already do this filtering for extension methods, see IsApplicableMethApprox https://github.com/dotnet/fsharp/blob/c88b79509989ba524c41958d6d96a45951344550/src/fsharp/service/FSharpCheckerResults.fs#L469

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions