如何保证F#应用程序中的引用透明度?

时间:2016-08-19 15:31:23

标签: f# functional-programming side-effects referential-transparency

所以我试图学习FP,并试图了解参考透明度和副作用。

我已经了解到在类型系统中明确显示所有效果是保证参照透明度的唯一方法:

  

“大多数函数式编程”的概念是不可行的。这是不可能的   通过仅部分消除隐式副作用,编程语言更安全。留下一种效果通常足以模拟你刚试图去除的效果。另一方面,允许用纯语言“忘记”效果也会以自己的方式造成混乱。

     

不幸的是,没有黄金中间,我们面临着一个经典的二分法:被排除在中间的诅咒,它提出了(a)尝试使用纯度注释来驯服效果的选择,但完全接受这样的事实:你的代码仍然是根本有效的;或(b)通过在类型系统中明确表达所有效果并务实地充分接受纯洁 - Source

我还了解到像Scala或F#这样的非纯FP语言无法保证引用透明度:

  

强制执行引用透明性的能力与Scala的目标是完全不兼容,即拥有可与Java互操作的类/对象系统。 - Source

而在非纯FP中,程序员需要确保引用透明度:

  

在ML,Scala或F#之类的不纯语言中,程序员需要确保引用透明度,当然在动态类型语言(如Clojure或Scheme)中,没有静态类型系统来强制引用透明性。 - Source

我对F#感兴趣,因为我有.Net背景所以我的下一个问题是:

如果F#编译器没有强制执行F#应用程序,我该怎么做才能保证引用透明度?

3 个答案:

答案 0 :(得分:5)

这个问题的简短回答是,无法保证F#中的引用透明度。 F#的一大优势在于它与其他.NET语言有很好的互操作性,但与Haskell等更加孤立的语言相比,它的缺点是副作用存在,你将不得不处理它们。 / p>

你如何在F#中实际处理副作用完全是一个不同的问题。

实际上没有什么可以阻止你将效果带入F#中的类型系统,就像你在Haskell中一样,尽管你有效地“选择”这种方法而不是强制执行你。 / p>

你真正需要的是这样的基础设施:

/// 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是monadic绑定函数,可以让你对效果进行排序。

run运行IO以执行所有封闭的效果。这就像Haskell中的unsafePerformIO一样。

然后,您可以使用这些原始函数定义计算表达式构建器,并为自己提供很多很好的语法糖。

另一个有价值的问题是,这在F#中是否有用?

F#和Haskell之间的根本区别在于F#是默认语言,而Haskell默认是懒惰。 Haskell社区(我怀疑.NET社区,在较小程度上)已经了解到,当你结合懒惰评估和副作用/ IO时,可能会发生非常糟糕的事情。

当你在Haskell的IO monad中工作时,你(通常)保证关于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#中是否值得的原始问题,我还是要回答“我不知道”。我一直在a library处理F#中的参考透明效果,以便自己探索这种可能性。如果你感兴趣的话,那里有更多关于这个主题的材料以及IO更全面的实现。

最后,我认为值得记住的是,排除中间的诅咒可能比典型的开发人员更多地针对编程语言设计师。

如果您使用的是不纯洁的语言,您将需要找到一种方法来应对和驯服您的副作用,您遵循的准确策略是开放的解释和最适合您自己和/或者你的团队,但我认为F#为你提供了大量的工具来实现这一目标。

最后,我对F#的务实和经验丰富的观点告诉我,实际上,“大多数功能性”编程几乎在所有时间内仍然比竞争对手有了很大的改进。

答案 1 :(得分:5)

我认为你需要在适当的背景下阅读源文章 - 它是一个来自特定视角的意见文章,它故意具有挑衅性 - 但这并不是一件难事。

如果您使用F#,则可以通过编写良好代码来获得引用透明度。这意味着将大多数逻辑编写为一系列转换,并在运行转换之前执行读取数据的效果。运行效果将结果写在某处之后。 (并非所有程序都适合这种模式,但那些可以以参考透明的方式编写的程序通常都适用。)

根据我的经验,你可以在“中间”幸福地生活。这意味着,大多数时候在中编写引用透明代码,但出于某些实际原因需要时打破规则。

回应引号中的一些具体要点:

  

通过仅部分消除隐含的副作用,不可能使命令式编程语言更安全。

我同意不可能让它们“安全”(如果安全我们的意思是它们没有副作用),但你可以通过删除一些来使它们更安全 副作用。

  

留下一种效果通常足以模拟你刚试图去除的效果。

是的,但提供理论证明的模拟效果并不是程序员所做的。如果没有足够的劝阻来实现这种效果,你将倾向于以其他(更安全)的方式编写代码。

  

我还了解到像Scala或F#这样的非纯FP语言无法保证引用透明度:

是的,这是真的 - 但“参考透明度”并不是函数式编程的意义所在。对我来说,它是关于有更好的方法来建模我的领域和拥有引导我沿着“快乐道路”的工具(如类型系统)。参考透明度是其中的一部分,但它不是灵丹妙药。参考透明度不会神奇地解决你所有的问题。

答案 2 :(得分:0)

就像Mark Seemann在评论中所证实的那样“F#中的任何内容都不能保证参考透明度。这取决于程序员考虑这一点。”

我一直在线搜索,我发现“纪律是你最好的朋友”以及一些建议,试图尽可能高地保持F#应用程序中的引用透明度: / p>

  • 不要使用mutable,for或while循环,ref关键字等。
  • 坚持使用纯粹不可变的数据结构(区分联合,列表,元组,映射等)。
  • 如果您需要在某个时刻执行IO,请构建您的程序,使它们与纯功能代码分开。不要忘记函数式编程就是限制和隔离副作用。
  • 代数数据类型(ADT)AKA“有区别的联合”而不是对象。
  • 学会爱懒惰。
  • 拥抱Monad。