使用 C# 偏函数(Partial Application)函数以 Map/Filter 风格编写的对象浏览器





5.00/5 (15投票s)
除其他事项外,这也是一次关于以函数式编程风格编写 C# 代码的探索。
最近,我想要直观地检查由某个服务创建的对象结构,于是我编写了一个函数,该函数通过反射来遍历对象,并构建一个类属性的树,然后深入到 List<T>
集合中。 足够简单,但我心想,这可能很有用,我想扩展它来处理 Dictionary<A, B>
、枚举、可空属性类型等。 主要的递归函数最终成了一个 130 行左右的、组织混乱的代码。 在考虑将其重构为更易于管理的部分时,我意识到所有这些代码基本上都是 map 和 filter 操作的命令式实现。 因此,我决定深入研究,探索一种以 map/filter 风格编写的实现方式。 当我意识到偏函数(Partial Application)函数也同样非常有用时,这个“兔子洞”就更深了。 此处提供的代码就是结果,感谢两位博主:
- Mike Hadlow,参考 http://mikehadlow.blogspot.com/2015/09/partial-application-in-c.html
- Jon Skeet,参考 https://codeblog.jonskeet.uk/2012/01/30/currying-vs-partial-function-application/
在本文中,我使用“函数”(function)而不是“方法”(method)(后者在 C# 中更常见),以传达我们在 C# 中进行函数式编程的概念。
我学到的东西
函数式编程会产生高度抽象的代码
使用偏函数(Partial Application)和接受函数作为参数的函数进行函数式编程,会产生一种让你感到“这到底做了什么?”的代码,因为一切都被高度抽象化了。 这可能解释了为什么函数式编程语言编写的程序要难得多。
抽象思考很难
我发现的另一件事是,在这个抽象级别上思考有多难。 为了达到高度抽象,必须仔细考虑函数的参数,尤其是在将函数与偏函数(Partial Application)结合使用时。 命令式编程通常更具体——它执行语句所说的操作——而像编写 for 循环这样的常见做法,虽然可以被抽象为生成迭代器,但除非你专门实现 IEnumerable<T>
集合,否则永远不会那样写。 命令式函数通常在不考虑参数顺序的情况下编写——我需要给函数 x、y 和 z,并期望得到 r。 这(可能是一件好事)限制了偏函数(Partial Application)和柯里化(Currying)可以达到的抽象级别。 例如,一个可重复的“迭代和操作”模式并将其转换为一个通用函数,如
void Map<T>(Func<PropertyNode, IEnumerable<T>> iterator, Action<PropertyNode, T> action, PropertyNode pn)
很少被做到。
类型推断和泛型
这真正考验了 C# 的泛型和类型推断。 函数式编程语言的优势在于其类型推断要好得多。 例如,在函数式编程中,我可能会写出类似这样的代码:
let mapper = (a, b, c) => Map(a, b, c);
然后类型推断引擎会根据其用法推断出 a、b、c 的类型,例如
action = mapper.PartialApply(DictionaryIterator, CreateCollectionChild);
在 C# 中,你不能写
var mapper = (a, b, c) => Map(a, b, c);
你必须显式定义类型,结果是(请做好准备)
Action<Func<PropertyNode, IEnumerable<KeyValue>>, Action<PropertyNode, KeyValue>, PropertyNode> mapper = (a, b, c) => Map(a, b, c);
这可能也解释了为什么 C# 代码写成命令式风格更多!
偏函数(Partial Application)和柯里化(Currying)
如果使用得当,偏函数(Partial Application)和柯里化(Currying)是极好的。 然而,这是两个非常不同的概念,我稍后会描述它们。 问题在于 C# 不支持将偏函数(Partial Application)和柯里化(Currying)作为语言语法的一部分。 有几种选择:
令人头疼的扩展方法
// Three parameter partial apply given one parameter: public static Action<B, C> PartialApply<A, B, C>(this Action<A, B, C> action, A arg1) => (b, c) => action(arg1, b, c);
以及一个用例,例如
action = mapper.PartialApply(DictionaryIterator, CreateCollectionChild);
使用 Lambda 表达式,第一部分
如果你不喜欢扩展方法,你可以写这个:
action = a => mapper(DictionaryIterator, CreateCollectionChild, a);
使用 Lambda 表达式,第二部分
或者更好(甚至摆脱了 mapper 变量):
action = a => Map(DictionaryIterator, CreateCollectionChild, a);
所以你看,有几种方法可以解决这个问题,最后一种是最简洁易读的,只要你熟悉 lambda 表达式。
现在注意 C# 的类型推断是如何完美工作的!
现在显然...
C# 已经通过 Linq 中的 select
和 where
关键字支持映射和过滤(以及使用 aggregate
进行折叠,它使用一个运行中的累加器)。 这意味着我的 Map 和 Filter 函数实际上是相当不必要的——它们在这里主要用作教学练习,正式声明一个操作是映射操作还是过滤操作。
另外,我一直忘记的一点是,能够根据 lambda 表达式的隐式参数来调用函数。 所以,例如:
return Filter(objectProperties, prop => GetMethodCountIsZero(prop));
被简化为:
return Filter(objectProperties, GetMethodCountIsZero);
这在功能上更符合函数式编程风格。
我喜欢最终结果的三个方面
最突出的一点是,生成的代码由函数组成,如果不是单行代码,通常也只有 5 行或更少!
我喜欢这种方法的第二点是,它完全消除了 if-then-else
代码,利用了 C# 7 的 case ... when
语法。 模式匹配,特别是当用于每个 case 都返回而不是 break 时,强制实现 default
或最终的 else
case,在我看来,这会导致代码更健壮。
第三点(但并非最不重要)是,我喜欢这种方法,特别是当使用如上所述的模式匹配并结合映射和过滤时,你的代码更健壮,更不容易抛出异常。 这是更函数式编程风格的一个巨大好处! 如果你的代码确实抛出了异常,那是因为你没有正确处理一个 case(好吧,这对于命令式代码也是显而易见的),只是函数式编程使得处理所有 cases 更加明确。 你需要使用一段时间才能相信这一点。
偏函数(Partial Application)与柯里化(Currying)
你在网上看到的关于偏函数(Partial Application)和柯里化(Currying)的大多数示例(即使在 F# 中)都谈论的是接受两个参数的简单函数。 这是没有意义的,因为当你偏应用一个参数或柯里化一个参数时,解决操作的调用是相同的,所以你真的无法体会到语法上的差异。 但是,我将通过演示三个(哇!)参数的偏函数(Partial Application)和柯里化(Currying)之间的区别来避免这种情况。
部分应用
非常简单地说,偏函数(Partial Application)允许你指定前 n 个参数,返回一个接受剩余参数的函数。 给定一个返回三个值之和的函数:
Func<int, int, int, int> add = (a, b, c) => a + b + c;
你可以偏应用一个或两个参数:
Func<int, int, int> needsTwoMoreParams = (b, c) => add(3, b, c); Func<int, int> needsOneMoreParam = (c) => add(3, 4, c);
使用这些偏函数(Partial Application)函数如下所示:
int twelve = needsTwoMoreParams(4, 5); twelve = needsOneMoreParam(5);
请注意,我们必须显式定义偏函数(Partial Application)函数的返回类型。 我们不能使用 var!
var oneParam = (b, c) => add(3, b, c);
无法将 lambda 表达式分配给隐式类型变量。
在 F# 这样的函数式编程语言中,类型是被推断的。
> let add a b c = a + b + c;; val add : a:int -> b:int -> c:int -> int > let add3 = add 3;; val add3 : (int -> int -> int) > let add3and4 = add 3 4;; val add3and4 : (int -> int)
在 C# 中进行偏函数(Partial Application)时要注意的关键是 (b, c)
这个结构,用于 needsTwoMoreParams
函数中的剩余参数。 这表明它是一个偏函数(Partial Application),而不是柯里化(Currying)!
你也可以对不是 C# Func
类型而是 Action
类型的函数做同样的事情:
Action<int, int, int> xadd = (a, b, c) => Console.WriteLine(a + b + c); Action<int, int> xoneParam = (b, c) => add(3, b, c); Action<int> xtwoParams = (c) => add(3, 4, c);
柯里化
正如 Jon Skeet 优雅地写道:“柯里化(Currying)有效地将函数分解为接受单个参数的函数。” 请仔细研究这个例子:
Func<int, Func<int, Func<int, int>>> curried = a => b => c => a + b + c; int twelve = curried(3)(4)(5); Func<int, Func<int, int>> oneParam = curried(3); Func<int, int> twoParams = curried(3)(4); twelve = twoParams(5);
注意柯里化(Curried)函数的参数是如何作为单独的参数调用的:(3)(4)(5)
。 另请注意,该函数定义为 a => b => c=>
,这表明这是一个柯里化(Curried)函数!
有趣的是,在这种情况下我们 *可以* 使用 var
。
var oneParam = curried(3); var twoParams = curried(3)(4);
但同样,C# 中定义柯里化(Curried)函数的语法必须是显式的——“一个接受 int 并返回一个接受 int 并返回一个接受 int 并返回 int 的函数。” 这很快就会变得很丑陋,而且在 F# 中,推断引擎知道你正在进行柯里化(Currying)。
> add(3);; val it : (int -> int -> int) = <fun:it@9> > let add3 = add(3);; val add3 : (int -> int -> int) > let add3and4 = add(3)(4);; val add3and4 : (int -> int) > add3and4(5);; val it : int = 12
在 F# 中也注意到这一点:
add3(4, 5);; add3(4, 5);; -----^^^^ stdin(13,6): error FS0001: This expression was expected to have type 'int' but here has type ''a * 'b'
在 F# 中,正确的语法与 C# 类似——每个“函数”一个参数。
> add3(4)(5);; val it : int = 12
同样,一旦你创建了一个柯里化(Curried)函数,你必须像对待柯里化(Curried)函数一样对待它,就像在 C# 中一样。
那柯里化(Curried)的 Action
函数呢? 它看起来会是这样:
Func<int, Func<int, Action<int>>> curried = a => b => c => Console.WriteLine(a + b + c); var oneParam = curried(3); var twoParams = curried(3)(4); twoParams(5); // prints 12
注意最后一个 Func
的最终结果是一个接受最后一个值的 Action
。
现在希望你已经清楚地理解了偏函数(Partial Application)和柯里化(Currying)之间的区别。
代码
让我们开始讨论本文的主题,“实例浏览器”。 回顾一下,基本思想是获取一个对象(某个东西的实例)并递归地创建一个所有属性的树。
扩展方法
首先,一些有用的扩展方法,它们应该是自explanatory的(尽管 Prepend 为什么存在将在后面解释)。
public static class ExtensionsMethods { public static string Quote(this string src) { return "\"" + src + "\""; } public static string Prepend(this string src, string pre) { return pre + src; } public static void ForEach<T>(this IEnumerable<T> items, Action<T> action) { foreach (T item in items) action(item); } // ====== From Phil's answer here: ============= // <a href="https://stackoverflow.com/questions/16466380/get-user-friendly-name-for-generic-type-in-c-sharp">https://stackoverflow.com/questions/16466380/get-user-friendly-name-for-generic-type-in-c-sharp</a> public static string ToCsv(this IEnumerable<object> collectionToConvert, string separator = ", ") { return String.Join(separator, collectionToConvert.Select(o => o.ToString())); } public static string FriendlyName(this Type type) { if (type.IsGenericType) { var namePrefix = type.Name.Split(new[] { '`' }, StringSplitOptions.RemoveEmptyEntries)[0]; var genericParameters = type.GetGenericArguments().Select(FriendlyName).ToCsv(); return namePrefix + "<" + genericParameters + ">"; } return type.Name; } // ============================================== }
FriendlyName
是一个巧妙的小函数,它会递归进入泛型参数,并将它们转换为你在代码中使用的熟悉语法。 功劳归于“Phil”以及 StringSplitOptions
和 Select(FriendlyName)
的巧妙使用。
树节点和节点容器
属性树的每个节点由以下内容处理:
public class Node { public string Name { get; set; } public string Value { get; set; } public string PropertyName { get; set; } public Node Parent { get; set; } public List<Node> ChildNodes { get; protected set; } public string DisplayText { get { return Name + Value?.Prepend(" : "); } } public Node() { ChildNodes = new List<Node>(); } public string TreeToString(StringBuilder sb = null, int indent = 0) { sb = sb ?? new StringBuilder(); sb.Append(new String(' ', indent)); sb.AppendLine(DisplayText); ChildNodes.ForEach(n => n.TreeToString(sb, indent + 2)); return sb.ToString(); } }
一个节点处理所有它需要知道的关于自己、它的子节点和它的父节点的信息。 它还可以创建一个递归缩进的字符串,包含对象的所有属性,我基本上只用它来将输出转储到控制台,如下所示:
List<int> list = new List<int>() { 1, 2, 3 }; root = Parser.CreateInstanceTree(list); Console.WriteLine(root.Node.TreeToString()); Console.WriteLine();
结果是:
我还为每个节点创建了一个包装器,该包装器关联了实际对象和对象类型:
public class PropertyNode { public object Object { get; set; } public Type ObjectType { get; set; } public Node Node { get; set; } }
我不确定这是否增加了太多价值——我最初打算通过使用一个关联两者的容器类来保持对象和树中的节点之间的清晰分离。 对于像这样简单的事情来说,这是过度设计了,而且 Node
最终比我预期的要专业化得多,所以将 Object
和 ObjectType
放入其中将简化一个糟糕的设计,但它仍然是一个糟糕的设计,哈哈。 然而,这一切都只是对文章主旨的题外话。
创建树
如上面的示例所示,创建树是一个简单的调用,传递你想要转换为属性树的对象,例如:
root = Parser.CreateInstanceTree(new List<int>() {1, 2, 3});
CreateInstanceTree
简单地做这个:
public static PropertyNode CreateInstanceTree(object obj, PropertyNode parent = null, string propertyName = null) { PropertyNode pn = ObjectToPropertyNodeTransform(obj, parent, propertyName); ObjectTypeToActionTransform(pn); return pn; }
这里有几点需要注意:
- 这个函数是递归函数的想法,通过一眼看这个函数是完全看不出来的。 你可能会推断出这里有些东西是递归的,因为它创建了一个树,但是什么呢?
- 我显式地命名函数...Transform,因为我真的想强调这里的想法,即这段代码只有三件事在发生:
- 将 A 类型对象转换为 B 类型对象的简单转换。
- 将集合映射到另一个集合(可能不同类型)或“动作”。
- 过滤集合,可能生成一个更小的同类型集合。
- 因此,其中一些代码稍微冗长一些,但稍微冗长一些的好处是代码的语义表达能力更强——函数告诉你代码的作用,而不是让你通过阅读代码来弄清楚代码的作用。
在这个例子中,有目的的第三条腿是“reduce”(也称为“fold”),正如前面提到的,它最常作为聚合器,使用一个运行中的累加器。
ObjectToPropertyNodeTransform
这是一个实例化 PropertyNode 的花哨名称!
private static PropertyNode ObjectToPropertyNodeTransform(object obj, PropertyNode parent = null, string propertyName = null) { Node node = CreateNode(parent?.Node); node.Name = PropertyNameOrObjectTypeName(propertyName, obj); parent?.Node.ChildNodes.Add(node); return new PropertyNode() { Node = node, Object = obj, ObjectType = obj?.GetType() }; }
这里的重点是,我们通常不将对象构造视为转换,但事实正是如此——我们获取一些值并从中构建一个对象。 例如,在这里,我们将一组输入转换为一个关联了 Node
和对象的容器。 如果从字面上看,这是一个转换:
Node node = CreateNode(parent?.Node);
而这是一个转换:
node.Name = PropertyNameOrObjectTypeName(propertyName, obj);
我相信我们太频繁地未能将代码视为转换,这导致代码的可读性降低(如果可以这么说的话,就是它的*语义价值*),并且影响了代码的可重用性和可测试性。
ObjectTypeToActionTransform
这个函数实际上是整个过程的核心,因为它决定了根据对象类型要采取什么操作。 这里我们使用的是 C# 7 的模式匹配语法。 这是整个代码库中唯一一个超过 5 行代码的函数。
private static void ObjectTypeToActionTransform(PropertyNode pn) { switch (pn.ObjectType) { // Test null first, so we don't throw exceptions on the rest of the cases. case null: SetNullValue(pn); return; // Test dictionary first, because Dictionary also implements IList. case var objType when IsDictionary(objType): ActionMap(DictionaryIterator(pn), c => CreateCollectionChild(pn, c)); return; case var objType when IsList(objType): ActionMap(CollectionIterator(pn), c => CreateCollectionChild(pn, c)); return; case var objType when IsPrimitive(objType): SetValue(pn); return; case var objType when IsString(objType): SetQuotedValue(pn); return; case var objType when IsEnum(objType): SetValue(pn); return; default: ActionMap(PropertyIterator(pn), c=> CreatePropertyChild(pn, c)); return; } }
题外话,虽然我通常喜欢函数有单一的退出点,但让每个 case
显式返回的好处是强制执行默认处理程序,在我看来,通过要求你处理(并思考)所有需要处理的情况,包括默认情况,可以提高代码质量。
C# 7.0 的模式匹配功能使得代码更加简洁,事实上,我认为它完全消除了对“if-then-else”语句的需求。 以前,像这样的东西是不可能的:
case typeof(IDictionary):
预期是一个常量值。
相反,带有非常量值的测试必须使用 if 语句来完成。
相反,我们现在可以测试表达式中的 case,这些 case 由以下实现处理:
private static bool IsPrimitive(Type t) { // We don't use IsValueType because structs have this flag set, // and we need to treat structs like objects so we iterate their properties. return t.IsPrimitive; } public static bool IsEnum(Type t) { return t.IsEnum; } private static bool IsString(Type t) { return t == typeof(string); } private static bool IsDictionary(Type t) { return t.GetInterfaces().Any(i => i == typeof(IDictionary)); } private static bool IsList(Type t) { return t.GetInterfaces().Any(i => i == typeof(IList)); }
显然,这些可以直接在 case 语句中编写,例如:
case var objType when objType.GetInterfaces().Any(i => i == typeof(IDictionary)):
冒着重复的风险,我喜欢“函数告诉你它做了什么,而不是代码”的改进语义,这可以通过调用函数来实现。
另一个题外话,因为代码按对象类型而不是对象进行匹配,所以我不能这样做:
case IDictionary d:
switch 语句操作的是对象类型,这样可以检查类型的各种属性,正如上面的代码所示。
SetValue, SetQuotedValue, SetNullValue
这三个方法根据对象类型,为节点设置属性值:
private static void SetValue(PropertyNode pn) { pn.Node.Value = pn.Object.ToString(); } private static void SetQuotedValue(PropertyNode pn) { pn.Node.Value = pn.Object.ToString().Quote(); } private static void SetNullValue(PropertyNode pn) { pn.Node.Value = NULL_VALUE; }
再次,单行代码,函数名称描述了它们的作用。
ActionMap
这就是乐趣真正所在,也是递归发生的地方。 ActionMap
是一个函数,它接受一个 enumerable
和一个 action
,遍历枚举,为枚举中的每个项调用 action 函数。
public static void ActionMap<T>(IEnumerable<T> iterator, Action<T> action) { iterator.ForEach(pi => action(pi)); }
这里的关键在于你为迭代器和 action 传递什么!
这就是偏函数(Partial Application)发挥作用的地方
有两个递归过程:
- 对象集合。
- 对象的属性。
两者都有一个与之关联的动作,用于处理正在迭代的项(集合或属性)。 对于这两个函数,第一个参数是相同的,即 PropertyNode,第二个参数根据正在迭代的项的类型而变化。
CreateCollectionChild(PropertyNode pn, object item) CreatePropertyChild(PropertyNode pn, PropertyInfo pi)
通过偏函数(Partial Application),我们可以指定 PropertyNode 对象,并返回一个只有一个参数的委托,该参数可以是 object
或 PropertyInfo
。 这个委托与 ActionMap 的 action
参数的签名匹配。
Action<T> action
通过“仔细”规划处理迭代器对象的那些方法的参数顺序,我们可以使用偏函数(Partial Application)来提供通用参数,并让另一个函数确定最终参数。 甚至 C# 的类型推断也同样出色,在函数定义中:
ActionMap<T>(IEnumerable<T> iterator, Action<T> action)
类型推断会找出 <T>
是什么!
处理 IList
让我们看看 IList是如何处理的。 假设:
ActionMap(CollectionIterator(pn), c => CreateCollectionChild(pn, c));
第一个参数实际上是一个返回 IEnumerable<object>
的函数调用。
private static IEnumerable<object> CollectionIterator(PropertyNode pn) { foreach (var obj in (IEnumerable)pn.Object) { yield return obj; } }
足够简单。 第二个参数 c => CreateCollectionChild(pn, c)
则是处理枚举中每个项的方法的*偏函数*(Partial Application)。
private static void CreateCollectionChild(PropertyNode pn, object item) { CreateInstanceTree(item, pn); }
又是单行代码,这就是递归发生的地方!
处理对象
处理非值类型(字符串除外)也很类似。 假设:
ActionMap(PropertyIterator(pn), c=> CreatePropertyChild(pn, c));
我们有一个不同的迭代器,它会反射那些不是索引器的属性:
private static IEnumerable<PropertyInfo> PropertyIterator(PropertyNode pn) { var objectProperties = pn.ObjectType.GetProperties(BindingFlags.Instance | BindingFlags.Public); return Filter(objectProperties, IsNotIndexer); }
其中 Filter 执行此操作:
private static IEnumerable<T> Filter<T>(IEnumerable<T> props, Func<T, bool> filter) { foreach (var prop in props) { if (filter(prop)) { yield return prop; } } }
以及另一个单行代码:
private static bool IsNotIndexer(PropertyInfo prop) { return prop.GetMethod.GetParameters().Count() == 0; }
这本可以用 Linq 的 where
子句代替,但同样,为了本文的目的,我想明确地演示这是一个过滤操作,并且有一个显式的过滤函数。
ActionMap
处理的每个 PropertyInfo
项都再次由我们用 c=> CreatePropertyChild(pn, c)
定义的*偏函数*(Partial Method)处理。
private static void CreatePropertyChild(PropertyNode pn, PropertyInfo pi) { object val = pi.GetValue(pn.Object); CreateInstanceTree(val, pn, pi.Name); }
处理 IDictionary
鉴于
ActionMap(DictionaryIterator(pn), c => CreateCollectionChild(pn, c));
我们已经看到 CreateCollectionChild
的作用了,并且再次注意到*偏函数*(Partial Application)。 实际上,这里的 DictionaryIterator
更有趣:
private static IEnumerable<KeyValue> DictionaryIterator(PropertyNode pn) { IDictionary dict = (IDictionary)pn.Object; return DoubleEnumeratorMap(dict.Keys.GetEnumerator(), dict.Values.GetEnumerator(), CreateKeyValue); }
这里我们使用了一个专门的映射器,它接受两个枚举并同时遍历它们,如下所示:
public static IEnumerable<R> DoubleEnumeratorMap<R>(IEnumerator e1, IEnumerator e2, Func<object, object, R> map) { while (e1.MoveNext()) { e2.MoveNext(); yield return map(e1.Current, e2.Current); } }
映射器的目的是将字典键值对(我不使用泛型 KeyValue<K, V>
类,以便能够与实现 IDictionary
的非泛型类兼容)转换为我自己的 KeyValue
对象。
public class KeyValue { public object Key { get; set; } public object Value { get; set; } }
我们使用具体类 KeyValue
而不是匿名对象,仅仅是因为它使树显示看起来更好,因为中间键值树节点显示的文本将是“KeyValue”。
CreateKeyValue
是一个非常简单的转换:
private static KeyValue CreateKeyValue(object key, object val) { return new KeyValue() { Key = key, Value = val }; }
同样,这本可以内联作为 lambda 表达式编写:
return DoubleEnumeratorMap( dict.Keys.GetEnumerator(), dict.Values.GetEnumerator(), (k, v) => new KeyValue() { Key = k, Value = v });
就是这样!
这就是全部——代码在语义上具有表达力,函数很小,并且偏函数(Partial Application)用于创建通用的动作映射器。
填充树视图是一个简单的递归练习:
protected void PopulateTreeView(TreeNodeCollection tvNodes, Node node) { TreeNode childNode = tvNodes.Add(node.DisplayText); node.ChildNodes.ForEach(child => PopulateTreeView(childNode.Nodes, child)); }
结论
为什么所有的类方法都是静态的?
Mike Hadlow 有一篇关于完全用静态方法编程的精彩博文,非常值得一读。 我的解析器类中的方法都是静态的,因为类没有维护任何状态。 这是函数式编程风格的一个绝妙之处。 显然,当然也有需要管理状态的时候(毕竟我们生活在一个有状态的世界里),但对于这样的事情来说,状态是完全不必要的。 正如 Mike 所写:
“我想如果我回顾我的编程生涯,它有以下演变:
过程式 –> 面向对象 –> 函数式
OO 阶段现在看起来有点像一个迂回。
C# 拥有你进行函数式编程所需的所有基本功能——高阶函数、闭包、lambda 表达式——这些功能允许你完全抛弃 OO 编程模型。这导致了更简洁、可读和可维护的代码。它对单元测试也有巨大影响,允许你摆脱复杂的模拟框架,编写更简单的测试。”
我的经验完全证实了这一点。
这段代码能否通过程序生成?
本文的潜台词是我问自己的问题,“我将如何绘制这个图,以便代码有可能被自动生成?” 我拼凑了上面的高级框图,似乎如果“某样东西”知道每个框的含义,那么这段代码就可以被自动生成。 当然,它可能看起来与我写的很不一样! 尽管如此,关于如何以图解方式(而不下降到流程图的级别)来表达代码,这个问题仍然让我着迷。 我个人认为面向对象技术是错误的途径——更函数式的编程风格和使用匹配表达式在将图翻译成代码方面实际上更直观、更具表达力。
关于代码下载
代码下载包含四个项目:
- InstanceTreeMapper - 这是我最初的代码版本,如果你查看 Mapper.cs,
PopulateProperties
方法,你将看到我创建的那个 130 行的混乱代码。 - MapperUnitTests - 这些是验证 InstanceTreeMapper 项目创建的树的单元测试。 你会注意到一些在本文中未介绍的代码,例如索引集合。
- MapReduceFilterExample - 这是与本文相关的项目。 如果将其选为启动项目,它会将一个测试树转储到控制台。
- SampleInstance - 我最初的测试对象图测试用例。
- WinFormDemo - 一个 WinForm 树。 它使用本文中的代码渲染树。 在
CreateInstanceTree
(DemoForm.cs) 中切换注释掉的代码,可以看到不同算法的渲染。 注意在我最初的版本中用于渲染的PopulateTreeView
中的杂乱代码。
一些进一步阅读
Map/Reduce: https://en.wikipedia.org/wiki/MapReduce
MapReduce 程序由一个 Map() 过程(方法)组成,该过程执行过滤和排序(例如,按名字对学生进行排序到队列中,每个名字一个队列),以及一个 Reduce() 方法执行摘要操作(例如,计算每个队列中的学生数量,得出名字频率)。 “MapReduce 系统”(也称为“基础设施”或“框架”)通过编排分布式服务器、并行运行各种任务、管理系统各部分之间的所有通信和数据传输,并提供冗余和容错来协调处理。
列表推导式: https://en.wikipedia.org/wiki/List_comprehension
列表推导式是某些编程语言中用于根据现有列表创建列表的语法结构。 它遵循数学集合构建符号(集合推导式)的形式,而不是使用 map 和 filter 函数。
Fold: https://en.wikipedia.org/wiki/Fold_(higher-order_function)
在函数式编程中,fold(也称为 reduce、accumulate、aggregate、compress 或 inject)指的是一系列高阶函数,它们分析递归数据结构,并通过使用给定的组合操作,重新组合递归处理其 constituent parts 的结果,构建一个返回值。 通常,fold 会提供一个组合函数、一个数据结构的顶层节点,以及在特定条件下可能使用的默认值。 然后,fold 会以系统化的方式使用该函数,继续组合数据结构层次结构中的元素。
Map/Reduce/Filter 是图灵完备的吗?
https://codeproject.org.cn/Lounge.aspx?msg=5408388#xx5408388xx
https://swizec.com/blog/are-map-reduce-and-filter-turing-complete/swizec/6341