Skip to content
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

Add assignment bang operator #1181

Closed
5 tasks done
isaacabraham opened this issue Sep 4, 2022 · 11 comments
Closed
5 tasks done

Add assignment bang operator #1181

isaacabraham opened this issue Sep 4, 2022 · 11 comments

Comments

@isaacabraham
Copy link

isaacabraham commented Sep 4, 2022

I propose we add the ability to bind within the context of an assignment to e.g. a record.

Given functions validateCustomerId, validateName and validateCountry, all of which take in a string and return some Result (let's assume for now to keep things simple it's Result<string, string>) and you want to go from a "raw" (unvalidated) Customer to a "validated" Customer:

type ValidateCustomer = RawCustomer -> Result<ValidatedCustomer, string>

Here's what a sample implementation with this feature might look like (I'm assuming that the computation expression already exists, see e.g. FSToolkit.ErrorHandling).

let validateCustomer (raw : RawCustomer) : Result<ValidatedCustomer, string> =
    result {
        return
            {
                Id =! validateCustomerId raw.CustomerId
                Name =! validateName raw.Name
                Country =! validateCountry raw.Country
            }
    }

The existing way of approaching this problem in F# is to explicitly let bang for each validation function, bind to a symbol and then immediately afterwards assign into a record which is returned back out of the CE.

let validateCustomer raw =
    result {
        let! customerId = validateCustomerId raw.CustomerId
        let! name = validateName raw.Name
        let! country = validateCountry raw.Country

        return
            {
                Id = customerId
                Name = name
                Country = country
            }
    }

We've already seen support for match! to short-circuit a very similar pattern and I wonder if something like this would also be of use.

I'm not especially fond of =! although it follows the standard pattern of placing ! after the function etc.. Totally happy to bikeshed the best way to "expose" this to the developer but I'm more interested if in principle this is a "good thing" or not.

Pros and Cons

The advantages of making this adjustment to F# are that code becomes a little more succinct, and we reduce boilerplate. From a pedagogic point of view, I think it would be easy to learn in the sense of "just put ! after the = whenever you have a result [or option or async etc.]) and it just works".

The disadvantages of making this adjustment to F# are another way to do things, an extra operator to learn, plus the effort to implement this. I also may be naively overlooking some breaking change that this could introduce or extra complexity in the compiler etc. - so am fully prepared for this to be shot down on the grounds of "this just isn't possible" (as well as "this is a terrible idea, let's forget about this" :-)).

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S. I suspect that this is more a syntactic sugar level feature than adding something magical into the language.

Related suggestions: I've been directed to #1070 . This is a similar - but more ambitious - feature. If implemented, it would (I believe) solve this issue as well.

Affidavit (please submit!)

Please tick this by placing a cross in the box:

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

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

@Hardt-Coded
Copy link

Hardt-Coded commented Sep 4, 2022

Oh I like it :D

Then it's also possible to do:

let value =! something()

instead of:

let! value = something()

But why not. I am totally fine with it.

@thinkbeforecoding
Copy link

One of my concerns with this is that we add new syntax for each construct.

The await syntax from C# is actually really smart as it handle all these cases with a single keyword.

I don't have a good name for it yet, but let's call it bang! for now,

we could could write:

result { return f (bang! x) }

and it would be similar to

result { let! y = x; return f y }

or for your example:

let validateCustomer (raw : RawCustomer) : Result<ValidatedCustomer, string> =
    result {
        return
            {
                Id = bang! validateCustomerId raw.CustomerId
                Name = bang! validateName raw.Name
                Country = bang! validateCountry raw.Country
            }
    }

It would work for if, match, function arguments, various computations.

It would also not require extra methods on the builder as it can just use Bind, bang! x becomes result.Bind(x, (fun y -> (* same expression, but with y instead of bang! x *) ))

@thinkbeforecoding
Copy link

This has actually already been suggested here: #1070

@isaacabraham
Copy link
Author

Just seen and updated the issue description to link to that.

@l3m
Copy link

l3m commented Sep 12, 2022

For me, this looks like injecting some magic. I feel the example with 3x explicit let! and a simple record construction is more readable, as it makes clear there is something else going on. It's less surprising.

@cspaniard
Copy link

cspaniard commented Sep 13, 2022

I don't have a good name for it yet, but let's call it bang! for now,

Please, this would great. We could have things like pipe without an intermediate let! binding.

task {
    bang! getNetworksAsync()
    |> Array.iter (fun n -> DoSomethingWith n.ToString())
}

This would be the same motivation as having match!, but for pipes.
I find myself having to use intermediate let! bindings all the time, just for the "await" within task {}.

This syntactic sugar would be extremelly useful.

@LyndonGingerich
Copy link

Just toying, but here's one idea:

let validateCustomer (raw : RawCustomer) : Result<ValidatedCustomer, string> =
    result {
        return
            {
                Id = (validateCustomerId raw.CustomerId)!
                Name = (validateName raw.Name)!
                Country = (validateCountry raw.Country)!
            }
    }

@AlexeyRaga
Copy link

AlexeyRaga commented Sep 14, 2022

Need to be careful with it because with these =! and bang! what the semantics are? Monadic or applicative?
It'd be a bit strange if only one of them is supported.

The proposal is triggering the monadic behaviour, but when I think about that, specifically for setting records members, there cannot be dependencies between results of computing functions, so it ought to be applicative (think of validation scenarios as one of the examples)!

@lucasteles
Copy link

I think the =! is not a good fit, too close to != which is a common not equal operator

@auduchinok
Copy link

This is not a breaking change to the F# language design

It's a valid custom operator today, so it's actually a breaking change.

@dsyme
Copy link
Collaborator

dsyme commented Apr 13, 2023

I have marked #1070 as approved-in-principle, which subsumes this one really.

@dsyme dsyme closed this as completed Apr 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests