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

Presence of return is misleading #38

Open
mexx opened this issue Dec 3, 2015 · 17 comments
Open

Presence of return is misleading #38

mexx opened this issue Dec 3, 2015 · 17 comments
Labels

Comments

@mexx
Copy link
Member

mexx commented Dec 3, 2015

Given the following expression asyncSeq { return [1] }.
I would expect the type of it to be AsyncSeq<int>, unfortunately it is AsyncSeq<'a>.

I understand that in first place it's my wrong expectation about the result's presence, actually as it's a sequential workflow, it shouldn't be there at all.
By looking at the source code I know that the presence of return is caused by the wish to support the do! action functionality, as it's translated by the F# compiler into Bind(action, fun () -> Return()).
Could it be a F# compiler problem? What if the compiler would transform it to Bind(action, Zero()), then the resultreturn could be dropped right?

05.12.2015: corrected identifier

@dsyme
Copy link
Contributor

dsyme commented Dec 3, 2015

Yes, IIRC this is an F# language limitation where it is problematic to express that a method takes a single parameter of type unit, rather than no parameters. It's almost never needed but the computation expression translation uses both foo.Return(()) and foo.Return() so we need a method that can accept either. If you can find an alternative workaround it would be much appreciated.

@dsyme dsyme added the question label Dec 4, 2015
@mexx
Copy link
Member Author

mexx commented Dec 5, 2015

@dsyme I've tried a patched version of compiler and could successfully remove the Return method from the builder.

As stated in the initial comment, the call Zero() instead of the Return() allows constructs like

asyncSeq {
    yield 1
    do! Async.Sleep 2000
    yield 30
}

to be processed without the need for Return method.

Should I create a PR with the compiler change in visualfsharp for further discussion?

@dsyme
Copy link
Contributor

dsyme commented Dec 6, 2015

Hi @maxx

We wouldn't change Return to Zero - that would be a breaking change. We might however remove the limitation that Return gets called with both () (zero) and (()) (one) arguments, which if IIRC is the cause of the problem.

However even that change might not meeting our no-breaking-change policy, and we might have to resort to just emitting a warning of some kind.

@mexx
Copy link
Member Author

mexx commented Dec 6, 2015

I think my concern is not clear. In the current asyncSeq one can use return whatever and the value of whatever expression is not processed any further by the asyncSeq. Furthermore the asyncSeq { return [1] } is an empty sequence. This is not intuitive and do not correspond to the behavior of other computation expressions. So IMHO the root cause is the presence of return.

Don, you are right about the breaking change if we simply replace the return () with zero. But at the time we inject the behavior we know about what methods are present on the builder and could inject Zero when there is no Return. This would allow us to write AsyncSeqBuilder without having the Return method on it and the asyncSeq { return whatever } would become illegal at compile time.

@mexx
Copy link
Member Author

mexx commented Dec 6, 2015

@dsyme I've updated my patch to fallback to Zero if Return is not present, you can find it here.

@dsyme
Copy link
Contributor

dsyme commented Dec 7, 2015

That approach looks good! Please add an fslang.uservoice.com entry for it - I'll mark it as approved for some future release - and submit the PR to Microsoft/visualfsharp.

It will take a long while before libraries like AsyncSeq can rely on this language change though, since it basically currently targets FSharp.Core 4.3.0.0 and supports use by F# 3.0+

@mexx
Copy link
Member Author

mexx commented Dec 7, 2015

UserVoice and PR

@mexx
Copy link
Member Author

mexx commented Dec 9, 2015

@dsyme
While implementing the tests for the PR on visualfsharp repo, I could define the implementation for return as Return (()) = empty which will be picked correctly for the unit parameter, and therefore allow only unit valued expressions to be used with return keyword. It doesn't work when you define Return() = empty. Is it intended to work this way?

It's very weird, actually the distinguishing problem only arises from the usage of signature file.
Only in the signature file I didn't found a way to define that there should be one parameter of type unit. Is there a way to define such signature in the .fsi file?

@eulerfx
Copy link
Contributor

eulerfx commented Apr 11, 2016

@mexx following up on this, is there still some action to take on this library or has focused shifted elsewhere?

@mexx
Copy link
Member Author

mexx commented Apr 13, 2016

The next version of F# compiler will allow to use Zero without the need to provide Return.
With this version this library could get rid of the Return method on the builder. However this would be a breaking change for users of the library.

@enricosada
Copy link
Contributor

enricosada commented Aug 28, 2018

I'll try do an updated summary because i got same issue ( #92 ), it's really error prone :D more so migrating from v1 to v2

So the compiler feature from @mexx was released in F# 4.1

image

NOTE

  • the FSharp.Core version doesnt matter (it's ok we continue to target >= v3), it matter just the compiler version. Right? /cc @mexx @dsyme
  • Until support for compiler builtin in VS 2015 is dropped by asyncseq, it's not possibile to use the new feature.

Q: Can be useful to do it just for netstandard2.0? that will add an #ifdef NETSTANDARD2_0 to remove the Return from asyncsec builder (if i understand right the change required by the feature).

  • PRO fix the issue for netstandard, because it require a F# 4.1+ compiler so we can expect to have that feature
  • PRO doesnt change anything for users targeting net only (no api surface changes), for older or newer compiler
  • CONS1 if a code multitarget both net and netstandard (or add netstandard after a migration), any usage of return will fail to compile (well, will be ok in net, but fails in netstandard).
  • CONS2 this change the current api surface for netstandard2.0, so it's anyway a breaking change -> fsharp.control.asyncseq v3**. but user with v2 can meanwhile start change the code to remove it, so override to v3 will work anyway.

I think the CONS1 is instead a nice to have.
Will break some compilation, but usage of return directly is 99% a bug afaik.
I cannot think why someone should use it directly, the desugar of while is done by compiler.
So doing that will surface a bug in the user implementation.

any code like this

let a = asyncSeq {
   yield 1
   return 2 // this compile, but 2 is ignored, so a bug without error
   }

will need to be changed to

let a = asyncSeq {
   yield 1
   }

who does the same thing.

@eulerfx @mexx @dsyme make sense?

@mattstermiller
Copy link
Contributor

I was extremely confused by this as well. My use case was yielding a single value in the sequence given an Async<T>. I used return, which type checked, so I was expecting the async seq to have this one value produced by the async expression, but got nothing. The crux of this issue is that the builder's return accepts any parameter and ignores it.

The adjustment to the compiler is nice, since I ran into that do! issue with my own CE as well - but I'd like to suggest a simple workaround in the meantime that does not cause this confusion: restrict Return's parameter type to unit. This allows you to continue to use do! and return () but not return 1 or any other arbitrary value. This would cause code written against this library to fail to compile if the code returns non-unit values, but this can only be a good thing since such code was probably written under the misconception that something was being done with the value.

@mattstermiller
Copy link
Contributor

I tried to create a pull request for the above, but encountered a problem with specifying the signature in the fsi file. The compiler required me to define the Return method as member x.Return (_: unit) = empty, but there doesn't seem to be a way to define the signature for that. member Return : unit -> AsyncSeq<'T> gives the error Module 'FSharp.Control.AsyncSeq' requires a value 'member AsyncSeq.AsyncSeqBuilder.Return : unit -> AsyncSeq<'T>'. This isn't an option unless we exclude this member from the signature file.

@giuliohome
Copy link

giuliohome commented Oct 30, 2019

I was really confused by this.
Found this thread and the other issue only after posting my own solution here

@mattstermiller
Copy link
Contributor

This issue confused two of my teammates again last week (we're a team of only 6, so that really hurts). I've submitted a PR with my best effort to fix this issue.

@mattstermiller
Copy link
Contributor

mattstermiller commented Nov 25, 2019

As a workaround to stop the pain now, I've added this to our project to define a version of the CE builder without the Return:

namespace FSharp

open FSharp.Control

/// Computation builder that allows creating of asynchronous sequences using the 'asyncSeq { ... }' syntax
/// This is a "fixed" version without the value-ignoring Return
type FixedAsyncSeqBuilder() =
    let builder = AsyncSeq.AsyncSeqBuilder()

    member __.Bind (source, body) = builder.Bind (source, body)
    member __.Combine (seq1, seq2) = builder.Combine (seq1, seq2)
    member __.Delay f = builder.Delay f
    member __.For (source: 'a seq, action) = builder.For (source, action)
    member __.For (source: 'a AsyncSeq, action) = builder.For (source, action)
    member __.TryFinally (body, comp) = builder.TryFinally (body, comp)
    member __.TryWith (body, handler) = builder.TryWith (body, handler)
    member __.Using (resource, binder) = builder.Using (resource, binder)
    member __.While (guard, body) = builder.While (guard, body)
    member __.Yield value = builder.Yield value
    member __.YieldFrom source = builder.YieldFrom source
    member __.Zero () = builder.Zero ()

module Control =
    // shadow the builder from FSharp.Control
    let asyncSeq = FixedAsyncSeqBuilder()

@dsyme
Copy link
Contributor

dsyme commented Nov 26, 2019

So if I understand correctly we should just remove the Return from the builder in this repo (and accept the breaking change if people are using return explicitly in asyncSeq)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants