Open TimLariviere opened 2 years ago
I think computationExpression {}.Property
, if legal, should be against the style guide and give an analyzer message to that effect. There is a special case for Constructor().Property
but that makes more sense as people are really used to bracketed arguments binding to the thing to their left. Perhaps others would feel differently but I would have a feeling of ambiguity on reading computationExpression {}.Property
.
For the application to DSLs, in general F#, while good at creating some DSLs, can't adjust to cater to all DSL syntaxes. In this case F# syntax, by making use of advanced language features (computation expressions) slightly more verbose, encourages use of simpler language features - function arguments or settable properties (with init-only properties support in progress), and lists.
VStack(
[
Label("Hello",
TextColor = Color.Black,
FontSize = 12.,
HorizontalOrientation = Center)
],
BackgroundColor = Color.Red,
Margin = 10.
)
While I understand your point of view, as you said, I feel it's more about "what people are used to". In other languages like Swift, this kind of "dot after {} bracket" is completely normal.
I understand this change would mostly accommodate the new Fabulous DSL, but I don't feel like it's unreasonable to have in F# given today we can't do anything after a CE, and that we already can "dot-through" after other { ... }
syntaxes.
Regarding your sample code, we unfortunately had to move away from this kind of DSL (which was what Fabulous v1 was doing) for many reasons: lack of type-safety, lack of modularity, lack of extensibility, lack of "obsoleting"-capability, way more memory-heavy and disk-size heavy, incredibly harder to create your own controls, etc.
The new DSL making very specific use of computation expressions and extension methods strikes the best balance between all those very important criteria to make for a good dev experience.
The only downside we're having now is this slight inconvenience of having to wrap in parenthesis.
For the application to DSLs, in general F#, while good at creating some DSLs, can't adjust to cater to all DSL syntaxes. In this case F# syntax, by making use of advanced language features (computation expressions) slightly more verbose, encourages use of simpler language features - function arguments or settable properties (with init-only properties support in progress), and lists.
VStack( [ Label("Hello", TextColor = Color.Black, FontSize = 12., HorizontalOrientation = Center) ], BackgroundColor = Color.Red, Margin = 10. )
One of the problems that we had in V1(using option parameters) was the that the info tooltip was massive showing dozens of parameters . You could not even see what you were trying to write.
the proposed suggestion will make F# capable of match other languages like Swift and Kotlin
VStack() {
Label("Hello")
.textColor(Color.Black)
.font(size = 12.)
.centerHorizontal()
}
.backgroundColor(Color.Red)
.margin(10.)
One of the problems that we had in V1(using option parameters) was the that the info tooltip was massive showing dozens of parameters . You could not even see what you were trying to write.
Maybe it's a tooling problem then?
Maybe it's a tooling problem then?
Not really(Even if we update the various toolings(code, rider and vs) and no show optional parameters, they now becomes non discoverable . This was a consequence of the V1 DSL.
This suggestion will enhance even more the new DSL making it more ergonomic .
@TimLariviere Would something like this be a viable alternative?
VStack()
.backgroundColor(Color.Red)
.margin(10.) {
Label("Hello")
.textColor(Color.Black)
.font(size = 12.)
.centerHorizontal()
}
Syntactically it compiles, I just don't know if Fabulous's CE builders can be adapted to work like this. And I guess it would be a bit odd that VStack
has its attributes first while Label
has its content first.
I personally like the unofficial experimental DSL for Avalonia FuncUI https://github.com/uxsoft/FuncUI.Experiments. In a recent project, I had to import and edit the code as there were incompatible changes introduced between Avalonia.FuncUI 0.5.0-beta and 0.5.0 and the CE DSL had not been updated for those changes. If the Fabulous DSL were updated to match the FuncUI CE DSL (assuming it can be done simply enough), then the example would be as follows.
vStack {
backgroundColor Color.Red
margin 10.
label {
textColor Color.Black
fontSize 12.
horizontalOrientation Center
"hello"
}
}
@TimLariviere Would something like this be a viable alternative?
@Tarmil This is interesting, but it comes with a number of challenges that I'm not sure F# supports.
VStack()
is a CE, just like async
. So, is it allowed to "do things" with an instantiated CE before giving it its body?
Say async.OnUIThread() { ... }
would be allowed?
Also the problem with putting the attributes before is that each attribute needs to remember it's still a CE waiting for a body.
Today as soon as we set the CE's body, the builder gives us a regular widget letting us chain with other attributes.
Attributes that can be shared with non-CE widgets. (like .margin
, it's the same for VStack and Label)
If the Fabulous DSL were updated to match the FuncUI CE DSL (assuming it can be done simply enough), then the example would be as follows.
@TheJayMann Unfortunately I'm not sure how good is the tooling support for custom operators. Does it list all available operators when you're adding a new line? How can you enforce some attributes to be mandatory with this DSL?
While working on my current project using Ionide, I believe I have always seen the custom operators, as well as nested CE builders, appear in the list. However, they appear along side many other available namespaces, types, and values, so they can get lost if you do not have at least some idea of what you want. Also, while it has been a while since using this in Visual Studio, my memory is that, sometimes, nested builders would not appear, and also would not be keyword colored.
As far as mandatory attributes, I am not sure if there is a way within the CE itself to make certain attributes mandatory. However, if the values are simple enough, it is possible for the builder to be a function rather than a value, thus requiring values being passed in to the function before being able to use the CE builder.
Maybe it's a tooling problem then?
Not really(Even if we update the various toolings(code, rider and vs) and no show optional parameters, they now becomes non discoverable . This was a consequence of the V1 DSL.
I haven't followed design of Fabulous's DSLs, but what is wrong with class constructor approach? It can use extension properties to initialize object and therefore looks very close to current CE-style approach
For anyone wondering why we did what we did in Fabulous 2, you can find the long version of the story here: https://github.com/fsprojects/Fabulous/issues/738
Thanks everyone proposing alternative DSLs here. I want to avoid polluting too much this suggestion to the F# language.
I think the decisions we made stroke the best balance for all our requirements and constraints with all the frameworks we are working with (Xamarin.Forms & MAUI).
This suggestion is simply to make one set of parenthesis optional based on the premise that today all other ways of accessing the value already work (let
value, |>
, (CE {}).Property
) and that nothing is currently using the dot notation after a CE's body.
Maybe this change is too big, has too much impact on the language or any other reasons. In which case it's alright, we will keep using the parenthesis. But on the chance we could get those parenthesis optional in the language, it would improve the development experience in Fabulous a little bit.
@jl0pd Your suggestion is also interesting, but unfortunately the view
function in MVU is executed an awful lot of times.
This means reference types such as your ViewBase
is a no-go, or you will keep triggering GC and make your app freeze all the time.
So, we are forced to use structs ... which don't support inheritance.
@dsyme would love to have your feedback on this suggestion . Thanks in advance
I personally like the unofficial experimental DSL for Avalonia FuncUI https://github.com/uxsoft/FuncUI.Experiments. In a recent project, I had to import and edit the code as there were incompatible changes introduced between Avalonia.FuncUI 0.5.0-beta and 0.5.0 and the CE DSL had not been updated for those changes. If the Fabulous DSL were updated to match the FuncUI CE DSL (assuming it can be done simply enough), then the example would be as follows.
vStack { backgroundColor Color.Red margin 10. label { textColor Color.Black fontSize 12. horizontalOrientation Center "hello" } }
Please submit a pull request with your fixes, I'd be happy to keep this project going!
Just to note curried application
f x
.prop
The "." binds more tightly than space-as-application, same as
f x .prop
f x . prop
And
f x.prop
Given F#'s indentation aware syntax you could even reasonably imagine precedence being different if on a new line, or simply because there's a space before the ".". Either of these would be breaking changes however.
At a first look I would take the conservative position of not "fixing" this for CEs without doing it more systematically for curried application - we want regularity rather than special quirks. However I realise that doesn't help solve the immediate problem.
Regarding choice of dsl presentations - I get the problem, and how each search for a solution leads to hitting a wall just before "perfection" is reached. Equally we can't rush a "fix" just for the chosen approach.
I do sometimes wonder if a low-precedence "." symbol might help, e.g.
f x
|.prop
|.meth(3)
ce {...}
|.prop
|.meth(3)
Or even $
as some precedence separator implying parenthesization of the expression to the left.
f x
$ .prop
$ .meth(3)
ce {...}
$ .prop
$ .meth(3)
Both are imperfect and I'd imagine it would get used often. You could also imagine this
f x
|>.prop
|>.meth(3)
ce {...}
|>.prop
|>.meth(3)
Or
f x
|> .prop
|> .meth(3)
ce {...}
|> .prop
|> .meth(3)
Or from another suggestion
f x
|> _.prop
|> _.meth(3)
ce {...}
|> _.prop
|> _.meth(3)
Getting progressively heavier unfortunately.
@TimLariviere Would the last of these be satisfying enough for your purposes, i.,e.
VStack() {
Label("Hello")
.textColor(Color.Black)
.font(size = 12.)
.centerHorizontal()
}
|> _.backgroundColor(Color.Red)
|> _.margin(10.)
I can see why you'd say "no"....
@dsyme I like this notation as a general purpose short-hand for lambdas, but in the context of this DSL I would still prefer the parenthesis.
|> _.xxx()
doesn't seem very trivial to use and doesn't fit the general ergonomics of the DSL I'm building.
I feel like it will be hard to explain, especially to people not familiar with functional programming.
Saying "you need parenthesis because that's how the language works" will be easier to understand than "you can pipe the CE result and use the short-hand notation to apply modifiers, but Label is not a CE so you can use the dot-notation".
Also, some people will expect to be able to do this:
VStack() {
Label("Hello")
|> _.textColor(Color.Black)
|> _.font(size = 12.)
|> _.centerHorizontal()
}
|> _.backgroundColor(Color.Red)
|> _.margin(10.)
One of my goals with Fabulous is to attract people outside of the fp community by showing that F# is not arcane magic and can be dead simple code, straight to the point, use familiar patterns and be more expressive while giving a lot of safety compared to other languages.
I feel like for this goal parenthesis make more sense.
We could argue that "Other languages do this so we should do this" until we're all tired, and we'd still be wrong, because there's always another language that doesn't do this. I feel this way for most language features that try to sell on "other lang does this". I don't think functions are any more mysterious than dot operators. They are in fact demonstrably dumber and less complex, so I don't think it aids your argument. I think uxsoft's suggestion is a lot easier to read and understand for both experienced and beginner programmers alike. I really don't want to see F# getting more appealing to experienced OO programmers by permitting unnecessary complexity, making the language harder to read and understand. It's just going to make it harder for beginners who would have no issue understanding F#, because it's literally simple. While I understand that objects better suit your needs in this case, I think you would get the same outcome by instead writing like this as two steps and it's a lot more straightforward to read. More broadly and probably more importantly I have a concern around the value of trying to compress into one statement at the cost of increased complexity. I feel like intermediate variables are an opportunity to document intent, disambiguate and make things much easier to read. Of course my variable names aren't any good in this example because I have no idea why we're coloring the background red and setting the margin here, but you get the general idea.
let hello = VStack() {
Label("Hello")
.textColor(Color.Black)
.font(size = 12.)
.centerHorizontal()
}
let helloAlert = hello.backgroundColor(Color.Red).margin(10.)
//or...
let helloAlert =
let hello = VStack() {
Label("Hello")
.textColor(Color.Black)
.font(size = 12.)
.centerHorizontal()
}
hello.backgroundColor(Color.Red).margin(10.)
I also see functional purists make the mistake of obviating intermediate values. Programming has intent that goes beyond the strict behaviors and functionality that should be documented. If you want to remove a parentheses, intermediate values are superior to some new syntax or affordance 95% of the time, and I'd say the same to my FP purist friends. Experience can blind us to the fact that other (less experienced) people will have to read our code. Same goes for when you're reaching for <| to avoid parentheses, it's a sign that you probably actually need an intermediate variable. There are times or contexts when it genuinely makes sense, but you should be weighing intermediate values first even before parentheses, especially for code that is going to be read by juniors and mid-levels.
@dsyme Based on our feedback above, https://github.com/fsharp/fslang-suggestions/issues/1163#issuecomment-1210412579. We should not have this only for CE; a more generalized solution is needed. IMO this is worthwhile as it will be a first step to opening a discussion on a broader solution.
Could |> _.Method()
be considered a suitable alternative to this proposal? (I haven't tried F# 8, though)
seq { 1 .. 10 } |> _.GetHashCode();;
it works, and it makes me think it is also good for #999, interesting :)
edit: I mean for fluent notation that looks like F#
Could
|> _.Method()
be considered a suitable alternative to this proposal? (I haven't tried F# 8, though)
It was already suggested in the previous comments. To answer you question: it is cool that works and can be used but specifically for the spirit of this suggestion and our DSL wrapping it with parentheses is still more ergonomic than the pipe.
@edgarfgp I concur, also saw the comments from Don about this before the feature shipped.
Main question if we'd like to move forward would be about risk of parser changes for supporting this, and what do we loose, in terms of "possible later ambiguity that would preclude more 'important' feature".
It overall feels safe that at the point the "." would be considered, the parsing and typechecking has already commited to "this is a CE", so I'd personally think we could move forward and have someone who "wants this a lot", to try to hack the parser to see what occurs.
We could create an "area" label related to fluent notation, there is overall lots of friction (even if overall mild) that should be considered, and several suggestions around it.
I propose we remove the need to wrap a Computation Expression and its body in parenthesis in order to access the type built by it. Instead we should be able to "dot-through" like most other types (Record, Anonymous, etc.).
What happens if you try to use the dot notation today:
The existing way of approaching this problem in F# is by wrapping the computation expression and its body with parenthesis, or storing the result into a let value, or using
|>
.As shown on the 1st screenshot, today we can't use the dot notation at all after the body of a CE without parenthesis. Those parenthesis feel unnecessary, and seem to be only here to satisfy the current constraint of the language. It would be nice to lift this constraint.
Context
In the new DSL of Fabulous, we are now using the builder pattern and make use of Computation Expression with the implicit yield feature to represent parent - children relationship.
The widgets (
VStack
,Label
) can be altered via modifiers using the dot notation (.textColor()
,.center()
, etc.). It works well on regular widgets such asLabel
, but when trying to apply modifiers on parent widgets, we are forced to wrap them.This suggestion would remove the need for the parenthesis.
Extra information
Estimated cost: S (based on similar suggestion https://github.com/fsharp/fslang-suggestions/issues/1053 - see below)
Related suggestions:
The question regarding this suggestion has been asked by @edgarfgp on Slack. @baronfel replied with this explanation:
Allow expr[idx] as indexer/slicing syntax
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.