我不得不承认我对函数式编程知之甚少。我从这里和那里读到过它,因此知道在函数式编程中,函数返回相同的输出,对于相同的输入,无论函数被调用多少次。它就像一个数学函数,对于函数表达式中涉及的输入参数的相同值,它的计算结果相同。
例如,考虑一下:
f(x,y) = x*x + y; // It is a mathematical function
无论您使用多少次 f(10,4)
,它的值始终是 104
。因此,无论您在哪里编写了 f(10,4)
,都可以将其替换为 104
,而不会更改整个表达式的值。此属性称为表达式的 referential transparency。
正如维基百科所说(link),
相反,在函数式代码中,函数的输出值仅取决于输入到函数的参数,因此使用相同的参数 x 值调用函数 f 两次将产生相同的结果 f(x) 两次。
函数式编程中可以存在时间函数(返回当前时间)吗?
如果是,那它怎么可能存在?不违反函数式编程的原则吗?它特别违反了引用透明性,这是函数式编程的属性之一(如果我正确理解的话)。
或者如果不是,那么在函数式编程中如何知道当前时间?
是和不是。
不同的函数式编程语言以不同的方式解决它们。
在 Haskell(一个非常纯粹的)中,所有这些东西都必须在称为 I/O Monad 的东西中发生 - 请参阅 here。
您可以将其视为在您的函数(世界状态)中获得另一个输入(和输出),或者更容易将其视为发生“不纯”(例如不断变化的时间)的地方。
其他语言(如 F#)只是内置了一些不纯性,因此您可以拥有一个为相同输入返回不同值的函数 - 就像普通的命令式语言一样。
正如 Jeffrey Burka 在他的评论中提到的:这里是直接来自 Haskell wiki 的 the nice introduction to the I/O Monad。
另一种解释方式是:没有 function 可以获取当前时间(因为它一直在变化),但是 action 可以获取当前时间。假设 getClockTime
是一个常量(或者你喜欢的空函数),它表示获取当前时间的动作。这个action无论什么时候使用,每次都是一样的,所以它是一个实常数。
同样,假设 print
是一个需要一些时间表示并将其打印到控制台的函数。由于函数调用在纯函数式语言中不会产生副作用,因此我们将其想象为一个函数,它接受时间戳并将其打印到控制台的action。同样,这是一个真正的函数,因为如果你给它相同的时间戳,它每次都会返回相同的 action 打印它。
现在,如何将当前时间打印到控制台?好吧,你必须将这两个动作结合起来。那么我们该怎么做呢?我们不能只将 getClockTime
传递给 print
,因为 print 需要一个时间戳,而不是一个动作。但我们可以想象有一个运算符 >>=
,它组合两个动作,一个获取时间戳,另一个将一个作为参数并打印出来。将此应用于前面提到的动作,结果是...... tadaaa......一个新的动作,它获取当前时间并打印它。顺便说一句,这正是 Haskell 中的做法。
Prelude> System.Time.getClockTime >>= print
Fri Sep 2 01:13:23 東京 (標準時) 2011
因此,从概念上讲,您可以这样看待它:纯函数式程序不执行任何 I/O,它定义了一个操作,然后运行时系统执行该操作。动作每次都是一样的,但执行的结果取决于执行时的情况。
我不知道这是否比其他解释更清楚,但它有时可以帮助我这样想。
getClockTime
称为操作而不是函数。好吧,如果你这样调用,然后调用每个函数action,那么即使是命令式编程也会变成函数式编程。或者,您可能希望将其称为actional 编程。
main
操作中。这允许将纯函数式代码与命令式代码分离,并且这种分离由类型系统强制执行。将动作视为第一类对象还允许您传递它们并构建您自己的“控制结构”。
->
的东西 - 这就是标准定义术语的方式,这实际上是 Haskell 上下文中唯一合理的定义。所以类型为 IO Whatever
的东西不是一个函数。
putStrLn
不是一个动作——它是一个返回一个动作的函数。 getLine
是一个包含动作的变量。动作是值,变量和函数是我们赋予这些动作的“容器”/“标签”。
在 Haskell 中,使用一种称为 monad 的构造来处理副作用。 monad 基本上意味着您将值封装到容器中,并具有一些函数将函数从值链接到容器内的值。如果我们的容器具有以下类型:
data IO a = IO (RealWorld -> (a,RealWorld))
我们可以安全地执行 IO 动作。此类型意味着: IO
类型的操作是一个函数,它接受 RealWorld
类型的标记并返回一个新标记以及结果。
这背后的想法是每个 IO 操作都会改变外部状态,由神奇的令牌 RealWorld
表示。使用单子,可以将多个改变现实世界的函数链接在一起。 monad 最重要的功能是 >>=
,发音为 bind:
(>>=) :: IO a -> (a -> IO b) -> IO b
>>=
执行一个操作和一个函数,该函数获取该操作的结果并由此创建一个新操作。返回类型是新的操作。例如,假设有一个函数 now :: IO String
,它返回一个表示当前时间的字符串。我们可以将它与函数 putStrLn
链接起来打印出来:
now >>= putStrLn
或者用命令式程序员更熟悉的 do
-Notation 编写:
do currTime <- now
putStrLn currTime
所有这些都是纯粹的,因为我们将外部世界的突变和信息映射到 RealWorld
标记。因此,每次运行此操作时,您当然会得到不同的输出,但输入不一样:RealWorld
标记不同。
RealWorld
烟幕不满意。然而,最重要的是这个所谓的对象是如何在链中传递的。缺少的部分是它开始的地方,与现实世界的源或连接所在的地方——它从运行在 IO monad 中的 main 函数开始。
RealWorld
对象。
main
函数采用 RealWorld
参数。只有在执行时才会传入。
RealWorld
并且只提供像 putStrLn
一样更改它的微不足道的函数的原因是,一些 Haskell 程序员不会用他们的程序之一更改 RealWorld
,例如 Haskell Curry 的地址和出生 -日期是这样的,他们长大后成为隔壁邻居(这可能会破坏时空连续体,从而损害 Haskell 编程语言。)
RealWorld -> (a, RealWorld)
即使在并发情况下也不会作为隐喻分解,只要您记住现实世界可能始终被您的函数(或当前进程)之外的宇宙的其他部分改变。所以(a)隐喻不会分解,并且(b)每次将具有 RealWorld
作为其类型的值传递给函数时,都必须重新评估该函数,因为现实世界 将 同时发生了变化(正如@fuz 解释的那样建模,每次我们与现实世界交互时都会返回不同的“令牌值”)。
大多数函数式编程语言都不是纯粹的,即它们允许函数不仅依赖于它们的值。在这些语言中,完全有可能有一个函数返回当前时间。从您标记此问题的语言来看,这适用于 Scala 和 F#(以及 ML 的大多数其他变体)。
在像 Haskell 和 Clean 这样的纯语言中,情况就不同了。在 Haskell 中,当前时间不会通过函数获得,而是通过所谓的 IO 动作获得,这是 Haskell 封装副作用的方式。
在 Clean 中,它将是一个函数,但该函数将一个世界值作为其参数,并返回一个新的世界值(除了当前时间)作为其结果。类型系统将确保每个世界值只能使用一次(并且每个使用世界值的函数都会产生一个新值)。这样,时间函数每次都必须使用不同的参数调用,因此每次都可以返回不同的时间。
“当前时间”不是函数。它是一个参数。如果您的代码取决于当前时间,则意味着您的代码是按时间参数化的。
它绝对可以以纯粹的功能方式完成。有几种方法可以做到这一点,但最简单的方法是让 time 函数不仅返回时间,还返回您必须调用的函数以获取下一次测量。
在 C# 中,您可以像这样实现它:
// Exposes mutable time as immutable time (poorly, to illustrate by example)
// Although the insides are mutable, the exposed surface is immutable.
public class ClockStamp {
public static readonly ClockStamp ProgramStartTime = new ClockStamp();
public readonly DateTime Time;
private ClockStamp _next;
private ClockStamp() {
this.Time = DateTime.Now;
}
public ClockStamp NextMeasurement() {
if (this._next == null) this._next = new ClockStamp();
return this._next;
}
}
(请记住,这是一个简单的示例,并不实用。特别是,列表节点不能被垃圾收集,因为它们以 ProgramStartTime 为根。)
这个“ClockStamp”类就像一个不可变的链表,但实际上节点是按需生成的,因此它们可以包含“当前”时间。任何想要测量时间的函数都应该有一个 'clockStamp' 参数,并且还必须在其结果中返回其最后一次测量(因此调用者看不到旧测量),如下所示:
// Immutable. A result accompanied by a clockstamp
public struct TimeStampedValue<T> {
public readonly ClockStamp Time;
public readonly T Value;
public TimeStampedValue(ClockStamp time, T value) {
this.Time = time;
this.Value = value;
}
}
// Times an empty loop.
public static TimeStampedValue<TimeSpan> TimeALoop(ClockStamp lastMeasurement) {
var start = lastMeasurement.NextMeasurement();
for (var i = 0; i < 10000000; i++) {
}
var end = start.NextMeasurement();
var duration = end.Time - start.Time;
return new TimeStampedValue<TimeSpan>(end, duration);
}
public static void Main(String[] args) {
var clock = ClockStamp.ProgramStartTime;
var r = TimeALoop(clock);
var duration = r.Value; //the result
clock = r.Time; //must now use returned clock, to avoid seeing old measurements
}
当然,必须将最后一次测量进出,进出,进出,这有点不方便。有很多方法可以隐藏样板,尤其是在语言设计级别。我认为 Haskell 使用了这种技巧,然后通过使用 monads 隐藏了丑陋的部分。
i++
不是引用透明的;)
struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); }
这样的东西。但是这样的代码看起来仍然不如使用 do
语法的 Haskell 好。
SelectMany
的方法来假装您在 C# 中有一个 monad,从而启用查询理解语法。尽管如此,你仍然不能在 monad 上进行多态编程,所以这对弱类型系统来说都是一场艰苦的战斗:(
令我惊讶的是,没有任何答案或评论提到余代数或共归纳法。通常,在推理无限数据结构时会提到共归纳,但它也适用于无穷无尽的观察流,例如 CPU 上的时间寄存器。一个代数模型隐藏状态;和观察该状态的共感应模型。 (构造状态的正态归纳模型。)
这是响应式函数式编程中的热门话题。如果您对这类内容感兴趣,请阅读以下内容:http://digitalcommons.ohsu.edu/csetech/91/(28 页)
Kieburtz, Richard B.,“反应式函数式编程”(1997 年)。 CSE技术。论文 91(链接)
是的,如果将时间作为参数给出,纯函数可以返回时间。不同的时间论证,不同的时间结果。然后也形成其他时间函数,并将它们与函数(时间)转换(高阶)函数的简单词汇结合起来。由于该方法是无状态的,因此这里的时间可以是连续的(与分辨率无关)而不是离散的,非常boosting modularity。这种直觉是函数响应式编程(FRP)的基础。
是的!你是对的! Now() 或 CurrentTime() 或这种风格的任何方法签名都没有以一种方式表现出引用透明性。但是通过对编译器的指令,它由系统时钟输入参数化。
通过输出,Now() 可能看起来不像遵循引用透明性。但是系统时钟的实际行为和它上面的功能是遵守参照透明的。
是的,获取时间函数可以存在于函数式编程中,使用函数式编程的一个稍微修改过的版本,称为不纯函数式编程(默认或主要的是纯函数式编程)。
在获取时间(或读取文件或发射导弹)的情况下,代码需要与外部世界交互才能完成工作,而这个外部世界并不是基于函数式编程的纯粹基础。为了让纯函数式编程世界与这个不纯的外部世界进行交互,人们引入了不纯函数式编程。毕竟,不与外界交互的软件除了进行一些数学计算之外没有任何用处。
很少有函数式编程语言内置了这种杂质特性,因此很难区分哪些代码是不纯的,哪些是纯的(如 F# 等),并且一些函数式编程语言确保当你做一些不纯的事情时与 Haskell 之类的纯代码相比,该代码显然很突出。
另一种有趣的方式是,函数式编程中的获取时间函数将采用一个“世界”对象,该对象具有世界的当前状态,如时间、世界上生活的人数等。然后从哪个世界获取时间对象将始终是纯的,即您以相同的世界状态传递,您将始终获得相同的时间。
您的问题将计算机语言的两个相关度量混为一谈:功能性/命令性和纯/不纯。
函数式语言定义函数的输入和输出之间的关系,命令式语言以特定的执行顺序描述特定的操作。
纯语言不会产生或依赖副作用,而不纯语言则始终使用它们。
百分之一百的纯程序基本上是没用的。他们可能会执行一个有趣的计算,但是因为他们没有副作用,所以他们没有输入或输出,所以你永远不会知道他们计算了什么。
要真正有用,程序必须至少有一点不纯。使纯程序有用的一种方法是将其放入一个薄的不纯包装器中。就像这个未经测试的 Haskell 程序:
-- this is a pure function, written in functional style.
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
-- This is an impure wrapper around the pure function, written in imperative style
-- It depends on inputs and produces outputs.
main = do
putStrLn "Please enter the input parameter"
inputStr <- readLine
putStrLn "Starting time:"
getCurrentTime >>= print
let inputInt = read inputStr -- this line is pure
let result = fib inputInt -- this is also pure
putStrLn "Result:"
print result
putStrLn "Ending time:"
getCurrentTime >>= print
IO
值和结果是纯粹的,那将会很有帮助。
您正在讨论函数式编程中一个非常重要的主题,即执行 I/O。许多纯语言的处理方式是使用嵌入式领域特定语言,例如,一种任务是编码动作的子语言,它可以产生结果。
例如,Haskell 运行时希望我定义一个名为 main
的操作,它由构成我的程序的所有操作组成。然后运行时执行此操作。大多数时候,这样做会执行纯代码。运行时会不时使用计算的数据来执行 I/O 并将数据反馈回纯代码。
你可能会抱怨这听起来像是作弊,在某种程度上确实如此:通过定义动作并期望运行时执行它们,程序员可以做任何普通程序可以做的事情。但是 Haskell 的强类型系统在程序的纯部分和“不纯”部分之间创建了一个强大的屏障:你不能简单地在当前 CPU 时间上添加两秒,然后打印它,你必须定义一个导致当前 CPU 时间的动作CPU 时间,并将结果传递给另一个增加两秒并打印结果的操作。但是,编写过多的程序被认为是一种不好的风格,因为与告诉我们关于值是什么的所有信息的 Haskell 类型相比,它很难推断导致了哪些影响。
示例:C 中的 clock_t c = time(NULL); printf("%d\n", c + 2);
与 Haskell 中的 main = getCPUTime >>= \c -> print (c + 2*1000*1000*1000*1000)
。运算符 >>=
用于组合动作,将第一个动作的结果传递给导致第二个动作的函数。这看起来很神秘,Haskell 编译器支持语法糖,允许我们编写后面的代码,如下所示:
type Clock = Integer -- To make it more similar to the C code
-- An action that returns nothing, but might do something
main :: IO ()
main = do
-- An action that returns an Integer, which we view as CPU Clock values
c <- getCPUTime :: IO Clock
-- An action that prints data, but returns nothing
print (c + 2*1000*1000*1000*1000) :: IO ()
后者看起来非常必要,不是吗?
如果是,那它怎么可能存在?不违反函数式编程的原则吗?它特别违反了参考透明度
它不存在于纯粹的功能意义上。
或者如果不是,那么在函数式编程中如何知道当前时间?
首先了解如何在计算机上检索时间可能很有用。本质上,板载电路可以跟踪时间(这就是计算机通常需要小型电池的原因)。然后可能会有一些内部进程在某个内存寄存器中设置时间值。这基本上归结为一个可以由 CPU 检索的值。
对于 Haskell,有一个“IO 动作”的概念,它表示可以用来执行某些 IO 过程的类型。因此,我们不引用 time
值,而是引用 IO Time
值。所有这些都是纯粹的功能性。我们引用的不是 time
,而是类似于 “读取时间寄存器的值” 的内容。
当我们实际执行 Haskell 程序时,IO 动作实际上会发生。
不用介绍FP的其他概念就可以回答。
可能性一:时间作为函数参数
一种语言由
语言核心和标准库。
引用透明性是语言核心的属性,而不是标准库的属性。它绝不是用该语言编写的程序的属性。
使用OP的符号,应该有一个功能
f(t) = t*v0 + x0; // mathematical function that knows current time
他们会要求标准库获取当前时间,例如 1.23
,并使用该值作为参数 f(1.23)
(或只是 1.23*v0 + x0
,引用透明度!)来计算函数。这样代码就可以知道当前时间。
可能性2:时间作为返回值
回答OP的问题:
函数式编程中可以存在时间函数(返回当前时间)吗?
是的,但是该函数必须有一个参数,并且您必须使用不同的输入来计算它,以便它返回不同的当前时间,否则它将违反 FP 的原则。
f(s) = t(s)*v0 + x0; // mathematical function t(s) returns current time
这是我上面描述的另一种方法。但话又说回来,首先获得那些不同的输入 s
的问题仍然归结为标准库。
可能性三:函数式反应式编程
这个想法是函数 t()
与函数 t2
配对计算当前时间。当一个人稍后再次需要当前时间时,他们将调用 t2()
,然后它会给出函数 t3
等等
(x, t2) = t(); // it's x o'clock now
...
(x2, t3) = t2(); // now it's already x2 o'clock
...
t(); x; // both evaluate to the initial time, referential transparency!
FP 还有更多内容,但我相信它超出了 OP 的范围。例如,如何要求标准库计算一个函数并以纯函数的方式对其返回值进行操作:这与其说是引用透明度,不如说是关于副作用。
函数式编程中如何存在时间函数?
早在 1988 年,Dave Harrison 在定义具有实时处理设施的早期函数式语言时就面临着这个问题。他为 Ruth 选择的解决方案可以在他的论文 Functional Real-Time Programming: The Language Ruth And Its Semantics 的第 50 页找到:
在运行时自动为每个 Ruth 进程提供一个独特的时钟,以提供实时信息,[...]
那么这些时钟是如何定义的呢?从第 61 页开始:
时钟树由一个节点组成,一个节点持有一个表示当前时间的非负整数,两个子树包含未来事件的时间。
此外:
当树被(懒惰地)评估时,每个节点都在实例化节点时用系统时间的值实例化,从而为程序提供当前时间的参考。
将其翻译成 Haskell:
type Clock = Tree Time
type Time = Integer -- must be zero or larger
data Tree a = Node { contents :: a,
left :: Tree a,
right :: Tree a }
除了访问当前时间(使用 contents
)外,每个 Ruth 进程还可以提供其他时钟(使用 left
和 right
)供程序中的其他地方使用。如果一个进程不止一次需要当前时间,它必须每次都使用一个新节点——一旦实例化,一个节点的内容保持不变。
这就是时间函数在函数式语言中的存在方式:无论何时调用它,始终应用于唯一的输入值(在这种情况下为时间树)。