当函数式编程看起来不可避免时,如何避免状态?(How does functional programming avoid state when it seems unavoidable?)

系统教程 行业动态 更新时间:2024-06-14 17:01:31
当函数式编程看起来不可避免时,如何避免状态?(How does functional programming avoid state when it seems unavoidable?)

假设我们定义了一个函数c sum(a, b) ,函数式编程风格,它返回它的参数总和。 到现在为止还挺好; 所有FP的好东西没有任何问题。

现在让我们假设我们在一个具有动态类型和单态,有状态错误流的环境中运行它。 然后让我们假设我们传递了a和/或b的值, a和不是用来处理的(即不是数字),并且它需要以某种方式指示错误。

但是如何? 这个功能应该是纯粹的,无副作用的。 它如何在全球错误流中插入错误而不违反该错误?

Let's say we define a function c sum(a, b), functional programming -style, that returns the sum of its arguments. So far so good; all the nice things of FP without any problems.

Now let's say we run this in an environment with dynamic typing and a singleton, stateful error stream. Then let's say we pass a value of a and/or b that sum isn't designed to handle (i.e. not numbers), and it needs to indicate an error somehow.

But how? This function is supposed to be pure and side-effect-less. How does it insert an error into the global error stream without violating that?

最满意答案

我所知道的任何编程语言都没有内置任何类似“单态有状态错误流”的东西,因此您必须创建一个。 如果你试图用纯粹的功能风格来编写你的程序,你根本就不会做出这样的事情。

但是,您可以使用求和函数来返回总和或错误指示。 实际上这种类型实际上通常以名称Either而为人所知。 然后,您可以轻松地创建一个调用可能返回错误的大量计算的函数,并返回其他计算中遇到的所有错误的列表。 这与你所谈论的非常接近; 它只是显式返回而不是全局。

请记住,当你编写一个功能程序时,问题是“我如何制作一个具有我想要的行为的程序?” 不是,“我将如何复制在另一种编程风格中采用的一种特定方法?”。 “全局状态错误流”是一种手段而非结束 。 您不能在纯函数样式中使用全局状态错误流,否。 但问问自己,你使用全局有状态错误流来实现什么 ; 不管它是什么,你都可以在函数式编程中实现这一点 ,而不是使用相同的机制。

询问纯函数式编程是否可以实现一种依赖于副作用的特定技术,就像询问你如何在面向对象编程中使用汇编技术。 面向对象提供不同的工具供您用来解决问题; 限制自己使用这些工具来模拟不同的工具集并不是一种有效的方法。


回应评论:如果你想用你的错误流实现的是将错误消息记录到终端,那么是的,在某个级别上,代码将不得不做IO。 1

打印到终端就像任何其他的IO一样,没有什么特别的特别之处,它使得它值得挑选出来,作为一种情况,国家似乎特别不可避免。 因此,如果这会将您的问题变成“纯粹的功能性程序如何处理IO?”,那么无疑有许多重复的问题,更不用说很多博客文章和教程正是针对这个问题。 对于纯编程语言的实现者和使用者来说,这不是一个突然的惊喜,这个问题已经存在了数十年了,并且已经有一些非常复杂的想法被应用于答案中。

不同语言采用不同的方法(Haskell中的IO monad,Mercury中的独特模式,Haskell历史版本中请求和响应的惰性流等等)。 其基本思想是提出一个可由纯代码操作的模型,并将模型的操作与语言实现中的实际不纯操作联系起来。 这可以让你保持纯度的好处(适用于纯代码的证据,但不适用于普通的不纯代码仍然适用于使用纯IO模型的代码)。

纯粹的模型必须经过精心的设计,以至于实际上无法做任何事情,这在实际的IO方面是没有意义的。 例如,水星通过编写程序完成IO, 就好像你正在通过宇宙的当前状态一样,作为一个额外的参数。 这个纯粹的模型准确地表示了依赖于并影响程序之外的宇宙的操作的行为,但只有当系统中的任何一个时刻都有一个宇宙状态时,它才会贯穿整个程序,从头到尾。 所以有一些限制

io类型是抽象的,因此无法构造该类型的值; 唯一的办法就是从你的调用者那里得到一个。 一个io值通过语言实现传递到main谓词中,从而将整个事物踢掉。 传入main的io值的模式被声明为唯一。 这意味着你不能做可能导致它被复制的事情,比如将它放在一个容器中,或者将相同的io值传递给多个不同的调用。 独特的模式可以确保你只能将io值分配给一个也使用独特模式的谓词,并且一旦你的值通过了“dead”并且不能在其他地方传递,它就会立即通过。

1请注意,即使在命令式程序中,如果您的错误日志记录系统返回错误消息流,然后才实际决定将它们打印在程序最外层附近,您将获得很大的灵活性。 如果你的日志调用是直接写输出的话,这里只是我能想到的一些事情,在我看来,这样的系统变得更加困难:

推测性地执行计算,并通过检查它是否发出任何错误来查看它是否失败 将多个高级系统组合为一个系统,在日志中添加标签以区分每个系统 仅当出现错误消息时才发出调试消息和信息日志消息(因此,当没有错误需要调试时输出是干净的,并且在有详细信息时会有丰富的细节)

No programming language that I know of has anything like a "singleton stateful error stream" built in, so you'd have to make one. And you simply wouldn't make such a thing if you were trying to write your program in a pure functional style.

You could, however, have a sum function that returns either the sum or an indication of an error. The type used to do this is in fact often known by the name Either. Then you could easily make a function that invokes a whole bunch of computations that could possibly return an error, and returns a list of all the errors that were encountered in the other computations. That's pretty close to what you were talking about; it's just explicitly returned rather than being global.

Remember, the question when you're writing a functional program is "how do I make a program that has the behavior I want?" not, "how would I duplicate one particular approach taken in another programming style?". A "global stateful error stream" is a means not an end. You can't have a global stateful error stream in pure function style, no. But ask yourself what you're using the global stateful error stream to achieve; whatever it is, you can achieve that in functional programming, just not with the same mechanism.

Asking whether pure functional programming can implement a particular technique that depends on side effects is like asking how you use techniques from assembly in object-oriented programming. OO provides different tools for you to use to solve problems; limiting yourself to using those tools to emulate a different toolset is not going to be an effective way to work with them.


In response to comments: If what you want to achieve with your error stream is logging error messages to a terminal, then yes, at some level the code is going to have to do IO to do that.1

Printing to terminal is just like any other IO, there's nothing particularly special about it that makes it worthy of singling out as a case where state seems especially unavoidable. So if this turns your question into "How do pure functional programs handle IO?", then there are no doubt many duplicate questions on SO, not to mention many many blog posts and tutorials speaking precisely to that issue. It's not like it's a sudden surprise to implementors and users of pure programming languages, the question has been around for decades, and there have been some quite sophisticated thought put into the answers.

There are different approaches taken in different languages (IO monad in Haskell, unique modes in Mercury, lazy streams of requests and responses in historical versions of Haskell, and more). The basic idea is to come up with a model which can be manipulated by pure code, and hook up manipulations of the model to actual impure operations within the language implementation. This allows you to keep the benefits of purity (the proofs that apply to pure code but not to general impure code will still apply to code using the pure IO model).

The pure model has to be carefully designed so that you can't actually do anything with it that doesn't make sense in terms of actual IO. For example, Mercury does IO by having you write programs as if you're passing around the current state of the universe as an extra parameter. This pure model accurately represents the behaviour of operations that depend on and affect the universe outside the program, but only when there is exactly one state of the universe in the system at any one time, which is threaded through the entire program from start to finish. So some restrictions are put in

The type io is made abstract so that there's no way to construct a value of that type; the only way you can get one is to be passed one from your caller. An io value is passed into the main predicate by the language implementation to kick the whole thing off. The mode of the io value passed in to main is declared such that it is unique. This means you can't do things that might cause it to be duplicated, such as putting it in a container or passing the same io value to multiple different invocations. The unique mode ensures that you can only ass the io value to a predicate that also uses the unique mode, and as soon as you pass it once the value is "dead" and can't be passed anywhere else.

1 Note that even in imperative programs, you gain a lot of flexibility if you have your error logging system return a stream of error messages and then only actually make the decision to print them close to the outermost layer of the program. If your log calls are directly writing the output immediately, here's just a few things I can think of off the top of my head that become much harder to do with such a system:

Speculatively execute a computation and see whether it failed by checking whether it emitted any errors Combine multiple high level systems into a single system, adding tags to the logs to distinguish each system Emit debug and info log messages only if there is also an error message (so the output is clean when there are no errors to debug, and rich in detail when there are)

更多推荐

本文发布于:2023-04-20 16:13:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/dzcp/218f008bbf9f66c4b9b3b72b2069376a.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:不可避免   函数   状态   functional   unavoidable

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!