use*_*855 22 functional-programming
好吧,我想我明确指出这个方向的方向很清楚.现在有很多关于不变性(constness)的优点的讨论.Java书中的Concurrent编程也谈到了很多.
然而,这一切正是我所读到的.我个人而言,在功能语言中没有多少编码.对我来说,使用不可变对象可以舒适地工作看起来非常令人惊讶.从理论上讲,这绝对是可能的.但是,从实际的角度来看,是一种非常舒适的体验.或者我必须开发什么样的新推理(对于FP),以便我不需要那么多的可变性.
当你被迫使用不可变对象时,我将不胜感激如何考虑编写程序.
Jay*_*rod 26
不变性有几个优点,包括(但不限于):
void.这意味着可以将多个操作链接在一起.例如("foo" + "bar" + "baz").length()
map,reduce,filter,等都是对集合的基本操作.这些可以以多种方式组合,并且可以替换程序中的大多数循环.当然有一些缺点:
但是,从实际的角度来看,是一种非常舒适的体验.
我喜欢在我的大多数函数式编程中使用F#.为了它的价值,你可以用C#编写功能代码,它真的非常讨厌和难看.此外,我发现GUI开发抵制功能编程风格.
幸运的是,业务代码似乎很好地适应了功能样式:)那也和Web开发一样 - 想想,每个HTTP请求都是无状态的.每次"修改"状态时,都会将服务器传递给某个状态,然后返回一个全新的页面.
当你被迫使用不可变对象时,我将不胜感激如何考虑编写程序.
不可变对象应该很小
在大多数情况下,当对象具有少于3或4个内在属性时,我发现不可变数据结构最容易使用.例如,红黑树中的每个节点都有4个属性:颜色,值,左子和右子.堆栈有两个属性,一个值和指向下一个堆栈节点的指针.
考虑一下您公司的数据库,您可能拥有包含20,30,50个属性的表.如果您需要在整个应用程序中修改这些对象,那么我肯定会拒绝让这些对象变为不可变的冲动.
C#/ Java/C++不是很好的函数式语言.请改用Haskell,OCaml或F#
根据我自己的经验,不可变对象比类似于C的语言更容易在类似ML的语言中读写1000倍.对不起,但是一旦你有模式匹配和联合类型,你就不能放弃了它们:)此外,一些数据结构可以利用尾调用优化,这是你在某些C-中没有得到的一个特性.喜欢语言.
但只是为了好玩,这是C#中的一个不平衡的二叉树:
class Tree<T> where T : IComparable<T>
{
public static readonly ITree Empty = new Nil();
public interface ITree
{
ITree Insert(T value);
bool Exists(T value);
T Value { get; }
ITree Left { get; }
ITree Right { get; }
}
public sealed class Node : ITree
{
public Node(T value, ITree left, ITree right)
{
this.Value = value;
this.Left = left;
this.Right = right;
}
public ITree Insert(T value)
{
switch(value.CompareTo(this.Value))
{
case 0 : return this;
case -1: return new Node(this.Value, this.Left.Insert(value), this.Right);
case 1: return new Node(this.Value, this.Left, this.Right.Insert(value));
default: throw new Exception("Invalid comparison");
}
}
public bool Exists(T value)
{
switch (value.CompareTo(this.Value))
{
case 0: return true;
case -1: return this.Left.Exists(value);
case 1: return this.Right.Exists(value);
default: throw new Exception("Invalid comparison");
}
}
public T Value { get; private set; }
public ITree Left { get; private set; }
public ITree Right { get; private set; }
}
public sealed class Nil : ITree
{
public ITree Insert(T value)
{
return new Node(value, new Nil(), new Nil());
}
public bool Exists(T value) { return false; }
public T Value { get { throw new Exception("Empty tree"); } }
public ITree Left { get { throw new Exception("Empty tree"); } }
public ITree Right { get { throw new Exception("Empty tree"); } }
}
}
Run Code Online (Sandbox Code Playgroud)
Nil类代表一棵空树.我更喜欢这种表示而不是null表示,因为null检查是魔鬼的化身:)
每当我们添加节点时,我们都会创建一个插入节点的全新树.这比听起来更有效,因为我们不需要复制树中的所有节点; 我们只需要"在路上"复制节点并重用任何未更改的节点.
假设我们有一棵这样的树:
e
/ \
c s
/ \ / \
a b f y
Run Code Online (Sandbox Code Playgroud)
好的,现在我们要插入w到列表中.我们将从根开始e,移动到s,然后到y,然后y用一个替换左边的孩子w.我们需要在下来的路上创建节点的副本:
e e[1]
/ \ / \
c s ---> c s[1]
/ \ / \ / \ /\
a b f y a b f y[1]
/
w
Run Code Online (Sandbox Code Playgroud)
好的,现在我们插入一个g:
e e[1] e[2]
/ \ / \ / \
c s ---> c s[1] --> c s[2]
/ \ / \ / \ /\ / \ / \
a b f y a b f y[1] a b f[1] y[1]
/ \ /
w g w
Run Code Online (Sandbox Code Playgroud)
我们重用树中的所有旧节点,因此没有理由从头开始重建整个树.该树具有与其可变对应物相同的计算复杂度.
编写红黑树,AVL树,基于树的堆以及许多其他数据结构的不可变版本也非常容易.
许多函数式语言都是非纯粹的(允许突变和副作用)。
例如,如果您查看集合中的一些非常低级别的构造,您会发现其中一些在幕后使用迭代,并且相当多的使用一些可变状态(如果您想获取集合的前 n 个元素)例如,有一个计数器就更容易)。
诀窍在于,这通常是:
大量的功能代码证明了可以在很大程度上避免状态变化。对于使用命令式语言长大的人来说,这有点难以理解,尤其是之前在循环中将代码编写为递归函数。更棘手的是在可能的情况下将它们编写为尾递归。知道如何做到这一点是有益的,并且可以带来更具表现力的解决方案,重点关注逻辑而不是实现。很好的例子是那些处理集合的例子,其中没有、一个或多个元素的“基本情况”被清晰地表达,而不是成为循环逻辑的一部分。
虽然情况更好,但实际上是 2。最好通过一个例子来完成:
获取您的代码库并将每个实例变量更改为只读[1][2]。仅将那些需要它们可变以使代码正常运行的部分改回(如果您只在构造函数外部设置它们一次,请考虑尝试将它们作为构造函数的参数,而不是通过属性之类的东西进行可变。
有一些代码库不能很好地工作,例如 gui/widget 重代码和一些库(特别是可变集合),但我想说,最合理的代码将允许超过 50% 的所有实例字段变为只读。
此时您必须问自己,“为什么默认值是可变的?”。事实上,可变字段是程序的一个复杂方面,因为即使在单线程世界中,它们的交互也有更大的不同行为范围;因此,它们最好被突出显示并吸引编码人员的注意,而不是“赤裸裸”地遭受世界的破坏。
值得注意的是,大多数函数式语言要么没有 null 的概念,要么使其很难使用,因为它们不是与变量一起工作,而是与命名值一起工作,这些命名值的值与名称同时定义(井范围)。
我发现不幸的是,c# 也没有复制 java 的局部变量不变性概念。能够强调断言某些内容不会改变有助于明确意图,即值是在堆栈上还是在对象/结构中。
如果你有 NDepend 那么你可以找到这些WARN IF Count > 0 IN SELECT FIELDS WHERE IsImmutable AND !IsInitOnly