这个问题的简短回答是,在 F# 中无法保证引用透明性。F# 的一个巨大优势是与其他 .NET 语言有很好的互操作性,但相比较于像 Haskell 这样更隔离的语言,副作用是存在的,你必须处理它们。
如何在 F# 中实际处理副作用是完全不同的问题。
实际上,你可以以非常类似于 Haskell 的方式将效果带入 F# 的类型系统中,尽管事实上你是“选择”这种方法而不是被强制执行它。
你真正需要的只是一些基础设施,就像这样:
/// 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#是急切的语言,而Haskell是默认惰性的。 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,您可以获得一些控制,防止重复评估,但仍无法控制效果发生的时间和顺序。
相比之下,当您使用急切地评估数据结构时,其后果通常不会那么难以捉摸,因此我认为与Haskell相比,F#中显式效果的要求大大降低了。
尽管如此,在类型系统中使所有效果显式的一个很大的优点是它有助于强制执行良好的设计。像Mark Seemann之类的人会告诉您,设计健壮的系统(无论是面向对象还是函数式)的最佳策略涉及将副作用隔离在系统边缘并依赖于参照透明度高、高度单元测试的核心。
如果您正在使用显式效果和IO类型系统,并且所有函数最终都要编写成IO,则这是一个强烈而明显的设计味道。
回到最初的问题,即在F#中是否值得这样做,我仍然必须回答“我不知道”。我一直在开发用于参照透明效果的库,以探索这个可能性。如果您感兴趣,可以在那里找到更多有关此主题以及更完整的IO实现的材料。
最后,我认为值得记住的是排除中间诅咒可能更针对编程语言设计人员而不是普通开发人员。
如果您正在使用不纯的语言,则需要找到一种应对和驯服副作用的方法,具体策略取决于您自己和/或团队的需要,但我认为F#为此提供了很多工具。
最后,我的实用和经验丰富的观点告诉我,实际上,“大多数功能”编程仍然比其竞争对手在绝大多数情况下都要好很多。