函数式编程语言如何工作?
如果函数式编程语言无法保存任何状态,那么它们如何执行简单的操作,例如读取用户的输入?它们如何“存储”输入(或存储与此相关的任何数据?)
例如:这个简单的 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
正如您所了解的,函数式编程没有状态,但这并不意味着它不能存储数据。不同之处在于,如果我按照以下方式编写(Haskell)语句,
我可以保证
x
的值在...
中始终相同:没有什么可以可能会改变它。同样,如果我有一个函数f :: String -> Integer
(接受字符串并返回整数的函数),我可以确定f
不会修改其参数,或更改任何全局变量,或将数据写入文件,并且很快。正如 sepp2k 在上面的评论中所说,这种不可变性对于推理程序确实很有帮助:您编写折叠、旋转和破坏数据的函数,返回新副本,以便您可以将它们链接在一起,并且您可以确定没有这些函数调用可以做任何“有害”的事情。您知道x
始终是x
,并且您不必担心有人在声明之间的某个位置编写了x := foo bar
x
及其使用,因为这是不可能的。现在,如果我想读取用户的输入怎么办?正如 KennyTM 所说,这个想法是,不纯函数是一个将整个世界作为参数传递的纯函数,并返回其结果和世界。当然,您实际上并不想这样做:一方面,它非常笨重,另一方面,如果我重用同一个世界对象会发生什么?所以这会以某种方式被抽象化。 Haskell 使用 IO 类型来处理它:
这告诉我们 main 是一个不返回任何内容的 IO 操作;执行这个动作就是运行 Haskell 程序的意思。规则是 IO 类型永远无法逃脱 IO 操作;在这种情况下,我们使用
do
引入该操作。因此,getLine
返回一个IO String
,可以通过两种方式来考虑它:首先,作为一个操作,在运行时生成一个字符串;第二,作为一个操作,在运行时生成一个字符串。其次,作为一个被 IO“污染”的字符串,因为它是不纯粹获取的。第一个更正确,但第二个可能更有帮助。<-
从IO String
中取出String
并将其存储在str
中 - 但因为我们'当处于 IO 操作中时,我们必须将其重新包装起来,这样它就无法“逃逸”。下一行尝试读取一个整数 (reads
) 并获取第一个成功的匹配项 (fst . head
);这都是纯粹的(无 IO),因此我们用let no = ...
为其命名。然后我们可以在...
中同时使用no
和str
。因此,我们存储了不纯数据(从getLine
到str
)和纯数据(let no = ...
)。这种使用 IO 的机制非常强大:它可以让您将程序的纯粹算法部分与不纯粹的用户交互部分分开,并在类型级别强制执行。您的
minimumSpanningTree
函数不可能更改代码中其他位置的内容,或者向用户写入消息等。很安全。这是在 Haskell 中使用 IO 所需了解的全部内容;如果这就是你想要的,你可以停在这里。但如果您想了解为什么有效,请继续阅读。 (请注意,这些东西将特定于 Haskell — 其他语言可能会选择不同的实现。)
因此,这可能看起来有点像作弊,以某种方式为纯 Haskell 添加了杂质。但事实并非如此——事实证明,我们可以完全在纯 Haskell 中实现 IO 类型(只要我们给出
RealWorld
)。这个想法是这样的:IO 操作IO type
与函数RealWorld -> 相同。 (type, RealWorld)
,它采用现实世界并返回type
类型的对象和修改后的RealWorld
。然后我们定义几个函数,这样我们就可以使用这种类型而不会发疯:第一个函数允许我们讨论不执行任何操作的 IO 操作:
return 3
是一个不执行任何操作的 IO 操作查询现实世界并仅返回3
。>>=
运算符,发音为“bind”,允许我们运行 IO 操作。它从 IO 操作中提取值,通过函数将其与现实世界一起传递,然后返回结果 IO 操作。请注意,>>=
强制执行我们的规则,即永远不允许 IO 操作的结果逃逸。然后,我们可以将上面的
main
转换为以下一组普通的函数应用程序: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:抱歉,篇幅太长了。我扯得有点远了。)
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
I am guaranteed that the value of
x
is always the same in the...
: nothing can possibly change it. Similarly, if I have a functionf :: String -> Integer
(a function taking a string and returning an integer), I can be sure thatf
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 thatx
is alwaysx
, and you don't have to worry that somebody wrotex := foo bar
somewhere in between the declaration ofx
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:
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 usingdo
. Thus,getLine
returns anIO 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 theString
out of theIO String
and stores it instr
—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 withlet no = ...
. We can then use bothno
andstr
in the...
. We've thus stored impure data (fromgetLine
intostr
) 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 actionIO type
is the same as a functionRealWorld -> (type, RealWorld)
, which takes the real world and returns both an object of typetype
and the modifiedRealWorld
. We then define a couple functions so we can use this type without going insane: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 returns3
. 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:The Haskell runtime jump-starts
main
with the initialRealWorld
, 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 thedo
notation,return
, and>>=
work with any of them. As you saw here, monads aren't magical; all that's magical is thatdo
blocks turn into function calls. TheRealWorld
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.)
这里有很多很好的答案,但它们很长。我将尝试给出一个有用的简短答案:
函数式语言将状态放在与 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".
我正在中断对新答案的评论回复,以提供更多空间:
我写道:
诺曼写道:
@Norman:在什么意义上概括?我建议通常给出的指称模型/解释,
World -> (a,World)
与 HaskellIO
不匹配,因为它没有考虑不确定性和并发性。可能有一个更复杂的模型确实适合,例如World -> PowerSet [(a,World)]
,但我不知道这样的模型是否已经制定出来并被充分证明&持续的。鉴于IO
中填充了数千个 FFI 导入的命令式 API 调用,我个人怀疑能否找到这样的野兽。因此,IO
正在实现其目的:(摘自 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:
Norman wrote:
@Norman: Generalizes in what sense? I'm suggesting that the denotational model/explanation usually given,
World -> (a,World)
, doesn't match HaskellIO
because it doesn't account for nondeterminism and concurrency. There might be a more complex model that does fit, such asWorld -> 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 thatIO
is populated by thousands of FFI-imported imperative API calls. And as such,IO
is fulfilling its purpose:(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, sayingThis 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.
函数式编程源自 lambda 演算。如果您确实想了解函数式编程,请查看 http://worrydream.com/AlligatorEggs/
这是一个“有趣”的方式来学习 lambda 微积分,带您进入令人兴奋的函数式编程世界!
了解 Lambda 演算对函数式编程有何帮助。
因此,Lambda 演算是许多现实世界编程语言的基础,例如 Lisp、Scheme、ML、Haskell,......
假设我们想要描述一个将 3 与任何输入相加的函数,我们可以这样写:
阅读“plus3 是一个函数,当应用于任何数字 x 时,会产生 x 的后继者的后继者的后继者”
请注意,该函数将 3 与任何数字相加数字不必命名为 plus3;名称“plus3”只是命名此函数的方便简写
(
plus3 x) (succ 0) ≡ ((λ x.(succ (succ (succ x)))) (succ 0))
请注意,我们使用 lambda 符号表示函数(我认为它看起来有点像鳄鱼,我猜这就是鳄鱼蛋的想法的来源)
lambda 符号是 Alligator (一个函数) x 是它的颜色。您还可以将 x 视为一个参数(Lambda 演算函数实际上只假设有一个参数),其余的您可以将其视为函数的主体。
现在考虑抽象:
参数 f 用于函数位置(在调用中)。
我们将 ga 称为高阶函数,因为它需要另一个函数作为输入。
您可以将其他函数调用 f 视为“eggs”。
现在,利用我们创建的两个函数或“鳄鱼”,我们可以执行以下操作:
如果您注意到,您可以看到我们的 λ 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:
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:
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:
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.
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 ofscanf
if you had it, and then pass that function to the I/O API. That's exactly what>>=
does when you use theIO
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.(某些函数式语言允许不纯函数。)
对于纯函数语言,现实世界的交互通常作为函数参数之一包含在内,如下所示:
不同的语言有不同的策略来将世界从现实世界中抽象出来。程序员。例如,Haskell 使用 monad 来隐藏
world
参数。但函数式语言本身的纯粹部分已经是图灵完备的,这意味着在 C 中可行的任何事情在 Haskell 中也可行。与命令式语言的主要区别在于,不是就地修改状态:
将修改部分合并到函数调用中,通常将循环转变为递归:
(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:
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:
You incorporate the modification part into a function call, usually turning loops into recursions:
函数式语言可以保存状态!他们通常只是鼓励或强迫你明确这样做。
例如,查看 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.
可能有用,
面向我们其他人的函数编程
might be useful ,
Function Programming for the rest of us
haskell:
你当然可以用函数式语言将东西赋值给变量。你只是无法改变它们(所以基本上所有变量都是函数语言中的常量)。
haskell:
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).
该语言可能不会,但它的实现肯定会!
想想其中的所有状态 - 至少一个堆栈、一个或多个堆、各种文件描述符、当前配置等等。值得庆幸的是,处理这一切的是计算机,而不是你。嗯 - 让计算机处理无聊的事情:多好的概念啊!
按照这个速度,实现现在任何一天都将承担所有沉闷的 I/O 活动 - 然后你将听到外延语言...是的,还有更多针对新手的行话!但现在,我们将重点关注已经存在的功能 - 函数式语言:它们如何执行简单的 I/O 操作,例如读取输入?
非常小心!
大多数函数式语言与命令式语言的不同之处在于,仅允许直接操作 I/O 状态 - 您不能在定义例如记录其使用次数。为了防止这种情况发生,通常使用类型来区分基于 I/O 的代码和无 I/O 的代码,例如 Haskell 和 Clean 广泛使用该技术。
这可以很好地工作,甚至使函数式语言能够通过所谓的“外部函数接口”调用命令式语言中的
子例程过程。这允许将真正无限的以 I/O 为中心的操作(以及随后对基于 I/O 的状态的操作)引入到函数式语言中 -scanf()
只是开始.........等一下:“真正无限的以 I/O 为中心的操作”? 有限的实现不可能容纳所有这些,因此完全指示性语言将始终受到限制某种方式涉及其程序的外部交互。因此,I/O 必须始终是任何通用编程语言的一部分。
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
subroutinesprocedures 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.