-
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
Consider incorporating FSharp.Core.Fluent into FSharp.Core #1073
Comments
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:
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. |
I generally love the idea (adding popular module functions as extension methods) but not a fan of |
I think this well help with autocomplete since you write dot and then you can discover what operations you can apply. |
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 😋 |
Another con of fluent is more annotations are normally required, which sort of spoils the flow having to go back and add an annotation. |
It would be nice but as @cartermp said:
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? |
Since we now have pipeline debugging/stepping, it's a clear advantage to use them when diagnosing code. So imagine code like this:
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. |
@cartermp makes sense; thanks for elaborating! |
I don't like this at all! :)
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
VSCode (etc) could be made suggest a function with the appropriate type when you press '.'; after the completion, it would replace the
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 😊 |
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 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. |
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. |
Another way to look at this is that when people see that |
I can see why this is tempting. |
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? |
As a beginner that just recently finished F# From the Ground Up with no previous functional programming experience I don't think this is an issue. Having knowledge about what |> is or that there is a module with the same name of the type is just like knowing that you use |
The problem is, this bit is not true.
That is all fine - much of this you need to learn sooner or later - but it really is vastly more depth of understanding than
and you're done.
Regarding camelCase v. PascalCase - we would not do this for PascalCase As an aside, DiffSharp supports both |
There is an alternative that might address some of the issues raised above. Instead of adding new boilerplate methods that call the module functions, what if we add a way to allow the existing functions to be used as extension methods? For instance, when designing libraries for use in both F# and C#, I'll often do something like this: [<Extension>]
module List =
[<Extension>] // This is fine: lst1 is "this"
let append lst1 lst2 = lst1 @ lst2
// [<Extension>] // Doesn't work: we want `lst` to be "this", not `fn`
let map fn lst = (* ... *) The first case works because the first argument is the one we want to be used as "this." If we extend the extension method syntax to allow any argument to be used as "this," then we could do something like: // Pretend syntax- we could use a different attribute
let map fn ([<Extension>] lst) = (* ... *)
// Now you can do ...
let foo = [1; 2; 3].map (fun x -> x * 2)
|
Fair enough. It is certainly true that I'm an F# beginner but not a C#/.NET beginner so all that stuff was and is pretty natural to me except for the last point about |
I agree that this is a mess, but I submit that adding yet another thing, that also works in some cases but not others, adds to the mess instead of cleaning it up. In fact, it would be a big improvement to the stdlib if the rule that "there is a module with the same name of the type" were true in all cases instead of merely relatively often. |
I'm kind of ambivalent. From the perspective of a C# developer switching to F# it could make sense to import xs.Select(fun x -> x+1)
.Where(fun x -> x > 4)
.OrderBy(fun x -> x) The option to write this style of code already exists using standard libraries. The fluent style API looks like you remove the As a consumer of the fluent API, the assumption would be that it's complete: That given all of the F# standard library methods that you could want to use in a fluent style could be used in a fluent style. Perhaps some sort of generated fluent API surface would make sense (in a specific namespace)? |
With |> you can pipe any function with relevant input, while with fluent style you can not. |
Unfortunately this isn't possible for all of, say, ImmutableCollections. |
I strongly dislike the idea of adding, not a second, but a third way of doing core list manipulation to F#. New users can already open System.Linq if they want fluent. And in fact my coworker, who is currently writing his first F# project for a large client, instinctively reached for Linq. I think it’s great that he was able to leverage his existing knowledge to get things done in a pragmatic way, but I urged him to refactor to use the F# module functions and pipeline for a more idiomatic approach (and more importantly to embrace F# usage of Option types). What bothers me the most when teaching F# is having to give nuanced history lessons of why there are multiple ways of doing the same thing. For example: async vs tasks; curried vs tupled args. Another reason I dislike this idea is that it is so easy and elegant to create pipelines, whereas it is tedious and difficult to create fluent APIs, to the point where you seldom see fluent APIs in a code base unless it’s a public facing API library where someone wants to go out of their way to do a lot of work up front to make it easy for consumers. (there has to be a real incentive to create fluent APIs to make it worth the huge inconvenience.) My point here is that it’s trivially easy for F# devs to create elegant pipelines of their own that look like first class citizens alongside the core module functions, whereas embracing fluent API will drive a wedge down the most common and idiomatic workflows of F# core. The thought of having to constantly refactor fluent list manipulations back to pipelines really triggers me. Pipelines are such a celebrated feature amongst F# developers, to the point where they use the forward pipe operator on t-shirts, hats and coffee mugs! This is fixing something that is not broken, and even if it adds some level of convenience, that pragmatism will come with a cost to the perceived beauty of the language. |
For me, the initial/instinctive reaction was "No, do not mess with my beloved |>" (I do not have a T-shirt or a mug with a |> on it, but to me |> is just pure beauty) Then I tried to rationalize my vote. Not easy. I read all the comments (more than once). Still not easy to articulate a convincing explanation of my reaction. I guess sometimes you gotta do/say what your instinct suggest. :) Btw, I loved the idea of intellisense/autocmpletion/suggestions after |> suggested by @cartermp. |
TBH I would prefer that type discoverability in all IDEs would be improved and then this suggestion is not really that valuable. |
F# Fluent API as a library seems legit. When we compare pros and cons, pros are focused on learning purpose and for beginners. On the other hand, cons are related to the production code... |
This should not be done. If a language does not believe in itself and its view point on how to achieve a goal then what is the point? More practically,
|
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 It's totally possible to design a functional language around the fluent style - look at Kotlin, which has the 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 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. |
Perhaps a lib generator that does the code gen for certain rules? So that you given a F# style module API get extensions? |
Let's just add automatic IntelliSense for |
@Happypig375 much easier said than done :) |
@cartermp Still this would be more viable than adding fluent methods :) |
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. |
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. |
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 A challenge might be that all |
Not impossible, but hard. There are several challenges, in increasing order of difficulty:
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:
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. |
Maybe you could have a short list of the most commonly piped core modules (like Below that, you could show individual functions that match within a reasonable scope (one level up, or maybe only within the currently opened modules). |
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? 🚀 |
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 |
We already do this filtering for extension methods, see |
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/
Pros and Cons
The advantages of making this adjustment to F# are
xs.sum()
relies only on dot notation, c.f.xs |> Seq.sum
requires knowlesdge of|>
andSeq
, and knoweledge that input collections supportSeq
/IEnumerable
programmingThe disadvantages of making this adjustment to F# are
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:
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.
The text was updated successfully, but these errors were encountered: