所以我正在努力学习FP,我正在努力了解参考透明度和副作用。
我已经知道,在类型系统中使所有的效果显式是保证引用透明性的唯一方法:
"主要是函数式编程"的想法是不可行的。使命令式是不可能的编程语言仅通过部分消除隐式副作用而更安全。留下一种效果往往足以模拟你刚刚试图消除的效果。另一方面,在纯语言中允许效果被"遗忘"也会以自己的方式造成混乱。
不幸的是,没有黄金中间,我们面临着一个经典的二分法:被排除的中间的诅咒,它提出了以下两种选择:(a)试图使用纯度注释来驯服效果,但完全接受你的代码从根本上仍然有效的事实;或者(b)通过在类型系统中显式地显示所有效果并保持实用来完全拥抱纯粹性——来源
我还了解到,像Scala或f#这样的非纯FP语言不能保证引用透明性:
强制引用透明的能力与Scala的目标——拥有一个与Java可互操作的类/对象系统——非常不兼容。——源
并且在非纯FP中,由程序员来确保引用透明性:
在像ML, Scala或f#这样的非纯语言中,确保引用透明性取决于程序员,当然在像Clojure或Scheme这样的动态类型语言中,没有静态类型系统来强制引用透明性。——源
我对f#很感兴趣,因为我有。net背景,所以我的下一个问题是:
如果f#编译器没有强制执行,我可以做些什么来保证f#应用程序中的引用透明度?
对这个问题的简短回答是,在f#中没有办法保证引用的透明性。f#最大的优点之一是它可以与其他。net语言进行非常棒的互操作,但与Haskell这样更加孤立的语言相比,它的缺点是有副作用,你必须处理它们。
如何处理f#中的副作用是完全不同的问题。
实际上没有什么可以阻止你在f#中像在Haskell中一样将效果引入类型系统,尽管实际上你是"选择"了这种方法,而不是强制执行。
你真正需要的是一些像这样的基础设施:
/// A value of type IO<'a> represents an action which, when performed (e.g. by calling the IO.run function), does some I/O which results in a value of type 'a.
type IO<'a> =
private
|Return of 'a
|Delay of (unit -> 'a)
/// Pure IO Functions
module IO =
/// Runs the IO actions and evaluates the result
let run io =
match io with
|Return a -> a
|Delay (a) -> a()
/// Return a value as an IO action
let return' x = Return x
/// Creates an IO action from an effectful computation, this simply takes a side effecting function and brings it into IO
let fromEffectful f = Delay (f)
/// Monadic bind for IO action, this is used to combine and sequence IO actions
let bind x f =
match x with
|Return a -> f a
|Delay (g) -> Delay (fun _ -> run << f <| g())
return
的值在IO
内。
fromEffectful
吸收了unit -> 'a
的副作用函数,并将其纳入IO
。
bind
是一元绑定函数,允许你排序效果。
run
运行IO来执行所有封闭的效果。这就像Haskell中的unsafePerformIO
。
您可以使用这些基本函数定义计算表达式构建器,并为自己提供许多不错的语法糖。
另一个值得问的问题是,这在f#中有用吗?
f#和Haskell之间的根本区别是f#是默认的eager语言,而Haskell是默认的lazy语言。Haskell社区(我怀疑。net社区,在较小程度上)已经了解到,当您将惰性求值和副作用/IO结合在一起时,可能会发生非常糟糕的事情。
当你在Haskell中处理IO单子时,你(通常)是在保证IO的顺序性,并确保一个IO在另一个IO之前完成。你还保证了效果发生的频率和时间。
我喜欢在f#中摆姿势的一个例子是:
let randomSeq = Seq.init 4 (fun _ -> rnd.Next())
let sortedSeq = Seq.sort randomSeq
printfn "Sorted: %A" sortedSeq
printfn "Random: %A" randomSeq
乍一看,这段代码似乎是生成一个序列,对同一个序列排序,然后输出已排序和未排序的版本。
它不。它生成两个序列,其中一个是有序的,另一个不是有序的。它们可以,而且几乎可以肯定,具有完全不同的值。
这是结合了副作用和没有引用透明性的惰性求值的直接后果。您可以通过使用Seq.cache
重新获得一些控制权,它可以防止重复计算,但仍然无法控制何时以及以何种顺序发生效果。
相比之下,当你使用急切求值的数据结构时,结果通常不那么阴险,所以我认为f#中显式效果的要求与Haskell相比大大减少了。
也就是说,在类型系统中显式显示所有效果的一个很大的好处是,它有助于执行良好的设计。Mark Seemann之类的人会告诉你,设计健壮系统的最佳策略,无论是面向对象的还是功能性的,都包括在系统边缘隔离副作用,并依赖于引用透明、高度单元可测试的核心。
如果你在类型系统中使用显式效果和IO
,而你所有的函数最终都是用IO
编写的,这是一个强烈而明显的设计气味。
IO
的更完整的实现。
最后,我认为值得记住的是,排除中间的诅咒可能针对的是编程语言设计师,而不是典型的开发人员。
如果你使用的是一种不纯的语言,你需要找到一种方法来处理和控制你的副作用,你所遵循的精确策略是可以解释的,什么最适合你自己和/或你的团队的需要,但我认为f#给了你很多工具来做到这一点。
最后,我对f#的务实和经验告诉我,实际上,"大多数函数式"编程在几乎所有时候都比它的竞争对手有很大的改进。
我认为您需要在适当的上下文中阅读源文章-这是一篇来自特定角度的观点文章,它是故意挑衅的-但它不是一个确凿的事实。
如果你正在使用f#,你将通过编写好的代码获得引用透明性。这意味着将大多数逻辑写入转换序列,并在运行转换之前执行读取数据的效果。运行效果,然后在某处写入结果。(并不是所有的程序都适合这种模式,但是那些可以以引用透明的方式编写的通常可以。)
根据我的经验,你可以非常快乐地生活在"中间"。这意味着,在大多数情况下,编写引用透明的代码,但在出于某些实际原因需要时打破规则。
对引号中的一些特定点作出回应:
仅通过部分去除隐式副作用来使命令式编程语言更安全是不可能的。
我同意让它们"安全"是不可能的(如果安全意味着它们没有副作用),但是你可以通过消除一些副作用来使它们更安全。
留下一种效果往往足以模拟你刚刚试图删除的效果。
是的,但是模拟效果来提供理论证明并不是程序员所做的。如果完全不鼓励实现这种效果,您将倾向于以其他(更安全的)方式编写代码。
我还了解到,像Scala或f#这样的非纯FP语言不能保证引用透明性:
是的,这是真的——但是"引用透明性"不是函数式编程的目的。对我来说,它是关于有更好的方法来为我的领域建模,并有工具(如类型系统)来引导我沿着"快乐的道路"前进。参考透明度是其中的一部分,但它不是灵丹妙药。参考透明度不会神奇地解决你所有的问题。
就像Mark Seemann在评论中确认的那样" f#中没有任何东西可以保证引用的透明性。这要由程序员来考虑。"
我一直在网上做一些搜索,我发现"纪律是你最好的朋友"和一些建议,试图保持引用透明度的水平在你的f#应用程序尽可能高:
- 不要使用可变的,for或while循环,ref关键字等 坚持使用纯不可变数据结构(区分联合、列表、元组、映射等)。
- 如果您需要在某些时候做IO,那么构建您的程序,使它们与您的纯功能代码分开。不要忘记函数式编程是关于限制和隔离副作用。
- 代数数据类型(ADT)又名"区分联合"而不是对象。
- 学会爱上懒惰
- 拥抱单子