有人能举个例子解释在实践中人们说非纯函数破坏了函数式语言组合性是什么意思吗?
我想看一个组合性的例子,然后看看同样的例子假设非纯函数,以及非纯度如何破坏了组合性。
有人能举个例子解释在实践中人们说非纯函数破坏了函数式语言组合性是什么意思吗?
我想看一个组合性的例子,然后看看同样的例子假设非纯函数,以及非纯度如何破坏了组合性。
以下是我曾经遇到可变状态问题的一些例子:
我编写了一个函数来从一堆文本中爬取一些信息。它使用简单的正则表达式在混乱的文本中找到正确的位置并抓取一些字节。它停止工作了,因为我的程序中的另一个部分打开了正则表达式库中的大小写敏感模式;或者打开了改变正则表达式解析方式的“魔法”模式;或者其他十几个我在调用正则表达式匹配器时忘记可用的选项。
这在纯语言中不是问题,因为正则表达式选项会出现作为匹配函数的明确参数。
我有两个线程想对我的语法树进行一些计算。我毫不考虑地去做了。由于两个计算都涉及重写树中的指针,因此当我跟随之前良好但由于其他线程所做的更改而过时的指针时,我最终会崩溃。
在纯语言中,这不是问题,因为树是不可变的;两个线程返回的树存在于堆的不同部分,并且两者都可以看到原始的未被干扰的树。
我自己没有遇到这个问题,但我听过其他程序员的抱怨:基本上每个使用OpenGL的程序都有这个问题。管理OpenGL状态机是一场噩梦。如果你稍微弄错了任何部分的状态,每个调用都会做一些愚蠢的事情。
很难说在纯设置中会是什么样子,因为并没有那么多广泛使用的纯图形库。对于3D方面,可以看看来自Haskell领域的fieldtrip
,而在2D方面,也许可以看看diagrams
。在每种情况下,场景描述是组合的,意味着可以使用像“将这个场景放在那个场景左边”、“将这两个场景叠加”、“在那个场景之后显示这个场景”等组合器轻松地将两个小场景组合成一个更大的场景,而后端代码会确保在渲染这两个场景的调用之间修改底层图形库的状态。
以上非纯净场景的共同点是,无法从一个代码块中看出它的本地作用。必须全局理解整个代码库才能确信了解这个代码块将要执行的操作。这就是组合性的核心含义:可以组合小的代码块并理解它们的作用;当它们被嵌入到较大的程序中时,它们仍然会执行相同的操作。
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;
}
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
计算税收时,通过函数的输入和输出全部明确,成功避免了Bug。
return
。那么编写像示例一样的错误就更加困难了。 - Luis Casillasreturn
是问题,我认为这仍然回答了问题。return
是一个不纯的函数,一种效果。 - luquif1
获取Lock2
并且f2
需要f1
的输出来决定获取哪个锁,并且f2
根据输入请求Lock1
,则违反了顺序不变性,即使f1
和f2
分别满足它....
解决这个问题的可组合解决方案是软件事务内存或STM
。每个这样的计算都在一个事务中执行,并在其访问共享可变状态与另一个计算干扰时重新启动。这里严格要求计算是纯的 - 计算可以被中断和重新启动,在任何时候都可能会执行它们的副作用部分和/或多次执行。
一个方面是纯度使得惰性求值成为可能,惰性求值使得在严格求值的语言中无法实现某些形式的组合。
例如,在Haskell中,你可以创建只消耗O(1)内存的map
和filter
的管道,并且你有更多自由编写"控制流"函数,比如你自己的ifThenElse或Control.Monad中的内容。