score:1

Accepted answer

The problem is that you always call the accumulator with initialAccumulatorValue.

In order to achieve the goal, you need to maintain the accumulated value, and the easiest correct way to do that is using C# iterator method:

public static IEnumerable<TResult> Map<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this IEnumerable<TInput> input,
    Func<TInput, TAccumulatedValue, TResult> projectionMapping, Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator,
    TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
{
    var accumulatedValue = initialAccumulatorValue;
    foreach (var item in input)
        yield return projectionMapping(item, accumulatedValue = accumulator(accumulatedValue, increment));
}

Please note that the naïve attempt to use a combination of closure and Select

var accumulatedValue = initialAccumulatorValue;
return input.Select(item => projectionMapping(item, accumulatedValue = accumulator(accumulatedValue, increment)));

simply doesn't work because the accumulatedValue will be shared by the multiple executions of the returned select query, hence they will produce incorrect result. The iterator method has no such issue because the code is actually executed anytime the GetEnumerator() method is called.

score:0

Assuming the following

public class MyObject
{
    public int Row { get; }
    public string Value { get; }

    public MyObject(int row, string value)
    {
        Value = value;
        Row = row;
    }
}

With the input source being:

var rows = new[] { "Item 1", "Item 2", "Item 3", "Item 4" };

Solution

To achieve the specific result given to the input you've presented, you can simply use the Select that is provided in the framework as such:

var result = rows.Select((v, i) => new MyObject((i+1)*10, v)).ToList();

Granted, that doesn't look very nice, but does the trick.

Ivan's answer is neater, but I'll persist the above in case someone finds it useful.

score:1

I started by changing your 3rd function so that it just takes an accumulator function that returns the next index in the sequence. This allows the function to have state which you need to calculate the increasing accumulator values.

public static IEnumerable<TResult> Project<TInput, TAccumulatorValue, TResult>(this IEnumerable<TInput> input,
    Func<TInput, TAccumulatorValue, TResult> projectionMapping, 
    Func<TAccumulatorValue> accumulator)
{
    return input.Select(item => projectionMapping(item, accumulator()));
}

Then your function that takes the range arguments that didn't work can be written like this, which solves your problem.

public static IEnumerable<TResult> Project<TInput, TResult>(this IEnumerable<TInput> input,
    Func<TInput, int, TResult> projectionMapping, int initialAccumulatorValue = 0, int increment = 1)
{
    int curValue = initialAccumulatorValue;
    return input.Project(projectionMapping, 
        () => { var ret = curValue; curValue += increment; return ret; });
}

Alternatively

Thinking about this problem in a different way you can make it more generic. All you are really doing is combining two sequences together using projectionMapping to combine the elements. In this case the second sequence happens to contain the accumulator values. Then to use this you just use the standard Linq function Zip, passing in the accumulator sequence and the projectionMapping function.

To get a linear sequence we can use Enumerable.Range, but to get a non-linear range we need to write a Range generator like this

public static IEnumerable<int> Range(int start, int increment)
{
    for (; ; )
    {
        yield return start;
        start += increment;
    }
}

Examples

Showing both solutions in action

var items = new[] { "a", "b", "c" };

// use Project, returns: a10, b20, c30
var results = items.Project((s, i) => s + i.ToString(), 10, 10).ToList();

// use Zip with custom range, returns: a10, b20, c30
var results2 = items.Zip(Range(10, 10), (s, i) => s + i.ToString()).ToList();

// use Zip with standard range, returns: a1, b2, c3
var results3 = items.Zip(Enumerable.Range(1, int.MaxValue), (s, i) => s + i.ToString()).ToList();

Related Query

More Query from same tag