函数式编程语言如何工作?

发布于 2024-08-30 15:31:09 字数 460 浏览 10 评论 0原文

如果函数式编程语言无法保存任何状态,那么它们如何执行简单的操作,例如读取用户的输入?它们如何“存储”输入(或存储与此相关的任何数据?)

例如:这个简单的 C 语言如何转换为像 Haskell 这样的函数式编程语言?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(我的问题受到这篇优秀帖子的启发:《名词王国中的执行》。阅读它让我更好地理解了面向对象编程到底是什么、Java 如何以一种极端的方式实现它,以及函数式编程语言的对比。)< /em>

If functional programming languages cannot save any state, how do they do simple stuff like reading input from a user? How do they "store" the input (or store any data for that matter?)

For example: how would this simple C thing translate to a functional programming language like Haskell?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(My question was inspired by this excellent post: "Execution in the Kingdom of Nouns". Reading it gave me some better understanding of what exactly object oriented programming is, how Java implements it in one extreme manner, and how functional programming languages are a contrast.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

泪是无色的血 2024-09-06 15:31:09

如果函数式编程语言无法保存任何状态,那么它们如何做一些简单的事情,例如读取用户的输入(我的意思是它们如何“存储”它),或存储与此相关的任何数据?

正如您所了解的,函数式编程没有状态,但这并不意味着它不能存储数据。不同之处在于,如果我按照以下方式编写(Haskell)语句,

let x = func value 3.14 20 "random"
in ...

我可以保证 x 的值在 ... 中始终相同:没有什么可以可能会改变它。同样,如果我有一个函数 f :: String -> Integer (接受字符串并返回整数的函数),我可以确定 f 不会修改其参数,或更改任何全局变量,或将数据写入文件,并且很快。正如 sepp2k 在上面的评论中所说,这种不可变性对于推理程序确实很有帮助:您编写折叠、旋转和破坏数据的函数,返回新副本,以便您可以将它们链接在一起,并且您可以确定没有这些函数调用可以做任何“有害”的事情。您知道 x 始终是 x,并且您不必担心有人在声明之间的某个位置编写了 x := foo bar x 及其使用,因为这是不可能的。

现在,如果我想读取用户的输入怎么办?正如 KennyTM 所说,这个想法是,不纯函数是一个将整个世界作为参数传递的纯函数,并返回其结果和世界。当然,您实际上并不想这样做:一方面,它非常笨重,另一方面,如果我重用同一个世界对象会发生什么?所以这会以某种方式被抽象化。 Haskell 使用 IO 类型来处理它:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

这告诉我们 main 是一个不返回任何内容的 IO 操作;执行这个动作就是运行 Haskell 程序的意思。规则是 IO 类型永远无法逃脱 IO 操作;在这种情况下,我们使用 do 引入该操作。因此,getLine 返回一个 IO String,可以通过两种方式来考虑它:首先,作为一个操作,在运行时生成一个字符串;第二,作为一个操作,在运行时生成一个字符串。其次,作为一个被 IO“污染”的字符串,因为它是不纯粹获取的。第一个更正确,但第二个可能更有帮助。 <-IO String 中取出 String 并将其存储在 str 中 - 但因为我们'当处于 IO 操作中时,我们必须将其重新包装起来,这样它就无法“逃逸”。下一行尝试读取一个整数 (reads) 并获取第一个成功的匹配项 (fst . head);这都是纯粹的(无 IO),因此我们用 let no = ... 为其命名。然后我们可以在 ... 中同时使用 nostr。因此,我们存储了不纯数据(从 getLinestr)和纯数据(let no = ...)。

这种使用 IO 的机制非常强大:它可以让您将程序的纯粹算法部分与不纯粹的用户交互部分分开,并在类型级别强制执行。您的 minimumSpanningTree 函数不可能更改代码中其他位置的内容,或者向用户写入消息等。很安全。

这是在 Haskell 中使用 IO 所需了解的全部内容;如果这就是你想要的,你可以停在这里。但如果您想了解为什么有效,请继续阅读。 (请注意,这些东西将特定于 Haskell — 其他语言可能会选择不同的实现。)

因此,这可能看起来有点像作弊,以某种方式为纯 Haskell 添加了杂质。但事实并非如此——事实证明,我们可以完全在纯 Haskell 中实现 IO 类型(只要我们给出 RealWorld)。这个想法是这样的:IO 操作 IO type 与函数 RealWorld -> 相同。 (type, RealWorld),它采用现实世界并返回 type 类型的对象和修改后的 RealWorld。然后我们定义几个函数,这样我们就可以使用这种类型而不会发疯:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

第一个函数允许我们讨论不执行任何操作的 IO 操作:return 3 是一个不执行任何操作的 IO 操作查询现实世界并仅返回3>>= 运算符,发音为“bind”,允许我们运行 IO 操作。它从 IO 操作中提取值,通过函数将其与现实世界一起传递,然后返回结果 IO 操作。请注意,>>= 强制执行我们的规则,即永远不允许 IO 操作的结果逃逸。

然后,我们可以将上面的 main 转换为以下一组普通的函数应用程序:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

Haskell 运行时以初始 RealWorld 跳转启动 main,并且我们准备好了!一切都很纯粹,只是有一个奇特的语法。

[编辑:正如@Conal指出的< /a>,这实际上并不是Haskell用来做IO的。如果你添加并发性,或者在 IO 操作中添加任何改变世界的方式,这个模型就会被破坏,所以 Haskell 不可能使用这个模型。它仅对于顺序计算是准确的。因此,Haskell 的 IO 可能有点躲闪;即使不是,它也肯定没有这么优雅。根据 @Conal 的观察,请参阅 Simon Peyton-Jones 在 解决尴尬的小队问题 [pdf],第 3.1 节;他沿着这些思路提出了可能相当于替代模型的东西,但随后由于其复杂性而放弃了它并采取了不同的策略

。如果这就是您想知道的一切,您可以停止阅读此处。如果您想要最后一剂理论,请继续阅读 - 但请记住,在这一点上,我们已经离您的问题非常远了!

最后一件事:事实证明,这种结构(带有 return>>= 的参数类型)非常通用;它称为 monad,do 表示法、return>>= 可以与其中任何一个一起使用。正如您在这里所看到的,单子并不神奇;它是由单子组成的。神奇之处在于 do 块会变成函数调用。 RealWorld 类型是我们唯一能看到魔法的地方。像 [] 这样的类型(列表构造函数)也是 monad,它们与不纯的代码无关。

你现在知道(几乎)关于单子概念的一切(除了一些必须满足的定律和正式的数学定义),但你缺乏直觉。网上有大量的 monad 教程;我喜欢这个,但是你有选择。但是, 这可能不会帮助你;获得直觉的唯一真正方法是结合使用它们并在正确的时间阅读一些教程。

但是,您不需要那种直觉来理解 IO。全面了解 monad 是锦上添花,但您现在就可以使用 IO。在我向您展示第一个 main 函数后,您就可以使用它了。您甚至可以将 IO 代码视为不纯的语言!但请记住,有一个潜在的功能表示:没有人作弊。

(PS:抱歉,篇幅太长了。我扯得有点远了。)

If functional programming languages cannot save any state, how do they do some simple stuff like reading input from a user (I mean how do they "store" it), or storing any data for that matter?

As you gathered, functional programming doesn't have state—but that doesn't mean it can't store data. The difference is that if I write a (Haskell) statement along the lines of

let x = func value 3.14 20 "random"
in ...

I am guaranteed that the value of x is always the same in the ...: nothing can possibly change it. Similarly, if I have a function f :: String -> Integer (a function taking a string and returning an integer), I can be sure that f will not modify its argument, or change any global variables, or write data to a file, and so on. As sepp2k said in a comment above, this non-mutability is really helpful for reasoning about programs: you write functions which fold, spindle, and mutilate your data, returning new copies so you can chain them together, and you can be sure that none of those function calls can do anything "harmful". You know that x is always x, and you don't have to worry that somebody wrote x := foo bar somewhere in between the declaration of x and its use, because that's impossible.

Now, what if I want to read input from a user? As KennyTM said, the idea is that an impure function is a pure function that's passed the entire world as an argument, and returns both its result and the world. Of course, you don't want to actually do this: for one thing, it's horribly clunky, and for another, what happens if I reuse the same world object? So this gets abstracted somehow. Haskell handles it with the IO type:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

This tells us that main is an IO action which returns nothing; executing this action is what it means to run a Haskell program. The rule is that IO types can never escape an IO action; in this context, we introduce that action using do. Thus, getLine returns an IO String, which can be thought of in two ways: first, as an action which, when run, produces a string; second, as a string that's "tainted" by IO since it was obtained impurely. The first is more correct, but the second can be more helpful. The <- takes the String out of the IO String and stores it in str—but since we're in an IO action, we'll have to wrap it back up, so it can't "escape". The next line attempts to read an integer (reads) and grabs the first successful match (fst . head); this is all pure (no IO), so we give it a name with let no = .... We can then use both no and str in the .... We've thus stored impure data (from getLine into str) and pure data (let no = ...).

This mechanism for working with IO is very powerful: it lets you separate the pure, algorithmic part of your program from the impure, user-interaction side, and enforce this at the type level. Your minimumSpanningTree function can't possibly change something somewhere else in your code, or write a message to your user, and so on. It's safe.

This is all you need to know to use IO in Haskell; if that's all you want, you can stop here. But if you want to understand why that works, keep reading. (And note that this stuff will be specific to Haskell—other languages may choose a different implementation.)

So this probably seemed like a bit of a cheat, somehow adding impurity to pure Haskell. But it isn't—it turns out that we can implement the IO type entirely within pure Haskell (as long as we're given the RealWorld). The idea is this: an IO action IO type is the same as a function RealWorld -> (type, RealWorld), which takes the real world and returns both an object of type type and the modified RealWorld. We then define a couple functions so we can use this type without going insane:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

The first one allows us to talk about IO actions which don't do anything: return 3 is an IO action which doesn't query the real world and just returns 3. The >>= operator, pronounced "bind", allow us to run IO actions. It extracts the value from the IO action, passes it and the real world through the function, and returns the resulting IO action. Note that >>= enforces our rule that the results of IO actions never be allowed to escape.

We can then turn the above main into the following ordinary set of function applications:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

The Haskell runtime jump-starts main with the initial RealWorld, and we're set! Everything's pure, it just has a fancy syntax.

[Edit: As @Conal points out, this is not actually what Haskell uses to do IO. This model breaks if you add concurrency, or indeed any way for the world to change in the middle of an IO action, so it would be impossible for Haskell to use this model. It is accurate only for sequential computation. Thus, it may be that Haskell's IO is a bit of a dodge; even if it isn't, it's certainly not quite this elegant. Per @Conal's observation, see what Simon Peyton-Jones says in Tackling the Awkward Squad [pdf], section 3.1; he presents what might amount to an alternative model along these lines, but then drops it for its complexity and takes a different tack.]

Again, this explains (pretty much) how IO, and mutability in general, works in Haskell; if this is all you want to know, you can stop reading here. If you want one last dose of theory, keep reading—but remember, at this point, we've gone really far afield from your question!

So the one last thing: it turns out this structure—a parametric type with return and >>=— is very general; it's called a monad, and the do notation, return, and >>= work with any of them. As you saw here, monads aren't magical; all that's magical is that do blocks turn into function calls. The RealWorld type is the only place we see any magic. Types like [], the list constructor, are also monads, and they have nothing to do with impure code.

You now know (almost) everything about the concept of a monad (except a few laws that must be satisfied and the formal mathematical definition), but you lack the intuition. There are a ridiculous number of monad tutorials online; I like this one, but you have options. However, this probably won't help you; the only real way to get the intuition is via a combination of using them and reading a couple tutorials at the right time.

However, you don't need that intuition to understand IO. Understanding monads in full generality is icing on the cake, but you can use IO right now. You could use it after I showed you the first main function. You can even treat IO code as though it was in an impure language! But remember that there's an underlying functional representation: nobody's cheating.

(PS: Sorry about the length. I went a little far afield.)

木落 2024-09-06 15:31:09

这里有很多很好的答案,但它们很长。我将尝试给出一个有用的简短答案:

  • 函数式语言将状态放在与 C 相同的位置:在命名变量中和在堆上分配的对象中。区别在于:

    • 在函数式语言中,“变量”在进入作用域时(通过函数调用或 let 绑定)获取其初始值,并且此后该值不会更改 。同样,在堆上分配的对象会立即使用其所有字段的值进行初始化,此后这些字段不会更改。

    • “状态更改”不是通过改变现有变量或对象来处理,而是通过绑定新变量或分配新对象来处理。

  • IO 的工作有一个技巧。生成字符串的副作用计算由一个函数描述,该函数将 World 作为参数,并返回包含字符串和新 World 的对。世界包括所有磁盘驱动器的内容、发送或接收的每个网络数据包的历史记录、屏幕上每个像素的颜色等等。这个技巧的关键是对世界的访问受到严格限制,以便

    • 没有程序可以复制世界(你会把它放在哪里?)

    • 没有程序可以抛弃世界

    使用这一技巧可以让一个独特的世界成为可能,其状态会随着时间的推移而变化。语言运行时系统不是用函数式语言编写的,它通过就地更新唯一的世界而不是返回新的世界来实现副作用计算。

    西蒙·佩顿·琼斯 (Simon Peyton Jones) 和菲尔·瓦德勒 (Phil Wadler) 在他们的里程碑式论文中对这一技巧进行了精彩的解释 “命令式函数式编程”

Plenty of good answers here, but they are long. I'm going to try to give a helpful short answer:

  • Functional languages put state in the same places that C does: in named variables and in objects allocated on the heap. The differences are that:

    • In a functional language, a "variable" gets its initial value when it comes into scope (through a function call or let-binding), and that value doesn't change afterward. Similarly, an object allocated on the heap is immediately initialized with the values of all its fields, which don't change thereafter.

    • "Changes of state" handled not by mutating existing variables or objects but by binding new variables or allocating new objects.

  • IO works by a trick. A side-effecting computation that produces a string is described by a function that takes a World as argument, and returns a pair containing the string and a new World. The World includes the contents of all disk drives, the history of every network packet ever sent or received, the color of each pixel on the screen, and stuff like that. The key to the trick is that access to the World is carefully restricted so that

    • No program can make a copy of the World (where would you put it?)

    • No program can throw away the World

    Using this trick makes it possible for there to be one, unique World whose state evolves over time. The language run-time system, which is not written in a functional language, implement a side-effecting computation by updating the unique World in place instead of returning a new one.

    This trick is beautifully explained by Simon Peyton Jones and Phil Wadler in their landmark paper "Imperative Functional Programming".

冧九 2024-09-06 15:31:09

我正在中断对新答案的评论回复,以提供更多空间:

我写道:

据我所知,这个 IO 故事 (World -> (a,World)) 在应用于 Haskell 时是一个神话,正如该模型所解释的那样仅纯粹的顺序计算,而 Haskell 的 IO 类型包括并发。通过“纯顺序”,我的意思是,除了由于该计算之外,甚至世界(宇宙)都不允许在命令式计算的开始和结束之间发生变化。例如,当您的计算机正在运转时,您的大脑等却无法运转。并发可以通过类似于 World ->; 的方式来处理。 PowerSet [(a,World)],允许不确定性和交错。

诺曼写道:

@Conal:我认为 IO 故事很好地概括了非确定性和交错;如果我没记错的话,“尴尬小队”论文中有一个很好的解释。但我不知道有一篇好的论文能够清楚地解释真正的并行性。

@Norman:在什么意义上概括?我建议通常给出的指称模型/解释,World -> (a,World) 与 Haskell IO 不匹配,因为它没有考虑不确定性和并发性。可能有一个更复杂的模型确实适合,例如 World -> PowerSet [(a,World)],但我不知道这样的模型是否已经制定出来并被充分证明&持续的。鉴于 IO 中填充了数千个 FFI 导入的命令式 API 调用,我个人怀疑能否找到这样的野兽。因此,IO 正在实现其目的:

开放问题:IO monad 已成为 Haskell 的 sin-bin。 (每当我们不理解某些东西时,我们就把它扔进 IO monad 中。)

(摘自 Simon PJ 的 POPL 演讲 穿着 Hair 衬衫穿着 Hair 衬衫:Haskell 回顾。)

解决尴尬小队,西蒙指出了什么对于 type IO a = World -> 不起作用(a, World),包括“当我们添加并发性时,该方法无法很好地扩展”。然后他提出了一种可能的替代模型,然后放弃了指称解释的尝试,说

但是,我们将采用基于过程计算语义的标准方法的操作语义。

这种未能找到精确的&有用的指称模型是我认为 Haskell IO 背离了我们所谓的“函数式编程”(或者 Peter Landin 更具体地称为“指称编程”)的精神和深层好处的根本原因。 在此处查看评论。

I'm breaking off a comment reply to a new answer, to give more space:

I wrote:

As far as I can tell, this IO story (World -> (a,World)) is a myth when applied to Haskell, as that model explains only purely sequential computation, while Haskell's IO type includes concurrency. By "purely sequential", I mean that not even the world (universe) is allowed to change between the start and end of an imperative computation, other than due to that computation. For instance, while your computer is chugging away, your brain etc cannot. Concurrency can be handled by something more like World -> PowerSet [(a,World)], which allows for nondeterminism and interleaving.

Norman wrote:

@Conal: I think the IO story generalizes pretty nicely to nondeterminism and interleaving; if I remember right, there's a pretty good explanation in the "Awkward Squad" paper. But I don't know of a good paper that explains true parallelism clearly.

@Norman: Generalizes in what sense? I'm suggesting that the denotational model/explanation usually given, World -> (a,World), doesn't match Haskell IO because it doesn't account for nondeterminism and concurrency. There might be a more complex model that does fit, such as World -> PowerSet [(a,World)], but I don't know whether such a model has been worked out and shown adequate & consistent. I personally doubt such a beast can be found, given that IO is populated by thousands of FFI-imported imperative API calls. And as such, IO is fulfilling its purpose:

Open problem: the IO monad has become Haskell’s sin- bin. (Whenever we don’t understand something, we toss it in the IO monad.)

(From Simon PJ's POPL talk Wearing the hair shirt Wearing the hair shirt: a retrospective on Haskell.)

In Section 3.1 of Tackling the Awkward Squad, Simon points what doesn't work about type IO a = World -> (a, World), including "The approach does not scale well when we add concurrency". He then suggests a possible alternative model, and then abandons the attempt at denotational explanations, saying

However we will instead adopt an operational semantics, based on standard approaches to the semantics of process calculi.

This failure to find a precise & useful denotational model is at the root of why I see Haskell IO as a departure from the spirit and the deep benefits of what we call "functional programming", or what Peter Landin more specifically named "denotative programming". See comments here.

乄_柒ぐ汐 2024-09-06 15:31:09

函数式编程源自 lambda 演算。如果您确实想了解函数式编程,请查看 http://worrydream.com/AlligatorEggs/

这是一个“有趣”的方式来学习 lambda 微积分,带您进入令人兴奋的函数式编程世界!

了解 Lambda 演算对函数式编程有何帮助。

因此,Lambda 演算是许多现实世界编程语言的基础,例如 Lisp、Scheme、ML、Haskell,......

假设我们想要描述一个将 3 与任何输入相加的函数,我们可以这样写:

plus3 x = succ(succ(succ x)) 

阅读“plus3 是一个函数,当应用于任何数字 x 时,会产生 x 的后继者的后继者的后继者”

请注意,该函数将 3 与任何数字相加数字不必命名为 plus3;名称“plus3”只是命名此函数的方便简写

(plus3 x) (succ 0) ≡ ((λ x.(succ (succ (succ x)))) (succ 0))

请注意,我们使用 lambda 符号表示函数(我认为它看起来有点像鳄鱼,我猜这就是鳄鱼蛋的想法的来源)

lambda 符号是 Alligator (一个函数) x 是它的颜色。您还可以将 x 视为一个参数(Lambda 演算函数实际上只假设有一个参数),其余的您可以将其视为函数的主体。

现在考虑抽象:

g ≡ λ f. (f (f (succ 0)))

参数 f 用于函数位置(在调用中)。
我们将 ga 称为高阶函数,因为它需要另一个函数作为输入。
您可以将其他函数调用 f 视为“eggs”。
现在,利用我们创建的两个函数或“鳄鱼”,我们可以执行以下操作:

(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

如果您注意到,您可以看到我们的 λ f 鳄鱼吃掉了 λ x 鳄鱼,然后 λ x 鳄鱼就死了。然后我们的 λ x 鳄鱼在 λ f 的鳄鱼蛋中重生。然后重复这个过程,左边的 λ x 鳄鱼现在吃掉右边的另一只 λ x 鳄鱼。

然后你就可以用这套简单的“鳄鱼”吃“鳄鱼””的规则来设计语法,函数式编程语言就这样诞生了!

因此,如果您了解 Lambda 演算,您就会了解函数式语言的工作原理。

Functional programing derives from lambda Calculus. If you truly want to understand Functional programing check out http://worrydream.com/AlligatorEggs/

It is a "fun" way to learn lambda Calculus and bring you into the exciting world of Functional programming!

How knowing Lambda Calculus is helpful in functional programming.

So Lambda Calculus is the foundation for many real-world programming languages such as Lisp, Scheme, ML, Haskell,....

Suppose we want to describe a function that adds three to any input to do so we would write:

plus3 x = succ(succ(succ x)) 

Read “plus3 is a function which, when applied to any number x, yields the successor of the successor of the successor of x”

Note that the function which adds 3 to any number need not be named plus3; the name “plus3” is just a convenient shorthand for naming this function

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

Notice we use the lambda symbol for a function (I think it looks kind of like an Alligator I'm guessing thats where the idea for Alligator eggs came from)

The lambda symbol is the Alligator (a function) and the x is its color. You can also think of x as an argument (Lambda Calculus functions are really only suppose to have one argument) the rest you can think of it as the body of the function.

Now consider the abstraction:

g ≡ λ f. (f (f (succ 0)))

The argument f is used in a function position (in a call).
We call g a higher-order function because it takes another function as an input.
You can think of the other function calls f as "eggs".
Now taking the two functions or "Alligators" we have created we can do something like this:

(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

If you notice you can see that our λ f Alligator eats our λ x Alligator and then the λ x Alligator and dies. Then our λ x Alligator is reborn in the λ f's Alligator eggs. Then the process repeats and the λ x Alligator on the left now eats the other λ x Alligator on the right.

Then you can use this simple set of rules of "Alligators" eating "Alligators" to design a grammar and thus Functional programming languages were born!

So you can see if you know Lambda Calculus you will understand how Functional Languages work.

骷髅 2024-09-06 15:31:09

Haskell 中处理状态的技术非常简单。而且你不需要了解 monad 来掌握它。

在具有状态的编程语言中,通常会在某处存储一些值,执行一些代码,然后存储一个新值。在命令式语言中,这种状态只是“在后台”的某个地方。在(纯)函数式语言中,您可以将其明确化,因此您可以显式地编写转换状态的函数。

因此,您不必编写 X 类型的某种状态,而是编写将 X 映射到 X 的函数。就是这样!您从考虑状态转变为考虑要对状态执行哪些操作。然后,您可以将这些函数链接在一起,并以各种方式将它们组合在一起以制作整个程序。当然,您不仅限于将 X 映射到 X。您可以编写函数以将各种数据组合作为输入,并在最后返回各种组合。

Monad 是帮助组织这一过程的众多工具之一。但单子实际上并不是问题的解决方案。解决方案是考虑状态转换而不是状态。

这也适用于 I/O。实际上发生的情况是这样的:您不是使用 scanf 的直接等效项从用户那里获取输入并将其存储在某个地方,而是编写一个函数来说明您将如何处理 < code>scanf(如果有),然后将该函数传递给 I/O API。这正是当您在 Haskell 中使用 IO monad 时 >>= 所做的事情。因此,您永远不需要在任何地方存储任何 I/O 的结果 - 您只需要编写代码来说明您希望如何转换它。

The technique for handling state in Haskell is very straightforward. And you don't need to understand monads to get a handle on it.

In a programming language with state, you typically have some value stored somewhere, some code executes, and then you have a new value stored. In imperative languages this state is just somewhere "in the background". In a (pure) functional language you make this explicit, so you explicitly write the function that transforms the state.

So instead of having some state of type X, you write functions that map X to X. That's it! You switch from thinking about state to thinking about what operations you want to perform on the state. You can then chain these functions together and combine them together in various ways to make entire programs. Of course you're not limited to just mapping X to X. You can write functions to take various combinations of data as input and return various combinations at the end.

Monads are one tool, among many, to help organise this. But monads aren't actually the solution to the problem. The solution is to think about state transformations instead of state.

This also works with I/O. In effect what happens is this: instead of getting input from the user with some direct equivalent of scanf, and storing it somewhere, you instead write a function to say what you'd do with the result of scanf if you had it, and then pass that function to the I/O API. That's exactly what >>= does when you use the IO monad in Haskell. So you never need to store the result of any I/O anywhere - you just need to write code that says how you'd like to transform it.

多像笑话 2024-09-06 15:31:09

(某些函数式语言允许不纯函数。)

对于纯函数语言,现实世界的交互通常作为函数参数之一包含在内,如下所示:

RealWorld pureScanf(RealWorld world, const char* format, ...);

不同的语言有不同的策略来将世界从现实世界中抽象出来。程序员。例如,Haskell 使用 monad 来隐藏 world 参数。


但函数式语言本身的纯粹部分已经是图灵完备的,这意味着在 C 中可行的任何事情在 Haskell 中也可行。与命令式语言的主要区别在于,不是就地修改状态:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

将修改部分合并到函数调用中,通常将循环转变为递归:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

(Some functional languages permit impure functions.)

For purely functional languages, the real world interaction is usually included as one of the function arguments, like this:

RealWorld pureScanf(RealWorld world, const char* format, ...);

Different languages have different strategies to abstract the world away from the programmer. Haskell, for instance, uses monads to hide the world argument.


But the pure part of functional language itself is already Turing complete, meaning anything doable in C is also doable in Haskell. The main difference to imperative language is instead of modifying states in place:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

You incorporate the modification part into a function call, usually turning loops into recursions:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}
生来就爱笑 2024-09-06 15:31:09

函数式语言可以保存状态!他们通常只是鼓励或强迫你明确这样做。

例如,查看 Haskell 的 State Monad

Functional language can save state! They usually just either encourage or force you to be explicit about doing so.

For example, check out Haskell's State Monad.

哽咽笑 2024-09-06 15:31:09

haskell:

main = do no <- readLn
          print (no + 1)

你当然可以用函数式语言将东西赋值给变量。你只是无法改变它们(所以基本上所有变量都是函数语言中的常量)。

haskell:

main = do no <- readLn
          print (no + 1)

You can of course assign things to variables in functional languages. You just can't change them (so basically all variables are constants in functional languages).

旧城烟雨 2024-09-06 15:31:09

如果函数式编程语言无法保存任何状态,那么它们如何执行简单的操作,例如读取用户的输入[以供以后使用]?

该语言可能不会,但它的实现肯定会!
想想其中的所有状态 - 至少一个堆栈、一个或多个堆、各种文件描述符、当前配置等等。值得庆幸的是,处理这一切的是计算机,而不是你。嗯 - 让计算机处理无聊的事情:多好的概念啊!

按照这个速度,实现现在任何一天都将承担所有沉闷的 I/O 活动 - 然后你将听到外延语言...是的,还有更多针对新手的行话!但现在,我们将重点关注已经存在的功能 - 函数式语言:它们如何执行简单的 I/O 操作,例如读取输入?

非常小心!

大多数函数式语言与命令式语言的不同之处在于,允许直接操作 I/O 状态 - 您不能在定义例如记录其使用次数。为了防止这种情况发生,通常使用类型来区分基于 I/O 的代码和无 I/O 的代码,例如 Haskell Clean 广泛使用该技术。

这可以很好地工作,甚至使函数式语言能够通过所谓的“外部函数接口”调用命令式语言中的子例程过程。这允许将真正无限的以 I/O 为中心的操作(以及随后对基于 I/O 的状态的操作)引入到函数式语言中 - scanf() 只是开始......


...等一下:“真正无限的以 I/O 为中心的操作”? 有限的实现不可能容纳所有这些,因此完全指示性语言将始终受到限制某种方式涉及其程序的外部交互。因此,I/O 必须始终是任何通用编程语言的一部分。

If functional programming languages cannot save any state, how do they do simple stuff like reading input from a user [for later use]?

The language might not, but its implementation certainly does!
Think of all the state in there - at least one stack, one or more heaps, various file descriptors, the current configuration, and so forth. Thankfully, it's the computer which is dealing with all that, instead of you. Hmm - letting the computer deal with the boring bits: what a concept!

At this rate, implementations will be taking on all that dreary I/O activity any day now - then you'll be hearing about denotative languages...yes, more jargon for the newbies! But for now, we'll focus on what already exists - functional languages: how do they do simple I/O stuff like e.g. reading input?

Very carefully!

What makes most functional languages different from imperative languages is that only the direct manipulation of state for I/O is allowed - you cannot define some extra state anonymously inside a definition to e.g. record the number of times it was used. To prevent this from happening, types are often used to distinguish between I/O-based and I/O-free code, with Haskell and Clean making extensive use of the technique.

This can work well enough to even give functional languages the ability to call subroutines procedures in imperative languages via a so-called "foreign function interface". This allow a veritable infinitude of I/O-centric operations (and the consequent manipulation of I/O-based state) to be introduced into the functional language - scanf() is just the beginning...


...wait a moment: "a veritable infinitude of I/O-centric operations"? A finite implementation cannot possibly hold all that, so a totally-denotative language will always be limited in some way with regards to the outside interactions of its programs. Therefore I/O must always be a part of any general-purpose programming language.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文