Возможно ли реализовать рекурсивный "SelectMany"?

Как мы все знаем, Enumerable.SelectMany выравнивает последовательность последовательностей в одну последовательность. Что, если мы хотим получить метод, который мог бы сгладить последовательности последовательностей последовательностей и т.д. Рекурсивно?

Я быстро придумал реализацию с использованием ICollection<T>, то есть с нетерпением оцененной, но я все еще почесываю голову о том, как сделать лениво оцененную, скажем, с помощью ключевого слова yield.

static List<T> Flatten<T>(IEnumerable list)  {
    var rv = new List<T>();
    InnerFlatten(list, rv);
    return rv;
}

static void InnerFlatten<T>(IEnumerable list, ICollection<T> acc) {
    foreach (var elem in list) {
        var collection = elem as IEnumerable;
        if (collection != null) {
            InnerFlatten(collection, acc);
        }
        else {
            acc.Add((T)elem);
        }
    }
}

Любые идеи? Примеры в любом приветствии .NET.

Ответ 1

Это тривиально в F # с выражениями рекурсивной последовательности.

let rec flatten (items: IEnumerable) =
  seq {
    for x in items do
      match x with
      | :? 'T as v -> yield v
      | :? IEnumerable as e -> yield! flatten e
      | _ -> failwithf "Expected IEnumerable or %A" typeof<'T>
  }

Тест:

// forces 'T list to obj list
let (!) (l: obj list) = l
let y = ![["1";"2"];"3";[!["4";["5"];["6"]];["7"]];"8"]
let z : string list = flatten y |> Seq.toList
// val z : string list = ["1"; "2"; "3"; "4"; "5"; "6"; "7"; "8"]

Ответ 2

Насколько я понял вашу идею, это мой вариант:

static IEnumerable<T> Flatten<T>(IEnumerable collection)
{
    foreach (var o in collection)
    {
        if (o is IEnumerable && !(o is T))
        {
            foreach (T t in Flatten<T>((IEnumerable)o))
                yield return t;
        }
        else
            yield return (T)o;
    }
}

и проверьте его

List<object> s = new List<object>
    {
        "1",
        new string[] {"2","3"},
        "4",
        new object[] {new string[] {"5","6"},new string[] {"7","8"},},
    };
var fs = Flatten<string>(s);
foreach (string str in fs)
    Console.WriteLine(str);
Console.ReadLine();

Очевидно, что ему не хватает проверок валидности типа (InvalidCastExcpetion, если коллекция содержит не T и, вероятно, некоторые другие недостатки)... ну, по крайней мере, она лениво оценивается по желанию.

!(o is T) был добавлен для предотвращения сглаживания массива string до char