副作用在命令性代码中是不可能避免的,但是它们会使对程序行为的推理变得非常困难。F#允许我们使用命令性副作用,但通常最好不要使用。我们如何在执行有效需求的同时避免副作用?
作为一个例子,让我们举一个非常简单和常见的有效例子:日志记录。假设我们正在编写一个函数,该函数根据直角三角形的另两条边的长度计算直角三角形斜边的长度:
设斜边a b=printfn";a侧:%g";a printfn";a printfn";b让c=sqrt<;|(a*a+b*b)printfn";侧c:%g";c。
每次调用此函数时,我们都会将输入和输出记录到控制台作为副作用。这在单线程应用程序中很好,但是如果从两个不同的线程同时调用tentenuse会发生什么呢?这就是问题所在。
命令式/面向对象世界中的一种常见方法是使用依赖项注入(或普通旧接口)将日志记录API与其实现分开。但是,生成的代码仍然会产生副作用。有没有可能定义一个有效的、没有任何副作用的斜边函数?这在术语上听起来几乎是自相矛盾的,但它是可以做到的,而且解决方案非常有趣。
类型Effect<;';Result>;=|字符串日志*(unit->;Effect<;';Result>;)|';Result的结果。
日志是我们用来将字符串写入记录器的工具。此构造函数接受一个附加的延续函数,该函数将在记录字符串之后执行。
请注意,此类型是自由单体的一个示例。LOG对应于Free构造函数,Result对应于Pure。自由单子在这里很有用,因为它可以将效果链接在一起。例如,我们可以重写我们的计算,如下所示:
设斜边a b=Log((Sprintf";Side a:%g";a),Fun()->;Log((Sprint";Side b:%g";b),Fun()->;let c=sqrt<;|(a*a+b*b)Log((Sprint";Side c:%g";c),Fun()->;结果c))。
了解此版本的函数返回的是效果浮点而不是浮点本身,这一点很重要。您可以将此类型视为执行时将返回浮点数的有效计算。然而,在它被执行之前,它什么也不做-特别是,它没有副作用。它只是定义了一个计算。
为了实际计算结果,我们需要一些额外的代码来处理我们的效果,就像异常处理程序处理异常一样(这也是一种效果)。让我们编写一个在执行计算时将日志消息累积到列表中的处理程序:
let Handle Effect=let rec循环log=Function|Log(str,cont)-&>;let log';=str::log循环log';(cont())|Result-&>result,log let result,log=loop[]Effect Result,log|&>list。雷夫。
当我们传递有效的计算进行处理时,我们会得到两件事:计算的最终结果,以及在计算期间写入的所有日志消息的列表:
请注意,句柄也是一个纯函数-它不会向控制台写入任何内容,也不会执行任何其他副作用。如果我们愿意,我们可以将生成的日志写入控制台,但是在这一点上我们必须小心考虑涉及的实际副作用。重要的是,我们已经成功地将纯函数计算从向控制台写入日志的不纯副作用中分离出来。通过分别解决这两个问题,我们已经使我们更容易理解我们的程序是如何运行的。
当然,没有人愿意像这样编写难看的嵌套Log调用,因为它们完全分散了计算本身的逻辑。幸运的是,我们知道免费的Monad可以通过提供工作流来帮助我们:
让rec绑定f=function|Log(str,cont)->;Log(str,Fun()->;cont()|>;bind f)|result->;f结果类型EffectBuilder()=Member__。return(Value)=结果值成员__。BIND(Effect,f)=Bind f Effect let Effect=EffectBuilder()
这是自由Monad的标准绑定实现:它只是沿着链向下传递绑定函数,直到结束,此时通过应用函数将两种效果绑定在一起。
让log str=Log(str,Fun()->;result())让logf fmt=Printf。ksprintf日志fmt。
再一次,这遵循了我们以前看到的与自由单子相同的模式。有了这些工具,我们可以更优雅地重写我们的计算:
让斜边a,b=效果{do!LOGF";A面:%g";A DO!logf";side b:%g";b设c=sqrt<;|(a*a+b*b)做!logf";c面:%g";c返回c}。
此版本的函数产生的效果<;浮动>;与前一个相同。它只是更容易理解,本质上并不比直接写到控制台的原始版本的斜边更复杂。
通过将联合用例添加到我们的效果类型中,我们可以很容易地支持其他效果。然而,这种系统中所有效果的主列表并不是很实用。理想情况下,我们希望将效果模块化,这样它们就可以组合在一起。例如,我们希望能够将日志效果与异常效果和状态效果分开处理。不幸的是,这在F#中还不是特别容易做到,但是有一个名为EFF的库可以作为概念验证。(不过,我不会在生产中使用它,因为操控者相当难看。)。
在未来,我预计代数效果将成为主流,并且对显式效果类型的支持将被烘焙到函数式和命令式语言中。虽然这还需要几年的时间,但至少现在你知道它(可能)会到来。