louthy/language-ext · error · NotSupportedException

Tail calls can't transform in the `select`

Error message

Tail calls can't transform in the `select`

What it means

IOTail.resolve trampolines a tail call whose bind result is an IOTail<B> by re-typing the inner Tail as IO<C> — but that fast path is only legal when B and C are the same type. If a `select`/`project` function attempts to transform the value (B != C) while the result is still a tail call, the library cannot perform the conversion, so it throws NotSupportedException.

Solutions

  1. Move the transformation inside the recursion (apply project in the Bind continuation before the tail call) so the final select is type-preserving.
  2. Make the select type-identical (B == C) — e.g. return the value unchanged in select and Map afterwards on the resolved IO.
  3. Avoid `select` over tail-recursive loops; instead Bind the loop result and then Map on the resolved (non-tail) IO.
  4. Restructure using explicit recursion with an accumulator so the projection happens per iteration, not via the tail call's select.

Example fix

// before — throws when loop result is a tail call and types differ
var q = from x in loop
        from y in step
        select project(x, y);            // B != C
// after
IO<C> result = loop.Bind(x => step.Bind(y => IO.pure(project(x, y))));
// projection now happens inside Bind, no tail-call select
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the select over a possibly-tail loop is type-preserving:
// typeof(B) == typeof(C), or restructure the projection into a Bind.

Type guard

IO<C> safeSelect<B,C>(A init, IO<B> bindResult, Func<A,B,C> project) =>
    typeof(B) == typeof(C)
        ? IO<C>.pure((C)(object)default(B)) // type-preserving path
        : bindResult.Bind(y => IO<C>.pure(project(init, y))); // project inside Bind

Try / catch

try { r = IOTail<A>.resolve(init, bindResult, project); }
catch (NotSupportedException ex) when (ex.Message.Contains("can't transform"))
{
    r = bindResult.Bind(y => IO<C>.pure(project(init, y)));
}

Prevention

When it happens

Trigger: Using LINQ query syntax over IO where the `select` clause transforms the bound value (e.g. `from x in loop from y in ... select project(x, y)`) while the bind result remains an unresolved IOTail<B> and typeof(B) != typeof(C).

Common situations: Writing recursive IO with LINQ comprehension syntax (from/into/select) where the final select changes the result type; converting an IO loop that previously returned the same type to one returning a projected value.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of louthy/language-ext@2f0e362824 (2026-09-15). Data as JSON: /api/errors/ae4d059ea91b6a04. Report an issue: GitHub.

Appendix: source

Thrown at LanguageExt.Core/Effects/IO/DSL/IOTail.cs:25

record IOTail<A>(IO<A> Tail) : IO<A>
{
    public override IO<B> Map<B>(Func<A, B> f) => 
        throw new NotSupportedException("You can't map a tail call");

    public override IO<B> Bind<B>(Func<A, K<IO, B>> f) =>
        throw new NotSupportedException("You can't chain a tail call");

    public override IO<B> BindAsync<B>(Func<A, ValueTask<K<IO, B>>> f) => 
        throw new NotSupportedException("You can't chain a tail call");

    public override string ToString() => 
        "IO tail";

    public static IO<C> resolve<B, C>(A initialValue, IO<B> bindResult, Func<A, B, C> project)
        => bindResult switch
           {
               IOTail<B> tail when typeof(B) == typeof(C) => (IO<C>)(object)tail.Tail,
               IOTail<B> => throw new NotSupportedException("Tail calls can't transform in the `select`"),
               var mb => mb.Map(y => project(initialValue, y))
           };
}

View on GitHub (pinned to 2f0e362824)