fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
341 stars 20 forks source link

Syntax sugar for collection types #1158

Open wilbennett opened 2 years ago

wilbennett commented 2 years ago

I propose we ... (describe your suggestion here)

Add some syntax sugar to the creation of collections. This will make some DSLs cleaner and a LOT easier to refactor.

The existing way of approaching this problem in F# is ...

Here is a sample DSL snippet from Feliz:

    [<ReactComponent>]
    static member Counter() =
        let (count, setCount) = React.useState(0)

        Html.div [
            prop.style [
                style.backgroundColor color.aliceBlue
            ]
            prop.children [
                Html.h1 count
                Html.button [
                    prop.style [
                        style.marginLeft 5
                        style.marginRight 5
                    ]
                    prop.onClick (fun _ -> setCount(count - 1))
                    prop.text "Decrement"
                ]
                Html.button [
                    prop.style [
                        style.marginLeft 5
                        style.marginRight 5
                    ]
                    prop.onClick (fun _ -> setCount(count + 1))
                    prop.text "Increment"
                ]
            ]
        ]

That is only a label and two buttons! The noisiness of the brackets makes this code harder than necessary to read and refactor (this is just a simple example - add more functions and it gets really crazy!). F# already is indentation aware so I believe it can leverage that to provide a much cleaner output without having to try to match up end tokens (which is even more of a problem when pasting from somewhere else).

    [<ReactComponent>]
    static member Counter() =
        let (count, setCount) = React.useState(0)

        Html.div [[
            prop.style [[
                style.backgroundColor color.aliceBlue

            prop.children [[
                Html.h1 count
                Html.button [[
                    prop.style [[
                        style.marginLeft 5
                        style.marginRight 5

                    prop.onClick (fun _ -> setCount(count - 1))
                    prop.text "Decrement"

                Html.button [[
                    prop.style [[
                        style.marginLeft 5
                        style.marginRight 5

                    prop.onClick (fun _ -> setCount(count + 1))
                    prop.text "Increment"

The idea is that we can use indentation to end the definition of collections instead of requiring closing tokens. The start tokens can be whatever makes sense - I used [[ for list in the prior example. Here are some usages with different tokens:

    Parameter is a list:
        Html.div -[
            Html.h1 "hello"
            Html.h2 "World"

    Parameter is a sequence:
        Html.div -{
            Html.h1 "hello"
            Html.h2 "World"

    Parameter is an array:
        Html.div -[|
            Html.h1 "hello"
            Html.h2 "World"

Pros and Cons

The advantages of making this adjustment to F# are ...

Code will be much less noisy, easier to poor man's refactor (cut & paste), and I believe more readable.

The disadvantages of making this adjustment to F# are ...

Could be confusing?

Extra information

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

?

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.

Happypig375 commented 2 years ago

Alternative that does not need any new syntax:

    [<ReactComponent>]
    static member Counter() =
        let (count, setCount) = React.useState(0)
        Html.div <| fun prop ->
            prop.style <| fun style ->
                style.backgroundColor color.aliceBlue
            prop.children <| fun html ->
                html.h1 count
                html.button <| fun prop ->
                    prop.style <| fun style ->
                        style.marginLeft 5
                        style.marginRight 5
                    prop.onClick (fun _ -> setCount(count - 1))
                    prop.text "Decrement"
                html.button <| fun prop ->
                    prop.style <| fun style ->
                        style.marginLeft 5
                        style.marginRight 5
                    prop.onClick (fun _ -> setCount(count + 1))
                    prop.text "Increment"
uxsoft commented 2 years ago

Another option, my personal favorite for UI is using CE's:

This way is also already possible and no language changes are needed.

[<ReactComponent>]
let Counter () =
    let (count, setCount) = React.useState(0)
    Html.div {
        style
            [ style.backgroundColor color.aliceBlue ]

        Html.h1 { count }
        Html.button {
            style
                [ style.marginLeft 5
                  style.marginRight 5 ]
            onClick (fun _ -> setCount(count - 1))
            text "Decrement"
        }
        Html.button {
            style
                [ style.marginLeft 5
                  style.marginRight 5 ]
            onClick (fun _ -> setCount(count + 1))
            text "Increment"
        }
    }
Happypig375 commented 2 years ago

@uxsoft Your code has a brace mismatch error.

uxsoft commented 2 years ago

@uxsoft Your code has a brace mismatch error.

Thanks, fixed.

WilBennettJr commented 2 years ago

@uxsoft Your brace mismatch illustrates my point. Who wants to keep track of matching end tokens?

I would love to be able to do this kind of stuff with CEs but there are two problems.

  1. Intellisense is extremely lacking compared to the class/module approach
  2. You show nested CEs but as far as I know, that is not implemented in the language (at least not in that form - would need a let binding)
WilBennettJr commented 2 years ago

@kerams care to explain the downvotes? Is it the syntax or the idea in general?

WilBennettJr commented 2 years ago

@uxsoft BTW, if you do have an example of working CEs that nest like that, please share! I've been trying to do it with no success.

Happypig375 commented 2 years ago

@WilBennettJr What about my code?

WilBennettJr commented 2 years ago

Oh, sorry @Happypig375, that approach is nice as well. It is a little bit more verbose but I believe Fable uses that technique too - for selectively populating a record, I think. It definitely makes copy/paste easier. I'm tempted to use that instead of classes for my next DSL. I think I can live with being more verbose than I can with trying to match up end tokens. :)

Thanks for the suggestion!

uxsoft commented 2 years ago

BTW, if you do have an example of working CEs that nest like that, please share! I've been trying to do it with no success.

Library for Fable: https://github.com/uxsoft/fable.builders.antdesign Example: https://github.com/uxsoft/Fable.Builders.Website

Who wants to keep track of matching end tokens?

Never had an issue with that, all IDE's I used do a pretty good job at brace matching

nikoyak commented 2 years ago

BTW, if you do have an example of working CEs that nest like that, please share! I've been trying to do it with no success.

Library for Fable: https://github.com/uxsoft/fable.builders.antdesign Example: https://github.com/uxsoft/Fable.Builders.Website

Who wants to keep track of matching end tokens?

Never had an issue with that, all IDE's I used do a pretty good job at brace matching

BTW, why not extend the proposed syntax sugar to CEs? It smells of the C spirit from the abundance of curly braces. ;-) Example from https://github.com/uxsoft/fable.builders.antdesign :

open Fable.Builders.AntDesign

let view model dispatch =
    Content {
        PageHeader {
            title (str "Login")
            subTitle (str "Please log-in to enter.")
        }

        Form {
            style [ MaxWidth "320px"; Margin "0 auto" ]
            onFinish (fun values -> dispatch (BeginLogin(string values.["username"], string values.["password"])))

            FormItem {
                name "email"
                key "login-email"
                rules [
                    [ FormRule.RuleType FormRuleType.Email 
                      FormRule.Message "This isn't a valid email" ]
                    [ FormRule.Required true
                      FormRule.Message "This field is mandatory" ] ]
                Input {
                    prefix (basicIcon icons.MailOutlined { style [ Color "lightgray" ] })
                    placeholder "Email"
                }
            }

            FormItem {
                name "password"
                key "login-password"
                rules [
                    [ FormRule.Required true
                      FormRule.Message "This field is mandatory" ] ]
                Password {
                    prefix (basicIcon icons.LockOutlined { style [ Color "lightgray" ] })
                }
            }

            FormItem {
                key "login-submit"
                Button {
                    style [ Width "100%" ]
                    buttonType ButtonType.Primary
                    loading model.IsLoggingIn
                    htmlType ButtonHtmlType.Submit 

                    str "Login"
                }
            }

            FormItem {
                key "login-links"
                Button {
                    buttonType ButtonType.Link
                    str "Register"
                }
                Button {
                    style [ Float FloatOptions.Right ]
                    buttonType ButtonType.Link
                    str "Forgot password?"
                }
            }
        }
    }

could be rewritten like this:

open Fable.Builders.AntDesign

let view model dispatch =
    Content |{
        PageHeader |{
            title (str "Login")
            subTitle (str "Please log-in to enter.")

        Form |{
            style [ MaxWidth "320px"; Margin "0 auto" ]
            onFinish (fun values -> dispatch (BeginLogin(string values.["username"], string values.["password"])))

            FormItem |{
                name "email"
                key "login-email"
                rules |[
                    |[ FormRule.RuleType FormRuleType.Email 
                       FormRule.Message "This isn't a valid email"
                    |[ FormRule.Required true
                       FormRule.Message "This field is mandatory"
                Input |{
                    prefix (basicIcon icons.MailOutlined { style [ Color "lightgray" ] })
                    placeholder "Email"

            FormItem |{
                name "password"
                key "login-password"
                rules |[
                    |[ FormRule.Required true
                       FormRule.Message "This field is mandatory"
                Password |{
                    prefix (basicIcon icons.LockOutlined { style [ Color "lightgray" ] })

            FormItem |{
                key "login-submit"
                Button |{
                    style [ Width "100%" ]
                    buttonType ButtonType.Primary
                    loading model.IsLoggingIn
                    htmlType ButtonHtmlType.Submit 

                    str "Login"

            FormItem |{
                key "login-links"
                Button |{
                    buttonType ButtonType.Link
                    str "Register"
                Button |{
                    style [ Float FloatOptions.Right ]
                    buttonType ButtonType.Link
                    str "Forgot password?"

or even extreme:

open Fable.Builders.AntDesign

let view model dispatch =
    Content |{
        PageHeader |{
            title (str "Login")
            subTitle (str "Please log-in to enter.")

        Form |{
            style |[ MaxWidth "320px"; Margin "0 auto"
            onFinish (fun values -> dispatch (BeginLogin(string values.["username"], string values.["password"])))

            FormItem |{
                name "email"
                key "login-email"
                rules |[
                    |[ FormRule.RuleType FormRuleType.Email 
                       FormRule.Message "This isn't a valid email"
                    |[ FormRule.Required true
                       FormRule.Message "This field is mandatory"
                Input |{
                    prefix (basicIcon icons.MailOutlined |{ style |[ Color "lightgray" )
                    placeholder "Email"

            FormItem |{
                name "password"
                key "login-password"
                rules |[
                    |[ FormRule.Required true
                       FormRule.Message "This field is mandatory"
                Password |{
                    prefix (basicIcon icons.LockOutlined |{ style |[ Color "lightgray" )

            FormItem |{
                key "login-submit"
                Button |{
                    style |[ Width "100%" 
                    buttonType ButtonType.Primary
                    loading model.IsLoggingIn
                    htmlType ButtonHtmlType.Submit 

                    str "Login"

            FormItem |{
                key "login-links"
                Button |{
                    buttonType ButtonType.Link
                    str "Register"
                Button |{
                    style |[ Float FloatOptions.Right
                    buttonType ButtonType.Link
                    str "Forgot password?"

-19% lines of code without sacrificing readability. I like it.

Tarmil commented 2 years ago

I'm not completely against the idea of something like this, but I definitely don't like the specific proposed syntax. All these open braces without corresponding close braces are going to confuse the hell out of every text editor not specifically tuned to understand them. And probably out of beginners too.

bisen2 commented 1 year ago

Using an opening-only collection syntax feels like a footgun to me. For example, here is a case where the compiler can detect that a line is incorrectly indented and raise an error:

List.sum [
    1
    List.sum [
        2 // error: this value is not a function and cannot be applied
    3
    ]
]

With an opening-only syntax, incorrect indentation would actually change the logic, not cause an error.

List.sum [[
    1
    List.sum [[
        2
    3 // incorrect indenting causes `3` to be part of the outer collection

In my opinion, the confusion of adding an opening-only collection syntax would far outweigh any potential gains. If the goal is just to remove closing braces on dedicated lines, there are styles that can do this without sacrificing the benefits of a closing brace.

Html.button [
    prop.onClick (fun _ -> setCount(count + 1))
    prop.text "Increment" ]
// or
Html.button
  [ prop.onClick (fun _ -> setCount(count + 1))
    prop.text "Increment" ]
WilBennettJr commented 1 year ago

@bisen2 @Tarmil

Thanks for your input! Sorry for the late reply.

I'm fine with whatever syntax achieves the underlying goal. Showing opening braces is just an example to get the point across. F# already supports indentation based grouping and only adds these legacy C style relics in a few places (CEs, records, etc...).

To your example specifically @bisen2, this is no different than declaring nested functions - replace "List.sum" with a "let f () = ..." and it's the same concerns as everyone already deals with today. Your example may be an error or it may not be - that particular example is valid as it is. Again, the same as if it were nested functions.

Really.... Think about it for a minute.... Imagine a world where F# had braces when defining functions and this suggestion is to use indentation instead. You are basically saying it's a bad idea, the IDEs won't understand, etc... Yet, that's what it is today and I don't think anyone has an issue with it! As for newbies, if they don't have an issue with module/function/DU indentation, why would they have a problem with this? If anything, it's more consistent. As a newbie, I had to remember: use braces when defining a record but not when defining a class or interface. Not that it's a big deal, but that does not seem "easier" to me. Even C# is consistent there (except for the new syntax sugar now).

Having closing braces on separate lines or not is not the issue. Yes, typing the code in the IDE is fine. Where you run into issues is when you want to read/understand/refactor. If you have a whole stack of nested code like this and you want to copy a portion into a whole stack of other nested code, you end up shuffling end tokens most of the time. Personally, I think it makes the code noisy and adds no real benefit - the same argument for not having braces when defining functions. It seems a bit ironic actually. This is one of the big things touted as to why F# is easier to read and has "smaller" code than C# so I don't understand why there would be opposition since it is used practically everywhere else (module, function, DU, etc...)!

Not to keep harping on the similarities here but I'll say the following. If someone can make an argument against this that doesn't also apply to not having braces for modules/functions/DUs, I think the opposition will make more sense to me. Thinking about it a bit abstractly, a module is a list of functions, a function a list of expressions (including other functions), and a DU is a list of cases. None of these require delimiting tokens. Why does record/CE even use them? Classes/interfaces don't.

I look forward to reading your responses and thanks again for your input.

WilBennettJr commented 1 year ago

BTW, if you do have an example of working CEs that nest like that, please share! I've been trying to do it with no success.

Library for Fable: https://github.com/uxsoft/fable.builders.antdesign Example: https://github.com/uxsoft/Fable.Builders.Website

Who wants to keep track of matching end tokens?

Never had an issue with that, all IDE's I used do a pretty good job at brace matching

@uxsoft apologies for the late reply.

That is awesome! I haven't had time to look at it in depth yet. I hope you don't mind if I steal the technique! I'll have to see how well it plays with intellisense and how discoverable it is for a newbie to a DSL like this. I'm optimistic.

WilBennettJr commented 1 year ago

Oh, also @bisen2:

I couldn't simply copy and paste from your last examples. I would have to remove either the leading or trailing tokens, depending on the line I copied.

To be clear, it's not like these are end of the world type concerns. Just that it interrupts your flow and seem totally unnecessary to me.

wilbennett commented 1 year ago

Using keywords instead of brackets:

    Parameter is a list:
        Html.div listof
            Html.h1 "hello"
            Html.h2 "World"

    Parameter is a sequence:
        Html.div seqof
            Html.h1 "hello"
            Html.h2 "World"

    Parameter is an array:
        Html.div arrayof
            Html.h1 "hello"
            Html.h2 "World"
cartermp commented 1 year ago

I agree with @Tarmil, and to add to this:

I'm not completely against the idea of something like this, but I definitely don't like the specific proposed syntax. All these open braces without corresponding close braces are going to confuse the hell out of every text editor not specifically tuned to understand them. And probably out of beginners too.

Two additional things to consider:

And so an alternative to using close braces would have be eminently readable, discoverable, and understandable to be accepted in my opinion. I don't think the closest might be the plain english keywords, but that itself would imply massive changes elsewhere, supplanting symbolic syntax for type declarations and clarifying the scope of an expression.