What is a catamorphism and can it be implemented in C# 3.0?

后端 未结 5 625
闹比i
闹比i 2020-12-04 13:22

I\'m trying to learn about catamorphisms and I\'ve read the Wikipedia article and the first couple posts in the series of the topic for F# on the Inside F# blog.

相关标签:
5条回答
  • 2020-12-04 13:58

    LINQ's Aggregate() is just for IEnumerables. Catamorphisms in general refer to the pattern of folding for an arbitrary data type. So Aggregate() is to IEnumerables what FoldTree (below) is to Trees (below); both are catamorphisms for their respective data types.

    I translated some of the code in part 4 of the series into C#. The code is below. Note that the equivalent F# used three less-than characters (for generic type parameter annotations), whereas this C# code uses more than 60. This is evidence why no one writes such code in C# - there are too many type annotations. I present the code in case it helps people who know C# but not F# play with this. But the code is so dense in C#, it's very hard to make sense of.

    Given the following definition for a binary tree:

    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);
        }
    }
    

    One can fold trees and e.g. measure if two trees have different nodes:

    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);
        }
    }
    

    In this second example, another tree is reconstructed differently:

    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);
        }
    }
    

    And in this third example, folding a tree is used for drawing:

    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))));
        }
    }    
    
    0 讨论(0)
  • 2020-12-04 14:04

    I've been doing more reading, including a Micorosft Research paper on functional programming with catamorphisms ("bananas"), and it seems that catamorphism just refers to any function that takes a list and typically breaks it down to a single value (IEnumerable<A> => B), like Max(), Min(), and in the general case, Aggregate(), would all be a catamorphisms for lists.

    I was previously under the impression that it refefred to a way of creating a function that can generalize different folds, so that it can fold a tree and a list. There may actually still be such a thing, some kind of functor or arrow maybe but right now that's beyond my level of understanding.

    0 讨论(0)
  • 2020-12-04 14:14

    I understand that it's a generalization of folds (i.e., mapping a structure of many values to one value, including a list of values to another list).

    I wouldn't say one value.It maps it into another structure.

    Maybe an example would clarify.let's say summation over a list.

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

    Now this would reduce to 15. But actually,it can be viewed mapping to a purely syntactic structure 1 + 2 + 3 + 4 + 5 + 0. It is just that the programming language(in the above case,haskell) knows how to reduce the above syntactic structure to 15.

    Basically,a catamorphism replaces one data constructor with another one.In case of above list,

    [1,2,3,4,5] = 1:2:3:4:5:[] (: is the cons operator,[] is the nil element) the catamorphism above replaced : with + and [] with 0.

    It can be generalized to any recursive datatypes.

    0 讨论(0)
  • 2020-12-04 14:18

    Brian's answer in the first paragraph is correct. But his code example doesn't really reflect how one would solve similar problems in a C# style. Consider a simple class 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;
      }
    }
    

    With this we can create a tree in main:

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

    We define a generic fold function in Node's namespace:

    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)
        );
    }
    

    For catamorphisms we should specify the states of data, Nodes can be null, or have children. The generic parameters determine what we do in either case. Notice the iteration strategy(in this case recursion) is hidden inside the fold function.

    Now instead of writing:

    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; 
    }
    

    We can write

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

    Elegant, simple, type checked, maintainable, etc. Easy to use Console.WriteLine(Node.Sum_Tree(Tree));.

    It's easy to add new functionality:

    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# wins in the conciseness category for In_Order_fold but that's to be expected when the language provides dedicated operators for constructing and using lists.

    The dramatic difference between C# and F# seems to be due to F#'s use of closures, to act as implicit data structures, for triggering the tail call optimization. The example in Brian's answer also takes in to account optimizations in F#, for dodging reconstructing the tree. I'm not sure C# supports the tail call optimization, and maybe In_Order_fold could be written better, but neither of these points are relevant when discussing how expressive C# is when dealing with these Catamorphisms.

    When translating code between languages, you need to understand the core idea of the technique, and then implement the idea in terms of the language's primitives.

    Maybe now you'll be able to convince your C# co-workers to take folds more seriously.

    0 讨论(0)
  • 2020-12-04 14:18

    Brian had great series of posts in his blog. Also Channel9 had a nice video. There is no LINQ syntactic sugar for .Aggregate() so does it matter if it has the definition of LINQ Aggregate method or not? The idea is of course the same. Folding over trees... First we need a Node... maybe Tuple could be used, but this is more clear:

    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; }
    }
    

    Then, in C# we can make a recursive type, even this is unusual:

    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) { }
    }
    

    Now, I will quote some of Brian's code, with slight LINQ-style modifications:

    1. In C# Fold is called Aggregate
    2. LINQ methods are Extension methods that have the item as first parameter with "this"-keyword.
    3. Loop can be private

    ...

    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);
        }
    }
    

    Now, the usage is quite C#-style:

    [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();
    }
    

    I still like F# more.

    0 讨论(0)
提交回复
热议问题