fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Allow dot notation after body of computation expression without need for parenthesis

Open TimLariviere opened this issue 2 years ago • 23 comments

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

type BuiltType =
     { SomeProperty: string }

type MyCustomCE() =
    member x.Run(...) =
        { SomeProperty = "Hello" }

// Currently doesn't compile
// But with this suggestion, it would compile fine
MyCustomCE() {
    (...)
}
    .SomeProperty // <-- Error, unexpected argument

What happens if you try to use the dot notation today:

image image

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

(MyCustomCE() {
    (...)
})
    .SomeProperty // OK -- "Hello"

let builtType =
    MyCustomCE() {
        (...)
    }

builtType.SomeProperty // OK -- "Hello"

MyCustomCE() {
    (...)
}
|> fun t ->
    t.SomeProperty // OK -- "Hello"

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.

// VStack returns an instance of a computation expression
VStack () {
    Label("Hello")
    Label("World")
}

The widgets (VStack, Label) can be altered via modifiers using the dot notation (.textColor(), .center(), etc.). It works well on regular widgets such as Label, but when trying to apply modifiers on parent widgets, we are forced to wrap them.

(VStack() {
    Label("Hello")
        .textColor(Color.Black)
        .font(size = 12.)
        .centerHorizontal()
})
    .backgroundColor(Color.Red)
    .margin(10.)

This suggestion would remove the need for the parenthesis.

VStack() {
    Label("Hello")
        .textColor(Color.Black)
        .font(size = 12.)
        .centerHorizontal()
}
    .backgroundColor(Color.Red)
    .margin(10.)

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:
I think the problem comes down to precedence - {} can mean a record/object expression (in which case your syntax would be valid)
or it can mean a CE invocation (in which case your syntax would be parsed as CE ({BODY}.method()), which isn't valid)
There would need to be some kind of rework similar to the indexer rework that allowed x[y] syntax

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] 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:

  • [x] This is not a breaking change to the F# language design
  • [x] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

TimLariviere avatar Jul 13 '22 09:07 TimLariviere

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

charlesroddie avatar Jul 19 '22 10:07 charlesroddie

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.

TimLariviere avatar Jul 19 '22 13:07 TimLariviere

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

edgarfgp avatar Jul 19 '22 13:07 edgarfgp

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?

jl0pd avatar Jul 20 '22 02:07 jl0pd

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 .

edgarfgp avatar Jul 21 '22 08:07 edgarfgp

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

Tarmil avatar Jul 21 '22 09:07 Tarmil

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

TheJayMann avatar Jul 21 '22 10:07 TheJayMann

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

TimLariviere avatar Jul 21 '22 11:07 TimLariviere

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.

TheJayMann avatar Jul 21 '22 11:07 TheJayMann

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

long example
type Color(r, g, b) = class end

module DSL =
    type IView = interface end
    type ViewBase() = interface IView
    type VStackView(items: IView[]) = inherit ViewBase()
    type ButtonView() = inherit ViewBase()

    type ViewBase with
        member v.FontSize
            with get () = 12.
            and set value = () // assume they're stored somewhere

        member v.Background
            with get () = Color(255,255,255)
            and set value = ()

        member v.Foreground
            with get() = Color(0,0,0)
            and set value = ()

        member v.Width
            with get() = 640
            and set value = ()

        member v.Height
            with get() = 480
            and set value = ()

    let view () =
        VStackView([|
            ButtonView()
            VStackView([|
                    ButtonView(
                        FontSize = 14.
                    )
                |],
                Background = Color(255,0,0),
                FontSize = 16.

            )
        |])

Properties are forced to be at beginning of code completion for some time: https://github.com/fsharp/FsAutoComplete/pull/678 image

jl0pd avatar Jul 21 '22 12:07 jl0pd

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.

TimLariviere avatar Jul 21 '22 14:07 TimLariviere

@dsyme would love to have your feedback on this suggestion . Thanks in advance

edgarfgp avatar Aug 09 '22 17:08 edgarfgp

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!

uxsoft avatar Aug 09 '22 20:08 uxsoft

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.

dsyme avatar Aug 10 '22 09:08 dsyme

@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 avatar Aug 15 '22 16:08 dsyme

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

TimLariviere avatar Aug 16 '22 06:08 TimLariviere

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.

voronoipotato avatar Aug 29 '22 15:08 voronoipotato