当有人说非纯函数打破了函数式语言的可组合性时,有人能给出一个解释它在实践中意味着什么的例子吗?
我想看一个可组合性的例子,然后看看同样的例子假设非纯函数以及未完成如何违反可组合性。
答案 0 :(得分:10)
过去可变状态困扰我的一些例子:
我编写了一个函数来从一堆文本中删除一些信息。它使用一个简单的正则表达式来找到混乱中的正确位置并获取一些字节。它停止工作,因为我的程序的另一部分在正则表达式库中打开了区分大小写;或打开"魔法"改变正则表达式解析方式的模式;或者当我写一个正则表达式匹配器的调用时,我忘记了其他任何一个旋钮。
这在纯语言中不是问题,因为正则表达式选项显示为匹配函数的显式参数。
我有两个线程想在我的语法树上做一些计算。我不顾一切地去做。由于这两个计算都涉及重写树中的指针,因此当我按照之前很好的指针但由于其他线程所做的更改而变得陈旧时,我最终会发生segfaulting。
这在纯语言中不是问题,其中树是不可变的;两个线程返回生活在堆的不同部分的树,两者都可以看到原始原始而不受另一个的干扰。
我个人没有这方面的经验,但我听说过其他程序员正在喋喋不休:基本上每个使用OpenGL的程序。管理OpenGL状态机是一场噩梦。如果你让州的任何一部分有点不对,那么每次通话都会犯一些愚蠢的错误。
很难说纯粹的设置会是什么样子,因为没有那么多广泛使用的纯图形库。对于3d方面,可以从Haskell-land看fieldtrip
,在第2侧,也许diagrams
。在每个场景中,场景描述都是构图,意思是人们可以轻松地将两个小场景组合成一个更大的场景,例如"将这个场景留在那个场景中#34;,"叠加这两个场景&#34 ;,"在那个"等之后显示这个场景,后端确保在渲染两个场景的调用之间挖掘底层图形库的状态。
上面描述的非纯场景中的常见线程是,无法查看一大块代码并弄清楚它在本地的作用。人们必须全面了解整个代码库,以确保他们理解代码块将会做什么。这是组合性的核心含义:可以组成一小块代码并理解它们的作用;当他们被安排到一个更大的程序中时,他们仍然会做同样的事情。
答案 1 :(得分:3)
我不认为你会去看看同样的例子假设非纯函数以及未完成如何违反可组合性"。副作用是可组合性问题的任何情况都是纯函数不会产生的。
但是这里有一个例子,说明人们说'非纯函数打破了可组合性'时的意思":
假设你有一个POS系统,就像这样(假装这是C ++或其他东西):
class Sale {
private:
double sub_total;
double tax;
double total;
string state; // "OK", "TX", "AZ"
public:
void calculateSalesTax() {
if (state == string("OK")) {
tax = sub_total * 0.07;
} else if (state == string("AZ")) {
tax = sub_total * 0.056;
} else if (state == string("TX")) {
tax = sub_total * 0.0625;
} // etc.
total = sub_total + tax;
}
void printReceipt() {
calculateSalesTax(); // Make sure total is correct
// Stuff
cout << "Sub-total: " << sub_total << endl;
cout << "Tax: " << tax << endl;
cout << "Total: " << total << endl;
}
现在您需要添加对Oregon的支持(无销售税)。只需添加块:
else if (state == string("OR")) {
tax = 0;
}
到calculateSalesTax
。但是假设有人决定“聪明”。并说
else if (state == string("OR")) {
return; // Nothing to do!
}
代替。现在total
已经不再计算了!由于calculateSalesTax
函数的输出都不清楚,程序员做了一个不会产生所有正确值的更改。
切换回Haskell,具有纯粹的功能,上述设计根本无法工作;相反,你必须说出类似
的内容calculateSalesTax :: String -> Double -> (Double, Double) -- (sales tax, total)
calculateSalesTax state sub_total = (tax, sub_total + tax) where
tax
| state == "OK" = sub_total * 0.07
| state == "AZ" = sub_total * 0.056
| state == "TX" = sub_total * 0.0625
-- etc.
printReceipt state sub_total = do
let (tax, total) = calculateSalesTax state sub_total
-- Do stuff
putStrLn $ "Sub-total: " ++ show sub_total
putStrLn $ "Tax: " ++ show tax
putStrLn $ "Total: " ++ show total
现在显而易见的是,必须通过添加一行
来添加俄勒冈州 | state == "OR" = 0
到tax
计算。由于函数的输入和输出都是显式的,因此可以防止错误。
答案 2 :(得分:1)
一方面是纯度可以实现懒惰评估和lazy evaluation enables some forms of composition you can't do in a strictly evaluated language。
例如,在Haskell中,您可以创建仅占用O(1)内存的map
和filter
的管道,并且您可以更自由地编写“控制流”功能,例如您自己的ifThenElse或者是Control.Monad上的东西。
答案 3 :(得分:1)
答案实际上非常简单:如果你有不纯的功能,那就是有副作用的功能,副作用会相互干扰。基本示例是在执行期间将某些内容存储在外部变量中的函数。使用相同变量的两个函数不会组合 - 只保留一个结果。这个例子看似微不足道,但是在一个复杂的系统中,当访问各种资源时,多个不纯的函数冲突可能很难追踪。
一个经典的例子是在多线程环境中保护可变(或其他独占)资源。访问资源的单个函数可以正常工作。但是在不同的线程中运行的两个这样的函数不会 - 他们不会撰写。
因此,我们为每个资源添加一个锁,并根据需要获取/释放锁以同步操作。但同样,这些功能并不构成。只运行单个锁的并行运行函数可以正常工作,但是如果我们开始将我们的函数组合成更复杂的函数并且每个线程可以获得多个锁,我们就可以获得deadlocks(一个线程获得Lock1,然后请求Lock2 ,而另一个获得Lock2,然后要求Lock1)。
因此我们要求所有线程以给定顺序获取锁以防止死锁。现在该框架是无死锁的,但不幸的是,由于其他原因,函数不能再编写:如果f1
需要Lock2
而f2
需要f1
的输出决定要采取哪种锁定,并f2
根据输入请求Lock1
,违反订单不变量,即使f1
和f2
分别满足它....
此问题的可组合解决方案是Software transactional memory或STM
。如果对共享可变状态的访问干扰另一个计算,则每个这样的计算在事务中执行并重新启动。在这里,严格要求计算是纯粹的 - 计算可以随时中断和重新启动,因此任何副作用都只能部分和/或多次执行。