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

Simplify use of child scopes inside computation expressions #1000

Open
5 tasks done
SchlenkR opened this issue Apr 23, 2021 · 8 comments
Open
5 tasks done

Simplify use of child scopes inside computation expressions #1000

SchlenkR opened this issue Apr 23, 2021 · 8 comments

Comments

@SchlenkR
Copy link

SchlenkR commented Apr 23, 2021

I propose to add a mechanism that allows for implicit use of builder methods in child scopes when being inside of a computation expression.

Today, we have to start new CEs when not being at top level scope of a surrounding CE.

In this example, a second task workflow has to be started in order to be usable in the child scope of the let! b ... body:

task {
    let! a = Task.FromResult 1.0
    // a new language scope means also: start new CE
    let! b = task {
        let! c = Task.FromResult 2.0
        let! d = Task.FromResult 3.0
        return c + d
    }
    return a / b
}

This would become:

task {
    let! a = Task.FromResult 1.0
    let! b =
        // 'task' builder instance (or a 'fork' of it) is implicitly used
        let! c = Task.FromResult 2.0
        let! d = Task.FromResult 3.0

        // return might also be implicit
        c + d
    return a / b
}

In addition, it could be possible to introduce an anonymous bang, so that this:

task {
    let! three = task {
        let! tmp = Task.FromResult 1
        return tmp + 2
    }
        
    return three
}

becomes this:

task {
    let! three = (!! Task.FromResult 1) + 2
    return three
}

(I use !! in the example as a placeholder for the anonymous-bang operator.)

Pros and Cons

  • Reduce boilerplate code
  • The syntax feels more natural, since using scopes is a common thing in F#
  • It would simplify both readability and writability of code. Think of an update monad: It would almost look like imperative code. Writing such code today, I got interrupted by declaring new "update" workflows all the time.
  • The anonymous-bang would increase the expressiveness of computations where arithmetic operations are a key feature and it would make custom arithmetic operators like .+, .*, etc. ambiguous. In C#, this is also possible today, where you can write something like this: (await Task.FromResult(new [] {1,2,3})).Select(x => x + 1);
  • In other languages that have async/await hardcoded (e.g. C# and Typescript), this is already possible today and I personally use those features often.

The disadvantages of making this adjustment to F# are not obvious to me from a users point of view.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): I can't tell because I'm not familiar with the F# compiler code. I guess it might be quite complex (L - XL?)

Related suggestions:

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.

@smoothdeveloper
Copy link
Contributor

I really like that fsharp allows to do this:

let step(objects:List<MassObject>, newObjects:List<MassObject>, i:int, h:float) =
    let awsum, vwsum =
        // ... lots of intermediates
    newObjects.[i].position <- newObjects.[i].position + h*vwsum
    newObjects.[i].velocity <- newObjects.[i].velocity + h*awsum

out of

let step(objects:List<MassObject>, newObjects:List<MassObject>, i:int, h:float) =
    // everything under the same scope
    // ... lots of intermediates
    let awsum = // ...
    let vwsum = // ...
    newObjects.[i].position <- newObjects.[i].position + h*vwsum
    newObjects.[i].velocity <- newObjects.[i].velocity + h*awsum

So translating this into CE land to remove the noise is a great idea.

(I use !! in the example as a placeholder for the anonymous-bang operator.)

If that was another suggestion, I would not upvote that separate suggestion, so I'm ambivalent and biased against the anonymous-bang operator just on the face of it looking a bit alien / causing operator soup.

@SchlenkR
Copy link
Author

Please don't nail me on that operator or on the concrete syntax. It could also be a keyword or whatever. The point I tried to make is:

We should provide syntactic sugar for

Monad.map (e1) (fun x -> e2)

in a way where e1 can be implicitly used like "x" in e2 without binding it to "x", and without using the Map method of the builder. As I mentioned in the "Pros and Cons", this is already possible in other languages.

@smoothdeveloper
Copy link
Contributor

@ronaldschlenker not nailing you, sorry!

Just trying to be thorough so that my upvote comes with more context. Looking for other approach than !! to reevaluate or just postponing it, but I think it is a detail for the overall suggestion.

I'll try to better understand the fineprint of the generalization you are conveying with that aspect, but it may take me more time, like unbounded 🙂

@SchlenkR
Copy link
Author

No need be bo sorry, @smoothdeveloper ! I thought "nailing" was more like a joking term - so absolutely no offense from my side! :)

I'll try to better understand the fineprint of the generalization you are conveying with that aspect, but it may take me more time, like unbounded

I don't quite understand what you mean. In case my explanation was not clear, I try to clarify:

This

let result : Task<int> = (!! Task.FromResult 1) + 2

could be translated to this:

let result : Task<int> = Task.map (Task.FromResult 1) (fun x -> + 2)

@kerams
Copy link

kerams commented Apr 23, 2021

It's also very annoying with branches

let getData id includeAdditional = async {
    let! data = get id

    // nope :(
    // let additional =
    //     if includeAdditional then
    //         let! x = getAdditional id
    //         x
    //     else
    //         []

    // instead
    let! additional =
        if includeAdditional then
            getAdditional id
        else
            async { return [] } // oh no, pointless allocation and overhead

    return data, additional
}

@smoothdeveloper
Copy link
Contributor

I don't quite understand what you mean. In case my explanation was not clear, I try to clarify:

Thanks for the additional variation, I was implying at the stage I am, maybe as it is hard to grok >> the first time you encounter concept of point free function composition.

I'm not super versed with CE and still susceptible to trip on dealing with the binding and having the right semantic when I nest them.

The main suggestion, I'm confident it will help me, the !! syntactic sugar to (conceptually) bring it point free is a bit hard for me to parse, but it still looks powerful and useful; waiting to see it in real world code I'm stepping through I guess to solidify my opinion on that detail.

@Happypig375
Copy link
Contributor

I think a better name for the operator would be id!, because all other operators like let! are just modified let.

let a = id! 1

equivalent to

let! a = 1

@hvester
Copy link

hvester commented Jul 10, 2021

Anonymous bang corresponding to and! would also be useful.

For example it would be nice if instead of the following code (based on an example in Validus documentation)

// Construct Person if all validators return Success
validate {
    let! first = nameValidator "First name" input.FirstName
    and! last = nameValidator "Last name" input.LastName
    and! email = emailValidator "Email address" input.Email
    and! age = ageValidator "Age" input.Age
    and! startDate = dateValidator "Start Date" input.StartDate

    let name =
        { First = first
          Last = last }

    let person =
        { Name = name
          Email = email
          Age = age 
          StartDate = startDate }

    return person
}

you could write

// Construct Person if all validators return Success
validate {
    let name =
        { First = !!(nameValidator "First name" input.FirstName)
          Last = !!(nameValidator "Last name" input.LastName) }

    let person =
        { Name = name
          Email = !!(emailValidator "Email address" input.Email)
          Age = !!(ageValidator "Age" input.Age)
          StartDate = !!(dateValidator "Start Date" input.StartDate) }

    return person
}

Though I don't know if it would be possible to have same operator both for let!and and!. If not then would keywords be better than having two different operators.

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

6 participants