问题 非纯函数打破可组合性意味着什么?


当有人说非纯函数打破了函数式语言的可组合性时,有人能给出一个解释它在实践中意味着什么的例子吗?

我想看一个可组合性的例子,然后看到相同的例子假设非纯函数以及未完成如何违反可组合性。


1512
2018-04-16 18:46


起源

这是我最喜欢的关于纯度和懒惰如何实现可组合性的帖子之一: apfelmus.nfshost.com/articles/quicksearch.html - luqui


答案:


一些可变状态过去困扰我的例子:

  • 我编写了一个函数来从一堆文本中删除一些信息。它使用一个简单的正则表达式在混乱中找到正确的位置并获取一些字节。它停止工作,因为我的程序的另一部分在正则表达式库中打开了区分大小写;或打开改变正则表达式解析方式的“魔法”模式;或者当我写一个正则表达式匹配器的调用时,我忘记了其他任何一个旋钮。

    这在纯语言中不是问题,因为正则表达式选项显示为匹配函数的显式参数。

  • 我有两个线程想要在我的语法树上进行一些计算。我不顾一切地去做。由于两个计算都涉及重写树中的指针,因此当我遵循之前很好但由于其他线程所做的更改而过时的指针时,我最终会发生segfaulting。

    这在纯语言中不是问题,其中树是不可变的;两个线程返回生活在堆的不同部分的树,并且两者都可以看到原始原始而不受另一个的干扰。

  • 我本人并没有这方面的经验,但我听到其他程序员正在喋喋不休:基本上每个程序都使用OpenGL。管理OpenGL状态机是一场噩梦。如果你让州的任何一部分有点不对,那么每次通话都会犯一些愚蠢的错误。

    很难说纯粹的设置会是什么样子,因为没有那么多广泛使用的纯图形库。对于3D方面,人们可以看一下 fieldtrip也许在第二方面 diagrams,都来自Haskell-land。在每个场景中,场景描述都是组合的,人们可以轻松地将两个小场景组合成一个较大的场景,例如“将这个场景留在那个场景中”,“叠加这两个场景”,“在场景之后显示这个场景”等等,后端确保在渲染两个场景的调用之间挖掘底层图形库的状态。

上面描述的非纯场景中的共同线程是,人们无法查看一大块代码并弄清楚它的作用 本地。必须全面了解整个代码库,以确保他们了解代码块的作用。这是组合性的核心含义:可以组成一小块代码并理解它们的作用;当他们进入一个更大的计划时,他们会 仍然做同样的事情


10
2018-04-17 00:03





我不认为你会“看到同样的例子假设非纯函数以及未完成如何违反可组合性”。副作用是可组合性问题的任何情况都是纯函数不会出现的情况。

但这里有一个例子,当人们说“非纯函数破坏可组合性”时,人们的意思是:

假设你有一个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 计算。由于函数的输入和输出都是显式的,因此防止了错误。


3
2018-04-16 19:17



目前尚不清楚这个例子中的问题是变异,而不是早期的回归 - 一个长期受到批评的结构,因为程序性命令式编程的早期阶段。作为魔鬼的倡导者,请考虑一种混合的功能/命令式语言,如Schema或ML,其中对变异没有限制,但语言不提供早期 return 来自一个功能。然后编写像示例这样的bug要困难得多。 - Luis Casillas
即使 return 是问题,我认为这仍然是问题的答案。 return 是一种不纯的功能,一种效果。 - luqui


一个方面是纯度使懒惰评估和 延迟评估可以使用严格评估的语言无法完成某些形式的合成

例如,在Haskell中,您可以创建管道 map 和 filter 只花O(1)内存,你有更多的自由来编写“控制流”功能,如你自己的ifThenElse或Control.Monad上的东西。


1
2018-04-16 18:51





答案其实很简单:如果你有不纯的功能,那就是有副作用的功能,副作用会相互干扰。一个基本的例子是在执行期间将某些东西存储在外部变量中的函数。使用相同变量的两个函数不会合成 - 只保留一个结果。这个例子看似微不足道,但是在一个复杂的系统中,当访问各种资源时,多个不纯的函数冲突可能很难跟踪。

一个经典的例子是在多线程环境中保护可变(或其他独占)资源。访问资源的单个函数可以正常工作。但是在不同线程中运行的两个这样的函数却没有 - 它们不构成。

因此,我们为每个资源添加一个锁,并根据需要获取/释放锁以同步操作。但同样,这些功能并不构成。运行仅并行锁定单个锁的函数可以正常工作,但是如果我们开始将我们的函数组合成更复杂的函数并且每个线程可以获得多个锁,我们就可以得到 死锁 (一个线程获得Lock1,然后请求Lock2,而另一个获取Lock2,然后请求Lock1)。

因此,我们要求所有线程以给定顺序获取锁以防止死锁。现在框架是无死锁的,但不幸的是,函数不会因为不同的原因而构成:如果 f1 需要 Lock2 和 f2 需要输出 f1 决定采取哪种锁,以及 f2 要求 Lock1 根据输入,即使违反了顺序不变量 f1 和 f2 分别满足它....

这个问题的可组合解决方案是 软件事务内存 要不就 STM。如果对共享可变状态的访问干扰另一个计算,则每个这样的计算在事务中执行并重新启动。就在这里 严格要求计算是纯粹的  - 计算可以随时中断和重新启动,因此任何副作用都只能部分和/或多次执行。


0
2018-05-10 20:24