2

I'd created a Map function in C# to act, in many ways, as it's JavaScript equivalent to project object types. I've since renamed these methods to 'Select' to use as overloads so they feel more 'integrated'. This is a chain, so bear with me, but the affected functions look like this...

    public static TResult Project<TInput, TResult>(this TInput input, Func<TInput, TResult> projectionMapping)
        => projectionMapping(input);

    public static TResult Project<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this TInput input, Func<TInput, TAccumulatedValue, TResult> projectionMapping, 
        Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator, TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
        => projectionMapping(input, accumulator(initialAccumulatorValue, increment));

    public static IEnumerable<TResult> Select<TInput, TAccumulatedValue, TIncrementingValue, TResult>(this IEnumerable<TInput> input,
        Func<TInput, TAccumulatedValue, TResult> projectionMapping, Func<TAccumulatedValue, TIncrementingValue, TAccumulatedValue> accumulator, 
        TAccumulatedValue initialAccumulatorValue, TIncrementingValue increment)
        => input.Select(item => item.Project(projectionMapping, accumulator, initialAccumulatorValue, increment));

    // This doesn't work.
    public static IEnumerable<TResult> Select<TInput, TResult>(this IEnumerable<TInput> input,
        Func<TInput, int, TResult> projectionMapping, int initialAccumulatorValue = -1, int increment = 1)
    {
        return input.Select(projectionMapping, (acc, inc) => acc + inc,
            initialAccumulatorValue, increment);
    }

I am using the int version of the map method, with the accumulator written into it, as follows...

    MyList.Add(new List<MyObject>(rowValues.Map((val, headerNumber) 
            => new MyObject(headerNumber, val), 0, 10)));

The problem is, that the value of headerNumber never changes (It's always 10) - The accumulator runs once and then is running for each Mapping but it's not remembering it's accumulation between runs. I feel I'm missing something glaringly obvious here but I can't see the wood for the trees.

If I input (for example) an array like this...

rowValues = new string[] { "Item 1", "Item 2", "Item 3" };

I would expect a list of MyObject items that contain the following data...

  • 10 | "Item 1"
  • 20 | "Item 2"
  • 30 | "Item 3"
Keith Jackson
  • 3,078
  • 4
  • 38
  • 66
  • Please add source data and desired result for the source data. – Andre Andersen Feb 17 '17 at 14:27
  • Anything with an index is valid source data for this. The issue is with the accumulator of the index value, not the data. I will add the test that calls the method but I don't think that helps much. – Keith Jackson Feb 17 '17 at 14:29
  • Possible duplicate: http://stackoverflow.com/questions/428798/map-and-reduce-in-net – Andre Andersen Feb 17 '17 at 14:31
  • Not a duplicate at all - This issue is due with passing the accumulation through. – Keith Jackson Feb 17 '17 at 14:36
  • 2
    You've seem to put much effort into this, but there is an overload of `Select` that also takes the index: https://msdn.microsoft.com/en-us/library/bb534869(v=vs.110).aspx I think that already implements what you want – mortb Feb 17 '17 at 14:46
  • 1
    As described in the link I referenced above. – Andre Andersen Feb 17 '17 at 14:53
  • @mortb That may be a better solution for this specific use case. I wrote the Map method to handle scenarios where Select wouldn't fit, but I'll give this a try. – Keith Jackson Feb 17 '17 at 15:15
  • The given use case is now working but what the Map method I created was intended to solve is still valid where the accumulated value is NOT the index from the source item. Is this possible by wrapping multiple Select elements in some way? I will, in the future, have need for a Select method that takes an Accumulator and integrates it's value into a collection of objects based on a given starting point and increment. – Keith Jackson Feb 17 '17 at 15:18
  • I've changed the name of the 'Map' methods to 'Select' to act as overrides (except for the int based accumulator one which currently doesn't work) to be more dotnet standards orientated. I also deleted the first 'Map' overload because it doesn't actually do anything. – Keith Jackson Feb 17 '17 at 15:23

3 Answers3

1

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.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • I believe this is the wall my head has been banging itself against. Thanks Ivan. I'll give this a try and see if it works for me. – Keith Jackson Feb 17 '17 at 15:41
  • That worked for me - the method is now working like a charm as an additional Select overload that takes an accumulator and I have both integer and Time variants working. – Keith Jackson Feb 17 '17 at 18:06
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();
Rimp
  • 71
  • 2
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.

Andre Andersen
  • 1,211
  • 1
  • 11
  • 19
  • Looks like you didn't read the end of my post about closure and `Select` :) Both your second and third options suffer from closure not being reinitialized, which can easily be seen by enumerating (or `ToList`-ing) `result2` or `result3` variable twice. – Ivan Stoev Feb 17 '17 at 17:11
  • @IvanStoev Touché! I did not take multiple iterations into consideration. I will remove these options from my answer, as they can be greatly misleading. Thank you for bringing this to my attention. – Andre Andersen Feb 17 '17 at 18:10
  • This is probably valid for over half of cases that will need something like this in the future. Mine has to do weird sequencing stuff with both ints and DateTimes, hence the generic core method. – Keith Jackson Feb 17 '17 at 18:15