Skip to content

Introduce ValueSpreadAware binding for positional spread arguments #751

Description

@Seddryck

Context

FunctionFactory currently handles array and text explicitly because both functions need access to spread-aware positional arguments.

This introduces function-name hardcoding in the factory even though array and text share the same spread semantics:

A spread argument evaluates to multiple values that are inserted at that position in the function's positional argument sequence.

For example:

array(1, ...{2, 3}, 4)

is structurally equivalent to:

array(1, 2, 3, 4)

and:

text("foo", ...{"Nikola", "Tesla"}, "bar")

is structurally equivalent to:

text("foo", "Nikola", "Tesla", "bar")

The receiving function decides what to do with the resulting values: array collects them, while text concatenates them. The spread semantics themselves are identical.

Proposed design

Introduce a marker interface:

public interface IValueSpreadAware
{
}

Functions implementing this interface declare that their positional arguments must be bound through the value-spread-aware argument mechanism.

Initially:

public class Array : IFunction, IValueSpreadAware
public class Text : IFunction, IValueSpreadAware

The interface should remain a marker. Spread is a binding/argument-evaluation concern and should not expose runtime spread operations on the function instance itself.

Common value argument evaluator

Introduce a common evaluator abstraction used by every IValueSpreadAware function, for example:

public sealed class ValueArgumentEvaluator
{
    public Func<object?, object?> Evaluator { get; }
    public bool IsSpread { get; }

    public ValueArgumentEvaluator(
        Func<object?, object?> evaluator,
        bool isSpread)
        => (Evaluator, IsSpread) = (evaluator, isSpread);
}

The exact naming can vary, but the abstraction must preserve:

  1. how the argument is evaluated against the current input;
  2. whether the argument was introduced using spread syntax.

A common helper should expand arguments while preserving order:

public static IEnumerable<object?> Evaluate(
    IEnumerable<ValueArgumentEvaluator> arguments,
    object? input)
{
    foreach (var argument in arguments)
    {
        var value = argument.Evaluator(input);

        if (!argument.IsSpread)
        {
            yield return value;
            continue;
        }

        foreach (var item in SpreadValues.Enumerate(value))
            yield return item;
    }
}

Reuse the existing SpreadValues behavior where appropriate rather than introducing another definition of which runtime values can be spread.

A non-spread argument contributes exactly one value. A spread argument contributes zero or more values.

Pipeline semantics

Spread must not alter pipeline semantics.

In particular, spread should not implicitly turn a regular pipeline into element-wise evaluation.

Element-wise transformation must remain the responsibility of |>:

text(...({"Nikola", "Tesla"} |> prepend-space))

Conceptually:

{"Nikola", "Tesla"}
|> prepend-space

produces:

{" Nikola", " Tesla"}

which is then spread into the positional arguments of text.

Consequently, remove the text-specific handling of spread InputExpressionParameter if its purpose is to provide implicit element-wise transformation.

array and text must use exactly the same value-spread argument binding semantics.

Function constructors

array and text should consume the same common evaluator abstraction, for example:

public class Array : IFunction, IValueSpreadAware
{
    private Func<ValueArgumentEvaluator[]> Arguments { get; }

    public Array(Func<ValueArgumentEvaluator[]> arguments)
        => Arguments = arguments;
}
public class Text : IFunction, IValueSpreadAware
{
    private Func<ValueArgumentEvaluator[]> Arguments { get; }

    public Text(Func<ValueArgumentEvaluator[]> arguments)
        => Arguments = arguments;
}

Both functions should consume the expanded value sequence using the same helper. Their behavior diverges only after expansion.

Review ArrayArgumentEvaluator and TextArgumentEvaluator. If their difference only exists because spread handling evolved independently, replace them with the common ValueArgumentEvaluator.

Function-specific evaluator types should remain only where they represent genuinely function-specific semantics unrelated to spread.

FunctionFactory impact

FunctionFactory must resolve the function type through FunctionTypeMapper before deciding whether special argument binding is required.

Then dispatch by capability:

if (!TypeMapper.TryExecute(function.Name, out var type))
{
    // existing predicate / not implemented handling
}

if (typeof(IValueSpreadAware).IsAssignableFrom(type))
    return InstantiateValueSpreadAware(type, function, context);

Introduce a common path such as:

private IFunction InstantiateValueSpreadAware(
    Type type,
    Bindings.Function function,
    IContext context)
{
    var arguments = function.Arguments
        .Select(argument => new ValueArgumentEvaluator(
            BuildValueEvaluator(argument.Value, context),
            argument.IsSpread))
        .ToArray();

    // instantiate the resolved type using the common evaluator provider
}

The method must instantiate the resolved type; it must not branch again based on whether that type is Array or Text.

Explicit cleanup requirement

As part of this implementation, remove array and text from the manually handled function names in FunctionFactory.

The factory must no longer contain routing equivalent to:

if (name.Equals("array", StringComparison.OrdinalIgnoreCase))
    return BuildArrayFunction(function, context);

if (name.Equals("text", StringComparison.OrdinalIgnoreCase))
    return BuildTextFunction(function, context);

BuildArrayFunction and BuildTextFunction should be removed, or reduced into one generic capability-based mechanism that contains no knowledge of the concrete function names/types.

Adding another function implementing IValueSpreadAware must not require modifying FunctionFactory.

Expected factory flow

function name
    ↓
FunctionTypeMapper
    ↓
CLR function type
    ↓
does type implement IValueSpreadAware?
    ├─ yes
    │    ↓
    │  build ValueArgumentEvaluator[]
    │    ↓
    │  instantiate resolved function type
    │
    └─ no
         ↓
       other special capabilities
         ↓
       normal generic instantiation

The factory should reason about capabilities attached to the resolved type rather than maintaining another registry of function names.

Tests

Preserve/add coverage for:

array(1, 2, 3)
array(1, ...{2, 3}, 4)
array(...{1, 2}, ...{3, 4})
text("a", ...{"b", "c"}, "d")

Also cover mapped values followed by spread:

array(...({1, 2, 3} |> multiply(10)))

Expected:

{10, 20, 30}

and:

text(...({"nikola", "tesla"} |> upper))

Expected:

NIKOLATESLA

No test should rely on spread changing | into element-wise evaluation.

Add a test demonstrating that value-spread-aware dispatch is based on IValueSpreadAware / resolved type rather than the function name.

Acceptance criteria

  • Introduce IValueSpreadAware.
  • array implements IValueSpreadAware.
  • text implements IValueSpreadAware.
  • Both use one common value argument evaluator representation.
  • Spread has the same positional expansion semantics for both functions.
  • Element-wise transformation remains the responsibility of |>, not ....
  • Remove obsolete text-specific spread-expression handling.
  • FunctionFactory detects value-spread-aware functions through the resolved CLR type.
  • Remove hardcoded array handling from FunctionFactory.
  • Remove hardcoded text handling from FunctionFactory.
  • Remove or generalize BuildArrayFunction and BuildTextFunction so the factory has no concrete knowledge of these functions.
  • A future IValueSpreadAware function can use the same path without modifying FunctionFactory.
  • Existing non-spread behavior of array and text remains unchanged.
  • Existing and new spread tests pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementEnhancement to an existing feature

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions