简体   繁体   中英

How to build a sequence using a fluent interface?

I'm trying to using a fluent interface to build a collection, similar to this (simplified) example:

   var a = StartWith(1).Add(2).Add(3).Add(4).ToArray();
   /* a = int[] {1,2,3,4};  */

The best solution I can come up with add Add() as:

  IEnumerable<T> Add<T>(this IEnumerable<T> coll, T item)
  {
     foreach(var t in coll) yield return t;
     yield return item;
  }

Which seems to add a lot of overhead that going to be repeated in each call.

IS there a better way?

UPDATE: in my rush, I over-simplified the example, and left out an important requirement. The last item in the existing coll influences the next item. So, a slightly less simplified example:

   var a = StartWith(1).Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();
   /* a = int[] {1,12,123,1234};  */

public static IEnumerable<T> StartWith<T>(T x)
{
    yield return x;
}

static public  IEnumerable<int> Times10Plus(this IEnumerable<int> coll, int item)
{
    int last = 0;
    foreach (var t in coll)
    {
        last = t;
        yield return t;
    }
    yield return last * 10 + item;
}

A bit late to this party, but here are a couple ideas.

First, consider solving the more general problem:

public static IEnumerable<A> AggregateSequence<S, A>(
  this IEnumerable<S> items,
  A initial,
  Func<A, R, A> f) 
{
  A accumulator = initial;
  yield return accumulator;
  foreach(S item in items)
  {
    accumulator = f(accumulator, item);
    yield return accumulator;
  }
}

And now your program is just new[]{2, 3, 4}.AggregateSequence(1, (a, s) => a * 10 + s).ToArray()

However that lacks the "fluency" you want and it assumes that the same operation is applied to every element in the sequence.

You are right to note that deeply nested iterator blocks are problematic; they have quadratic performance in time and linear consumption of stack, both of which are bad.

Here's an entertaining way to implement your solution efficiently.

The problem is that you need both cheap access to the "right" end of the sequence, in order to do an operation on the most recently added element, but you also need cheap access to the left end of the sequence to enumerate it. Normal queues and stacks only have cheap access to one end.

Therefore: start by implementing an efficient immutable double-ended queue. This is a fascinating datatype; I have an implementation here using finger trees:

https://blogs.msdn.microsoft.com/ericlippert/2008/01/22/immutability-in-c-part-10-a-double-ended-queue/

https://blogs.msdn.microsoft.com/ericlippert/2008/02/12/immutability-in-c-part-eleven-a-working-double-ended-queue/

Once you have that, your operations are one-liners:

static IDeque<T> StartWith<T>(T t) => Deque<T>.Empty.EnqueueRight(t);
static IDeque<T> Op<T>(this IDeque<T> d, Func<T, T> f) => d.EnqueueRight(f(d.PeekRight()));
static IDeque<int> Times10Plus(this IDeque<int> d, int j) => d.Op(i => i * 10 + j);

Modify IDeque<T> and Deque<T> to implement IEnumerable<T> in the obvious way and you then get ToArray for free. Or do it as an extension method:

static IEnumerable<T> EnumerateFromLeft(this IDeque<T> d)
{
  var c = d;
  while (!c.IsEmpty) 
  { 
    yield return c.PeekLeft(); 
    c = c.DequeueLeft(); 
  }
}

You could do the following:

public static class MySequenceExtensions
{
    public static IReadOnlyList<int> Times10Plus(
        this IReadOnlyList<int> sequence, 
        int value) => Add(sequence, 
                          value,
                          v => sequence[sequence.Count - 1] * 10 + v);

    public static IReadOnlyList<T> Starts<T>(this T first)
        => new MySequence<T>(first);

    public static IReadOnlyList<T> Add<T>(
        this IReadOnlyList<T> sequence,
        T item,
        Func<T, T> func)
    {
        var mySequence = sequence as MySequence<T> ?? 
                         new MySequence<T>(sequence);
        return mySequence.AddItem(item, func);
    }

    private class MySequence<T>: IReadOnlyList<T>
    {
        private readonly List<T> innerList;

        public MySequence(T item)
        {
            innerList = new List<T>();
            innerList.Add(item);
        }

        public MySequence(IEnumerable<T> items)
        {
            innerList = new List<T>(items);
        }

        public T this[int index] => innerList[index];
        public int Count => innerList.Count;

        public MySequence<T> AddItem(T item, Func<T, T> func)
        {
            Debug.Assert(innerList.Count > 0);
            innerList.Add(func(item));
            return this;
        }

        public IEnumerator<T> GetEnumerator() => innerList.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

Note that I'm using IReadOnlyList to make it possible to index into the list in a performant way and be able to get the last element if needed. If you need a lazy enumeration then I think you are stuck with your original idea.

And sure enough, the following:

var a = 1.Starts().Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();

Produces the expected result ( {1, 12, 123, 1234} ) with, what I think is, reasonable performance.

You can do like this:

public interface ISequence
{
    ISequenceOp StartWith(int i);
}

public interface ISequenceOp
{
    ISequenceOp Times10Plus(int i);
    int[] ToArray();
}

public class Sequence : ISequence
{
    public ISequenceOp StartWith(int i)
    {
        return new SequenceOp(i);
    }
}

public class SequenceOp : ISequenceOp
{
    public List<int> Sequence { get; set; }

    public SequenceOp(int startValue)
    {
        Sequence = new List<int> { startValue };
    }

    public ISequenceOp Times10Plus(int i)
    {
        Sequence.Add(Sequence.Last() * 10 + i);
        return this;
    }

    public int[] ToArray()
    {
        return Sequence.ToArray();
    }
}

An then just:

var x = new Sequence();
var a = x.StartWith(1).Times10Plus(2).Times10Plus(3).Times10Plus(4).ToArray();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM