嗯,我想我明确指出这方面的方向很清楚。 现在有很多关于不变性(constness)的优点的讨论。 Java书中的Concurrent编程也谈到了很多。
但是,这一切正是我所读到的。我个人而言,在功能语言中没有多少编码。对我来说,使用不可变对象可以舒适地工作看起来非常令人惊讶。从理论上讲,这绝对是可能的。但是,从实际的角度来看,是一种非常舒适的体验。或者我必须开发什么样的新推理(对于FP),以便我不需要那么多的可变性。
我很感激当你被迫使用不可变对象时如何考虑编写程序。
答案 0 :(得分:25)
不变性有几个优点,包括(但不限于):
void
。这意味着可以将多个操作链接在一起。例如 ("foo" + "bar" + "baz").length()
map
,reduce
,filter
等操作是集合的基本操作。这些可以以多种方式组合,并且可以替换程序中的大多数循环。当然有一些缺点:
答案 1 :(得分:8)
但是,从实际的角度来看, 是一种非常舒适的体验。
我喜欢在大多数函数式编程中使用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"); } }
}
}
Nil类代表一棵空树。我更喜欢这种表示而不是空表示,因为空检查是魔鬼的化身:)
每当我们添加节点时,我们都会创建一个插入节点的全新树。这比听起来更有效,因为我们不需要复制树中的所有节点;我们只需要“在路上”复制节点,并重用任何没有改变的节点。
假设我们有这样一棵树:
e
/ \
c s
/ \ / \
a b f y
好的,现在我们要将w
插入列表中。我们将从根e
开始,转移到s
,然后转到y
,然后用y
替换w
的左边孩子。我们需要在下来的路上创建节点的副本:
e e[1]
/ \ / \
c s ---> c s[1]
/ \ / \ / \ /\
a b f y a b f y[1]
/
w
好的,现在我们插入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
我们重用树中的所有旧节点,因此没有理由从头开始重建整个树。该树具有与其可变对应物相同的计算复杂度。
编写红黑树,AVL树,基于树的堆以及许多其他数据结构的不可变版本也非常容易。
答案 2 :(得分:5)
另一个优点是,更容易推断以函数式编写的程序的语义(因此,没有副作用)。功能性编程本质上更具说明性,强调结果应该是什么,而不是更少,如何实现。不可变的数据结构有助于使您的程序更具功能性。
Mark Chu-Carrol对这个主题有一个很好的blog entry。
答案 3 :(得分:4)
许多功能语言都是非纯的(允许突变和副作用)。
例如,f#是,如果你看一下集合中的一些非常低级的结构,你会发现有几个使用迭代,而且有很多使用一些可变状态(如果你想采取第一个)例如,序列的n个元素就更容易拥有一个计数器。诀窍在于,这通常是:
可以在很大程度上避免变异状态,这可以通过大量的功能代码来证明。对于那些提出命令式语言的人来说,这有点难以理解,尤其是先前在循环中编写代码作为递归函数。甚至更棘手的是在可能的情况下将它们写为尾递归。知道如何做到这一点是有益的,并且可以产生更具表现力的解决方案,专注于逻辑而不是实现。很好的例子是那些处理集合的集合,其中没有,一个或多个元素的“基本情况”被清晰地表达而不是循环逻辑的一部分。
虽然事情变得更好,但真的是2。最好通过一个例子来完成:
获取代码库并将每个实例变量更改为readonly [1] [2]。只更改那些你需要它们可变的代码才能运行的代码(如果你只在构造函数之外设置它们,那么考虑尝试使它们成为构造函数的参数,而不是像属性那样可变。
有一些代码库不适用于gui / widget重代码和一些库(特别是可变集合),但我会说最合理的代码将允许超过50%的所有实例字段只读的。
此时你必须问自己,“为什么默认是可变的?”。 可变字段实际上是程序的复杂方面,因为即使在单个线程世界中,它们的交互也有更多不同行为的范围;因此,它们最好突出显示并引起编码人员的注意,而不是“赤身裸体”地留在世界的蹂躏中。
值得注意的是,大多数函数式语言都没有null的概念,或者使用起来非常困难,因为它们可以工作,而不是使用变量,但是使用名为 values 的函数,其值同时定义(范围)名称是。
我觉得很遗憾c#也没有用局部变量复制java的不变性概念。能够强调断言某些事情不会发生变化有助于明确表明某个值是在堆栈上还是在一个对象/结构中。
如果您有NDepend,那么您可以使用WARN IF Count > 0 IN SELECT FIELDS WHERE IsImmutable AND !IsInitOnly
答案 4 :(得分:2)
使用不可变数据结构,数据结构上的操作共享结构更加可行,从而使副本更便宜。 (Clojure这样做)
使用不可变数据结构的递归效果很好,因为无论如何你都会将数据结构传递给递归调用。一流的功能有助于分解细节。
答案 5 :(得分:1)
老实说,编写没有可变状态的程序很难。我尝试过的几次,我能想到的唯一方法是你要复制而不是变异。例如,您可以使用所需的值创建一个新的堆栈帧,而不是循环。
势在必行:
for(int num = 0; num < 10; num++) {
doStuff(num);
}
功能:
def loop(num) :
doStuff(num)
if(num < 10) :
loop(num + 1)
在这种情况下,您在每次迭代时复制num,并在复制它时更改其值。
对我而言,这是一种非常不舒服的体验。您可以在不使用任何高级语言的可变状态的情况下编写程序。功能语言带走了一个主要选择。也就是说,即使您不在并发环境中,在不改变编程风格的情况下使用不可变性也可以使程序更易于推理,因为您知道哪些内容可以修改,哪些不可修改。