Open dsyme opened 3 years ago
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.
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.
I think this well help with autocomplete since you write dot and then you can discover what operations you can apply.
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 😋
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:
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?
@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.
@cartermp makes sense; thanks for elaborating!
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 😊
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.
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.
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.
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.
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?
conceptually simpler options for beginner code, xs.sum() relies only on dot notation, c.f. xs |> Seq.sum requires knowlesdge of |> and Seq
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 let
to bind to a value or a function. It's part of the basics of game. I remember that I had issues with things like partial application or getting used to this level of type inference (I still don't know why the error "lookup on error of indeterminate type" shows up even though Ionide infers the right type but thanks to Eason I learnt I can fix it with a type annotation). Furthermore I think that assuming Fluent was already part of Core and Eason would have showed me both approaches (because if Fluent is in Core then as Biggar said newbies will expect to learn about it in any learning material they get) then my reaction would have been "why are there two ways to do the same thing? Is there a difference?" and start googling to learn what the differences are and when I should NOT use X and stick to Y.
@dggmez
or that there is a module with the same name of the type
The problem is, this bit is not true.
Dictionary
or ResizeArray
or System.Collections.Generic.List
Dictionary
or ResizeArray
or System.Collections.Generic.List
moduleSeq
(which is also called IEnumerable
and IEnumerable<T>
Seq
thing is good for everything, except you don't use it for List
and Array
somehow. |>
you may need to know about currying and partial application and first-class function types.That is all fine - much of this you need to learn sooner or later - but it really is vastly more depth of understanding than
.
sum()
and you're done.
I generally love the idea but not a fan of camelCase method names, especially in the standard library.
Regarding camelCase v. PascalCase - we would not do this for PascalCase Map
, Filter
etc. So it would be normalizing the idea that F#-specific APIs can use foo.camelCase()
. Guidance would be needed.
As an aside, DiffSharp supports both dsharp.sum(tensor)
and tensor.sum()
systematically for all, I've found it very pleasant the worst thing being it requires duplication of documentation. Also Python API design actually has this problem too, with PyTorch supporting both torch.sum(tensor)
and tensor.sum()
, likewise all other operations.
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)
@dsyme
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 |>
, currying, partial application, etc. For other beginners it's a different ball game.
or that there is a module with the same name of the type
The problem is, this bit is not true.
- Assume you're starting with
Dictionary
orResizeArray
orSystem.Collections.Generic.List
- Now there is no
Dictionary
orResizeArray
orSystem.Collections.Generic.List
module- So what do you do? You have to understand that these things all implement this thing called
Seq
(which is also calledIEnumerable
andIEnumerable<T>
- And then you have to loosely understand the idea of subtyping (which is a complex and slippery concept that Python and initial programmers will be very hazy on)
- And you have to understand that this
Seq
thing is good for everything, except you don't use it forList
andArray
somehow.- If you also want to dig into
|>
you may need to know about currying and partial application and first-class function types.That is all fine - much of this you need to learn sooner or later - but it really is vastly more depth of understanding than
- typing
.
- selecting
sum()
and you're done.
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 System.Linq
and write
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 |> List.
and replace it with .
instead.
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.
@pbiggar
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.
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. But as part of the Core, I strongly dislike the idea too.
When we compare pros and cons, pros are focused on learning purpose and for beginners. And I'm not even sure for which kind of beginners. Those who are familiar with fluent api (not a lot) or any kind of beginners that in this case having an explicit api and one way of doing things, in my opinion is preferable. And I dont know why the discovery would be an issue if every thing is explicit. Also even if enumerators are a very important notion of dotnet, they are rarely used in F# and usually for consuming C# apis.
On the other hand, cons are related to the production code... It's interesting to have a more succinct code for some rare one-liners but at which cost?
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 |>
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.
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 Seq
module functions when we type |>
after a seq type.
@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.
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.
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).
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.
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:
|>
, ||>
, |||>
, >>
)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.
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 matchA challenge might be that all
Seq
andList
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 'Arrayand '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).
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? 🚀
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
@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
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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.