Skip to content

Tail recursion for TaskSeq #62

Description

@abelbraaksma

We've removed the tail recursion because it was hard to consolidate it into yield! (it was instead done with return!, which has no place in taskSeq).

However, today I helped someone with some code that he considered for taskSeq which had the following approach:

let getPoliciesAsync policyid =
    asyncSeq{
        use connection = new NpgsqlConnection(npgsqlConnectionStringBuilder.ConnectionString)
        use command = new NpgsqlCommand($"SELECT policy_data FROM policy.tbl_policy where policy_id = {Sql.uuid policyid};", connection)
        let! reader = command.ExecuteReaderAsync() |> Async.AwaitTask
        let rec someRec() = asyncSeq{
            let! rowExists = reader.ReadAsync() |> Async.AwaitTask
            if rowExists then
                yield {| dt1 = reader.GetString(0) |}
                yield! someRec()
        }
        yield! someRec()
    } |> AsyncSeq.toAsyncEnum

As you can see, it uses asyncSeq, but also: it is recursive. The same approach with taskSeq would likely be more performant, however, if there are a lot of rows, this becomes problematic. This code can be rewritten with a loop, though.

@dsyme, sharing this with you in case we want to revisit this at some point.

Activity

  1. dsyme commented on Nov 1, 2022

    @dsyme
    Contributor

    Yes, this should be rewritten with a loop

  2. dsyme commented on Nov 5, 2022

    @dsyme
    Contributor

    @abelbraaksma BTW I think I am going to prioritise an F# 8 addition to allow builders to specify ReturnFromTailcall or YieldFromTailcall methods.

  3. abelbraaksma commented on Nov 6, 2022

    @abelbraaksma
    MemberAuthor

    @dsyme that would certainly simplify an addition like this, and would also allow task to support tail calls. Awesome! We can do some early-adoption testing here through TaskSeq.

  4. abelbraaksma commented on Oct 29, 2023

    @abelbraaksma
    MemberAuthor
  5. added this to the vFuture milestone on Mar 18, 2024
  6. majocha commented on Aug 1, 2025

    @majocha

    This is now implemented, yield! desugars to YieldFromFinal in tail call positions: dotnet/fsharp#18804
    Needs RFC, though.

  7. abelbraaksma commented on Nov 14, 2025

    @abelbraaksma
    MemberAuthor

    @majocha, thanks for letting me know! I only found out myself with the release of F# 10. Definitely going to give this a go.

  8. dsyme commented on Mar 8, 2026

    @dsyme
    Contributor

    /repo-assist let's make use of this, make a PR to add YieldFromFinal and implement a tailall transfer in the state machine driver. If you can work that out I'll be impressed

  9. github-actions commented on Mar 8, 2026

    @github-actions
    Contributor

    Pull request created: #310

    Generated by Repo Assist

  10. dsyme commented on Mar 8, 2026

    @dsyme
    Contributor

    Added the F# bug about YieldFromFinal here: dotnet/fsharp#19402

    We can't make progress on this until that's fixed

  11. github-actions commented on Mar 14, 2026

    @github-actions
  12. changed the title [-]Consider tail recursion[/-] [+]Tail recursion for TaskSeq[/+] on Mar 14, 2026
  13. jberzy commented on Jul 16, 2026

    @jberzy

    Hello,

    Now that the upstream fix is in, is it possible that we reintroduce tail recursion support?

    Thanks

  14. github-actions commented on Jul 31, 2026

    @github-actions
    Contributor

    🤖 This is an automated response from Repo Assist.

    Thanks for the ping, @jberzy! You're right — the upstream F# fix (dotnet/fsharp#19402, implementing YieldFromFinal) has landed in F# 10.

    However, actually enabling tail-call transfer in taskSeq is non-trivial. The challenge (as analysed in PR #380 which implemented the taskSeqDynamic resumable path) is that the YieldFromFinal overload is invoked by the compiler in all tail-call positions of yield!, including calls inside loop bodies (e.g. for c in source do yield! f c). At that call site there is no way to distinguish a genuine "final" call (where we could hand off the enumerator permanently) from a within-loop tail call (where we must not, or we'd lose all remaining iterations).

    The safe incremental step would be to add the YieldFromFinal overload stubs to the builder so F# 10 compiles without warning, delegating to YieldFrom for now. The actual optimization can come later once a reliable way to discriminate call sites is available (perhaps via source-level annotation or a future compiler hint).

    @abelbraaksma — would you like me to open a draft PR that adds the YieldFromFinal/YieldFromFinalAsync stubs to the TaskSeqBuilder, so F# 10 users at least get clean compilation? I can do that on request.

    Generated by 🌈 Repo Assist, see workflow run. Learn more.
    Comment /repo-assist to run again

    Add this agentic workflow to your repo

    To install this agentic workflow, run

    gh aw add githubnext/agentics/workflows/repo-assist.md@42c2ab5b4e4c9273534c39259b2e0df7f20f07e9
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    blocked: awaiting-fsharp-featureThere's a change to F# compiler or library needed to implement this featureperformancePerformance questions, improvements and fixes

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions