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
- Move the transformation inside the recursion (apply project in the Bind continuation before the tail call) so the final select is type-preserving.
- Make the select type-identical (B == C) — e.g. return the value unchanged in select and Map afterwards on the resolved IO.
- Avoid `select` over tail-recursive loops; instead Bind the loop result and then Map on the resolved (non-tail) IO.
- 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
- Avoid `select` clauses that change the result type over tail-recursive IO loops.
- Apply projections inside Bind continuations instead of in the final select.
- Convert LINQ-comprehension loops to explicit recursive Bind + Map on resolved IO.
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
- You can't map a tail call
- You can't chain a tail call
- Invalid iterator
- Ord attribute should have a struct type that derives from…
- Hashable attribute should have a struct type that derives…
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)