Что такое катаморфизм и может ли он быть реализован в С# 3.0?

Я пытаюсь узнать о катаморфизмах, и я прочитал статью в Википедии и первые сообщения в разделе серию тем для F # в блоге Inside F #.

Я понимаю, что это обобщение складок (т.е. отображение структуры многих значений в одно значение, включая список значений в другой список). И я понимаю, что сгиб-лист и свод-дерево являются каноническим примером.

Можно ли это показать, как это делается на С#, используя оператор LINQ Aggregate или какой-либо другой метод более высокого порядка?

Ответ 1

LINQ Aggregate() предназначен только для IEnumerables. Катаморфизмы в целом относятся к схеме свертывания для произвольного типа данных. Таким образом, Aggregate() для IEnumerables FoldTree что FoldTree (ниже) для Trees (ниже); оба являются катаморфизмами для их соответствующих типов данных.

Я перевел часть кода в части 4 серии на С#. Код ниже. Обратите внимание, что эквивалентный F # использовал три символа меньше (для аннотаций параметров общего типа), тогда как этот код С# использует более 60. Это свидетельствует о том, почему никто не пишет такой код в С# - слишком много аннотаций типов. Я представляю код на случай, если он поможет людям, которые знают С#, но не F #, поиграть с этим. Но код на С# настолько плотный, что его очень сложно понять.

Дано следующее определение для двоичного дерева:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

class Tree<T>   // use null for Leaf
{
    public T Data { get; private set; }
    public Tree<T> Left { get; private set; }
    public Tree<T> Right { get; private set; }
    public Tree(T data, Tree<T> left, Tree<T> rright)
    {
        this.Data = data;
        this.Left = left;
        this.Right = right;
    }

    public static Tree<T> Node<T>(T data, Tree<T> left, Tree<T> right)
    {
        return new Tree<T>(data, left, right);
    }
}

Можно сложить деревья и, например, измерить, если два дерева имеют разные узлы:

class Tree
{
    public static Tree<int> Tree7 =
        Node(4, Node(2, Node(1, null, null), Node(3, null, null)),
                Node(6, Node(5, null, null), Node(7, null, null)));

    public static R XFoldTree<A, R>(Func<A, R, R, Tree<A>, R> nodeF, Func<Tree<A>, R> leafV, Tree<A> tree)
    {
        return Loop(nodeF, leafV, tree, x => x);
    }

    public static R Loop<A, R>(Func<A, R, R, Tree<A>, R> nodeF, Func<Tree<A>, R> leafV, Tree<A> t, Func<R, R> cont)
    {
        if (t == null)
            return cont(leafV(t));
        else
            return Loop(nodeF, leafV, t.Left, lacc =>
                   Loop(nodeF, leafV, t.Right, racc =>
                   cont(nodeF(t.Data, lacc, racc, t))));
    }

    public static R FoldTree<A, R>(Func<A, R, R, R> nodeF, R leafV, Tree<A> tree)
    {
        return XFoldTree((x, l, r, _) => nodeF(x, l, r), _ => leafV, tree);
    }

    public static Func<Tree<A>, Tree<A>> XNode<A>(A x, Tree<A> l, Tree<A> r)
    {
        return (Tree<A> t) => x.Equals(t.Data) && l == t.Left && r == t.Right ? t : Node(x, l, r);
    }

    // DiffTree: Tree<'a> * Tree<'a> -> Tree<'a * bool> 
    // return second tree with extra bool 
    // the bool signifies whether the Node "ReferenceEquals" the first tree 
    public static Tree<KeyValuePair<A, bool>> DiffTree<A>(Tree<A> tree, Tree<A> tree2)
    {
        return XFoldTree((A x, Func<Tree<A>, Tree<KeyValuePair<A, bool>>> l, Func<Tree<A>, Tree<KeyValuePair<A, bool>>> r, Tree<A> t) => (Tree<A> t2) =>
            Node(new KeyValuePair<A, bool>(t2.Data, object.ReferenceEquals(t, t2)),
                 l(t2.Left), r(t2.Right)),
            x => y => null, tree)(tree2);
    }
}

Во втором примере другое дерево реконструируется по-другому:

class Example
{
    // original version recreates entire tree, yuck 
    public static Tree<int> Change5to0(Tree<int> tree)
    {
        return Tree.FoldTree((int x, Tree<int> l, Tree<int> r) => Tree.Node(x == 5 ? 0 : x, l, r), null, tree);
    }

    // here it is with XFold - same as original, only with Xs 
    public static Tree<int> XChange5to0(Tree<int> tree)
    {
        return Tree.XFoldTree((int x, Tree<int> l, Tree<int> r, Tree<int> orig) =>
            Tree.XNode(x == 5 ? 0 : x, l, r)(orig), _ => null, tree);
    }
}

И в этом третьем примере сворачивание дерева используется для рисования:

class MyWPFWindow : Window 
{
    void Draw(Canvas canvas, Tree<KeyValuePair<int, bool>> tree)
    {
        // assumes canvas is normalized to 1.0 x 1.0 
        Tree.FoldTree((KeyValuePair<int, bool> kvp, Func<Transform, Transform> l, Func<Transform, Transform> r) => trans =>
        {
            // current node in top half, centered left-to-right 
            var tb = new TextBox();
            tb.Width = 100.0; 
            tb.Height = 100.0;
            tb.FontSize = 70.0;
                // the tree is a "diff tree" where the bool represents 
                // "ReferenceEquals" differences, so color diffs Red 
            tb.Foreground = (kvp.Value ? Brushes.Black : Brushes.Red);
            tb.HorizontalContentAlignment = HorizontalAlignment.Center;
            tb.VerticalContentAlignment = VerticalAlignment.Center;
            tb.RenderTransform = AddT(trans, TranslateT(0.25, 0.0, ScaleT(0.005, 0.005, new TransformGroup())));
            tb.Text = kvp.Key.ToString();
            canvas.Children.Add(tb);
            // left child in bottom-left quadrant 
            l(AddT(trans, TranslateT(0.0, 0.5, ScaleT(0.5, 0.5, new TransformGroup()))));
            // right child in bottom-right quadrant 
            r(AddT(trans, TranslateT(0.5, 0.5, ScaleT(0.5, 0.5, new TransformGroup()))));
            return null;
        }, _ => null, tree)(new TransformGroup());
    }

    public MyWPFWindow(Tree<KeyValuePair<int, bool>> tree)
    {
        var canvas = new Canvas();
        canvas.Width=1.0;
        canvas.Height=1.0;
        canvas.Background = Brushes.Blue;
        canvas.LayoutTransform=new ScaleTransform(200.0, 200.0);
        Draw(canvas, tree);
        this.Content = canvas;
        this.Title = "MyWPFWindow";
        this.SizeToContent = SizeToContent.WidthAndHeight;
    }
    TransformGroup AddT(Transform t, TransformGroup tg) { tg.Children.Add(t); return tg; }
    TransformGroup ScaleT(double x, double y, TransformGroup tg) { tg.Children.Add(new ScaleTransform(x,y)); return tg; }
    TransformGroup TranslateT(double x, double y, TransformGroup tg) { tg.Children.Add(new TranslateTransform(x,y)); return tg; }

    [STAThread]
    static void Main(string[] args)
    {
        var app = new Application();
        //app.Run(new MyWPFWindow(Tree.DiffTree(Tree.Tree7,Example.Change5to0(Tree.Tree7))));
        app.Run(new MyWPFWindow(Tree.DiffTree(Tree.Tree7, Example.XChange5to0(Tree.Tree7))));
    }
}    

Ответ 2

Я занимаюсь чтением, в том числе исследовательской работой Micorosft по функциональному программированию с катаморфизмом ("бананами"), и кажется, что катаморфизм просто относится к любой функции, которая принимает список и обычно разбивает его на одно значение (IEnumerable<A> => B), как Max(), Min() и в общем случае Aggregate(), будут катаморфизмами для списков.

Ранее у меня сложилось впечатление, что он ссылался на способ создания функции, которая может обобщать различные сгибы, чтобы можно было сгибать дерево и список. На самом деле может быть такая вещь, какой-то функтор или стрела, может быть, но прямо сейчас, за пределами моего уровня понимания.

Ответ 3

Ответ Брайана в первом абзаце верен. Но его пример кода на самом деле не отражает, как можно было бы решить аналогичные проблемы в стиле С#. Рассмотрим простой класс node:

class Node {
  public Node Left;
  public Node Right;
  public int value;
  public Node(int v = 0, Node left = null, Node right = null) {
    value = v;
    Left = left;
    Right = right;
  }
}

С этим мы можем создать дерево в основном:

var Tree = 
    new Node(4,
      new Node(2, 
        new Node(1),
        new Node(3)
      ),
      new Node(6,
        new Node(5),
        new Node(7)
      )
    );

Определим общую функцию fold в пространстве имен node:

public static R fold<R>(
  Func<int, R, R, R> combine,
  R leaf_value,
  Node tree) {

  if (tree == null) return leaf_value;

  return 
    combine(
      tree.value, 
      fold(combine, leaf_value, tree.Left),
      fold(combine, leaf_value, tree.Right)
    );
}

Для катаморфизмов мы должны указать состояния данных, узлы могут быть нулевыми или иметь дочерние элементы. Общие параметры определяют, что мы делаем в любом случае. Обратите внимание, что итерационная стратегия (в данном случае рекурсия) скрыта внутри функции сгиба.

Теперь вместо написания:

public static int Sum_Tree(Node tree){
  if (tree == null) return 0;
  var accumulated = tree.value;
  accumulated += Sum_Tree(tree.Left);
  accumulated += Sum_Tree(tree.Right);
  return accumulated; 
}

Мы можем написать

public static int sum_tree_fold(Node tree) {
  return Node.fold(
    (x, l, r) => x + l + r,
    0,
    tree
  );
}

Элегантный, простой, проверенный тип, поддерживаемый и т.д. Прост в использовании Console.WriteLine(Node.Sum_Tree(Tree));.

Легко добавить новую функциональность:

public static List<int> In_Order_fold(Node tree) {
  return Node.fold(
    (x, l, r) => {
      var tree_list = new List<int>();
      tree_list.Add(x);
      tree_list.InsertRange(0, l);
      tree_list.AddRange(r);
      return tree_list;
    },
    new List<int>(),
    tree
  );
}
public static int Height_fold(Node tree) {
  return Node.fold(
    (x, l, r) => 1 + Math.Max(l, r),
    0,
    tree
  );
}

F # выигрывает в категории краткости для In_Order_fold, но это следует ожидать, когда язык предоставляет выделенные операторы для построения и использования списков.

Резкое различие между С# и F #, по-видимому, связано с использованием F # закрытий, чтобы действовать как неявные структуры данных, для запуска оптимизации хвостового вызова. Пример в ответе Брайана также учитывает оптимизацию учетной записи в F # для уклонения от восстановления дерева. Я не уверен, что С# поддерживает оптимизацию хвостового вызова, и, возможно, In_Order_fold может быть написана лучше, но ни одна из этих точек не имеет значения при обсуждении того, как выразительный С# имеет дело с этими Catamorphisms.

При переводе кода между языками вам необходимо понять основную идею этой техники, а затем реализовать идею с точки зрения примитивов языка.

Возможно, теперь вы сможете убедить своих сотрудников С# более серьезно относиться к складкам.

Ответ 4

Я понимаю, что это обобщение складок (т.е. отображение структура многих значений для одного значение, включая список значений для другой список).

Я бы не сказал ни одного значения. Он отображает его в другую структуру.

Может быть, пример будет clarify.let сказать суммирование по списку.

foldr (\ x → \y → x + y) 0 [1,2,3,4,5]

Теперь это уменьшится до 15. Но на самом деле его можно рассматривать как сопоставление с чисто синтаксической структурой 1 + 2 + 3 + 4 + 5 + 0. Дело в том, что язык программирования (в приведенном выше случае, haskell) знает, как уменьшить указанную синтаксическую структуру до 15.

В принципе, катаморфизм заменяет один конструктор данных другим. В случае приведенного выше списка

[1,2,3,4,5] = 1: 2: 3: 4: 5: [] (: - оператор cons, [] - элемент nil) вышеперечисленный катаморфизм: с + и [] с 0.

Он может быть обобщен на любые рекурсивные типы данных.

Ответ 5

У Брайана была большая серия сообщений в его блоге. Также Channel9 имел приятное видео. Синтаксический сахар LINQ для .Aggregate() не имеет значения, имеет ли он определение метода LINQ Aggregate или нет? Идея, конечно же, та же. Складывание по деревьям... Сначала нам нужен Node... возможно, может быть использован Tuple, но это более понятно:

public class Node<TData, TLeft, TRight>
{
    public TLeft Left { get; private set; }
    public TRight Right { get; private set; }
    public TData Data { get; private set; }
    public Node(TData x, TLeft l, TRight r){ Data = x; Left = l; Right = r; }
}

Тогда в С# мы можем сделать рекурсивный тип, даже это необычно:

public class Tree<T> : Node</* data: */ T, /* left: */ Tree<T>, /* right: */ Tree<T>>
{
    // Normal node:
    public Tree(T data, Tree<T> left, Tree<T> right): base(data, left, right){}
    // No children:
    public Tree(T data) : base(data, null, null) { }
}

Теперь я приведу некоторые из кода Брайана с небольшими изменениями в стиле LINQ:

  • В С# Fold называется агрегатом
  • Методы LINQ - это методы расширения, которые имеют элемент как первый параметр с "this" -keyword.
  • Loop может быть закрытым

...

public static class TreeExtensions
{
    private static R Loop<A, R>(Func<A, R, R, Tree<A>, R> nodeF, Func<Tree<A>, R> leafV, Tree<A> t, Func<R, R> cont)
    {
        if (t == null) return cont(leafV(t));
        return Loop(nodeF, leafV, t.Left, lacc =>
                Loop(nodeF, leafV, t.Right, racc =>
                cont(nodeF(t.Data, lacc, racc, t))));
    }    
    public static R XAggregateTree<A, R>(this Tree<A> tree, Func<A, R, R, Tree<A>, R> nodeF, Func<Tree<A>, R> leafV)
    {
        return Loop(nodeF, leafV, tree, x => x);
    }

    public static R Aggregate<A, R>(this Tree<A> tree, Func<A, R, R, R> nodeF, R leafV)
    {
        return tree.XAggregateTree((x, l, r, _) => nodeF(x, l, r), _ => leafV);
    }
}

Теперь использование довольно похоже на С#:

[TestMethod] // or Console Application:
static void Main(string[] args)
{
    // This is our tree:
    //     4 
    //  2     6 
    // 1 3   5 7 
    var tree7 = new Tree<int>(4, new Tree<int>(2, new Tree<int>(1), new Tree<int>(3)),
                            new Tree<int>(6, new Tree<int>(5), new Tree<int>(7)));

    var sumTree = tree7.Aggregate((x, l, r) => x + l + r, 0);
    Console.WriteLine(sumTree); // 28
    Console.ReadLine();

    var inOrder = tree7.Aggregate((x, l, r) =>
        {
            var tmp = new List<int>(l) {x};
            tmp.AddRange(r);
            return tmp;
        }, new List<int>());
    inOrder.ForEach(Console.WriteLine); // 1 2 3 4 5 6 7
    Console.ReadLine();

    var heightTree = tree7.Aggregate((_, l, r) => 1 + (l>r?l:r), 0);
    Console.WriteLine(heightTree); // 3
    Console.ReadLine();
}

Мне еще нравится F # больше.