let! is not allowed in seq {...} in async?

Viewed 157

I'm trying to convert the following C# code to F#.

static async Task<IEnumerable<string>> ListingObjectsAsync()
{
    ListObjectsV2Request request = new ListObjectsV2Request
    {
        BucketName = bucketName,
        MaxKeys = 10
    };
    ListObjectsV2Response response;
    do
    {
        response = await client.ListObjectsV2Async(request);

        foreach (S3Object entry in response.S3Objects)
        {
            yield return entry.Key;
        }
        request.ContinuationToken = response.NextContinuationToken;
    } while (response.IsTruncated);
}

However, the following unfinished code got the error of

Error FS0795 The use of 'let! x = coll' in sequence expressions is not permitted. Use 'for x in coll' instead.

let listObjects bucketName = async {
    use client = new AmazonS3Client(RegionEndpoint.USEast2)
    let request = new ListObjectsRequest(BucketName = bucketName, MaxKeys = 10)
    // do while... todo
    seq {
        let! response = client.ListObjectsV2Async(request) |> Async.AwaitTask
        for entry in response.S3Objects do
            yield entry.Key
        request.ContinuationToken <- response.NextContinuationToken
    }

How to convert the C# do {...} while(...); to F# btw?

1 Answers

In general, you usually want to avoid nesting one computation expression inside another one, because it can get confusing which one refers to what. Here in particular, you intend the let! expression to be interpreted as in async { let! foo = AsyncFoo() }, but it's the seq { } computation expression that's interpreting the let!.

What I would recommend is to split the seq { } out into its own function, and likewise with the looped part of the async. The rest of the listObjects function doesn't need to be asynchronous,

let keysFromPartialResponse response = seq {
    for entry in response.S3Objects do
        yield entry.Key
}

let doRequest request resultSoFar = async {
    let! response = client.ListObjectsV2Async(request) |> Async.AwaitTask
    request.ContinuationToken <- response.NextContinuationToken
    let result = Seq.append resultSoFar (keysFromPartialResponse response)
    if request.IsTruncated then
        return! doRequest request result  // This is the loop step
    else
        return result
    }

let listObjects bucketName = async {
    use client = new AmazonS3Client(RegionEndpoint.USEast2)
    let request = new ListObjectsRequest(BucketName = bucketName, MaxKeys = 10)
    return! doRequest request Seq.empty
}

There might be a better solution using AsyncSeq, but I'll leave that one as an exercise for the reader.

Note that there's one issue with your F# code which still exists in my code here, which is that sequences are lazy and won't actually run the seq { ... } code until they're evaluated. And because you've used use, once the client object goes out of scope (i.e., once the async returns), the AmazonS3Client instance will be disposed. So by the time you evaluate the sequence (which will evaluate response.S3Objects) the client won't be valid any more. If that means that evaluating response.S3Objects will fail, then you'll have to convert this code to use lists instead of seqs. Should be simple enough, so I'll leave that to you, but let me know if you have trouble with that.

Related