时间函数在函数编程中如何存在?


646

我不得不承认我对函数式编程并不了解。我从这里到那里都读到它,因此知道在函数式编程中,无论调用多少次,函数对于相同的输入都会返回相同的输出。就像数学函数一样,该函数针对函数表达式中涉及的输入参数的相同值求出相同的输出。

例如,考虑一下:

f(x,y) = x*x + y; // It is a mathematical function

无论您使用多少次f(10,4),它的价值始终是104。这样,无论您在何处编写f(10,4),都可以将其替换为104,而无需更改整个表达式的值。此属性称为表达式的引用透明性。

由于维基说(链接

相反,在功能代码中,函数的输出值仅取决于输入到该函数的参数,因此,用参数x的相同值两次调用函数f会两次产生相同的结果f(x)。

函数编程中是否可以存在时间函数(返回当前时间)?

  • 如果是,那么它怎么存在?它不违反功能编程的原理吗?它特别违反了引用透明性,而引用透明性是函数式编程的特性之一(如果我正确理解的话)。

  • 否则,如何知道函数式编程的当前时间?


15
我认为大多数(或所有)函数式语言都不是那么严格,而是将函数式和命令式编程结合在一起。至少,这是我对F#的印象。
Alex F

13
@Adam:呼叫者首先如何知道当前时间?
纳瓦兹(Nawaz)

29
@Adam:实际上,在纯功能语言中,这是非法的(例如:不可能)。
sepp2k 2011年

47
@亚当:差不多。纯粹的通用语言通常提供一些便利来达到“世界状态”(例如,当前时间,目录中的文件等),而不会破坏参照透明性。在Haskell中,这是IO monad,在Clean中,这是世界类型。因此,在这些语言中,需要当前时间的函数要么将其作为参数,要么需要返回IO操作而不是其实际结果(Haskell),或者将世界状态作为其参数(Clean)。
sepp2k 2011年

12
考虑到FP时,很容易忘记:计算机是很大一部分可变状态。FP不会改变它,只是将其隐藏。
丹尼尔(Daniel)

Answers:


176

解释它的另一种方式是:没有函数可以获取当前时间(因为它不断变化),但是动作可以获取当前时间。假设这getClockTime是一个常量(如果愿意,则为null函数),它表示获取当前时间动作。无论何时使用,该动作都是相同的,因此它是一个实常数。

同样地,假设print有一个函数需要一些时间表示并将其打印到控制台。由于函数调用不能以纯函数语言产生副作用,因此我们可以想象它是一个需要时间戳并返回将其打印到控制台的操作的函数。同样,这是一个实函数,因为如果给它相同的时间戳,它将每次返回相同的打印操作

现在,如何将当前时间打印到控制台?好吧,您必须结合这两个动作。那么我们该怎么做呢?我们不能仅仅传递getClockTimeprint,因为print需要时间戳,而不是动作。但是我们可以想象有一个操作符,>>=组合了两个操作,一个操作获得时间戳,而一个操作者接受一个时间戳并打印出来。将其应用于前面提到的动作,结果是tadaaa ...一个新动作,它将获取当前时间并打印出来。顺便说一句,这正是在Haskell中完成的方式。

Prelude> System.Time.getClockTime >>= print
Fri Sep  2 01:13:23 東京 (標準時) 2011

因此,从概念上讲,您可以通过以下方式查看它:纯功能程序不执行任何I / O,它定义一个action,然后由运行时系统执行。该行动是相同的每一次,但在执行它的结果取决于在执行时的情况。

我不知道这是否比其他解释更清楚,但是有时它可以帮助我这样思考。


33
这对我没有说服力。您方便地调用getClockTime了动作而不是函数。好吧,如果您这样调用,然后调用每个函数action,那么即使命令式编程也将成为函数式编程。或者,也许您想将其称为行动编程。
Nawaz

92
@Nawaz:这里要注意的关键是您不能从函数内部执行操作。您只能将动作和功能组合在一起以进行新动作。执行动作的唯一方法是将其组合到main动作中。这允许将纯功能代码与命令性代码分隔开,并且这种分隔由类型系统强制执行。将动作视为一流的对象,还使您可以传递它们并构建自己的“控制结构”。
hammar 2011年

36
并不是Haskell中的所有功能都是函数-完全是胡说八道。函数的类型包含->-是标准定义术语的方式,而这实际上是Haskell上下文中唯一明智的定义。因此就其类型为IO Whatever不是一个函数。
sepp2k 2011年

9
@ sepp2k那么,myList :: [a-> b]是一个函数吗?;)
FUZ

8
@ThomasEding我参加聚会真的很晚,但我只想澄清一下:putStrLn不是动作–它是返回动作的函数。getLine包含操作的变量。动作是值,变量和函数,是我们赋予这些动作的“容器” /“标签”。
kqr

356

是的,没有。

不同的函数式编程语言以不同的方式解决它们。

在Haskell(非常纯净的一个)中,所有这些东西都必须在称为I / O Monad的东西中发生-参见此处

您可以将其视为向功能(世界状态)中添加另一个输入(和输出),或者将其视为“不确定性”(如获取变化的时间)发生的地方更容易。

其他语言(例如F#)只是内置了一些不纯,因此您可以使用一个函数为同一输入返回不同的值-就像普通的命令式语言一样。

正如Jeffrey Burka在他的评论中提到的:这是直接从Haskell Wiki 上对I / O Monad的很好的介绍


223
了解Haskell中的IO monad的关键是,解决这个问题不仅仅是破解。monad是在某些情况下定义一系列动作的一般解决方案。一种可能的情况是现实世界,为此我们拥有IO monad。另一个上下文是在原子事务中,为此我们有STM monad。还有一个上下文是将过程算法(例如Knuth shuffle)实现为纯函数,对此我们拥有ST monad。您也可以定义自己的单子。Monad是一种可重载的分号。
保罗·约翰逊

2
我发现不要将诸如获取当前时间的功能称为“函数”,而将诸如“过程”之类的东西称为有用(尽管可以争论的是,Haskell解决方案是对此的例外)。
singpolyma 2012年

从Haskell的角度来看,经典的“过程”(类型类似于“ ...->()”的东西)在某种程度上是微不足道的,因为带有...->()的纯函数根本无法做任何事情。
卡斯滕

3
典型的Haskell术语是“动作”。
塞巴斯蒂安·雷德尔

6
“单子是一种可重载的分号。” +1
user2805751

147

在Haskell中,使用名为monad的构造来处理副作用。monad基本上意味着将值封装到容器中,并具有一些函数以将函数从值链接到容器内的值。如果我们的容器具有以下类型:

data IO a = IO (RealWorld -> (a,RealWorld))

我们可以安全地执行IO操作。此类型的意思是:类型的动作IO是一个函数,它接受类型的令牌RealWorld并返回新的令牌以及结果。

其背后的思想是,每个IO操作都会改变由魔术令牌表示的外部状态RealWorld。使用monad,人们可以将多种功能链接在一起,从而将现实世界变异在一起。monad最重要的功能是>>=,发音为bind

(>>=) :: IO a -> (a -> IO b) -> IO b

>>=执行一个动作,然后执行该动作的结果并由此创建新动作的函数。返回类型是新操作。例如,让我们假设有一个函数now :: IO String,该函数返回代表当前时间的String。我们可以将其与函数链接以将putStrLn其打印出来:

now >>= putStrLn

或用do-Notation 编写,这对命令式程序员更熟悉:

do currTime <- now
   putStrLn currTime

所有这些都是纯净的,因为我们将突变和关于外部世界的信息映射到RealWorld令牌。因此,每次您运行此操作时,您当然都会得到不同的输出,但是输入并不相同:RealWorld令牌是不同的。


3
-1:我对RealWorld烟幕不满意。然而,最重要的是,这个声称的对象如何在链中传递。缺少的部分是从哪里开始的,它是到现实世界的源或连接的地方-它以在IO monad中运行的主要功能开始。
u0b34a0f6ae 2011年

2
@ kaizer.se您可以想到一个RealWorld在程序启动时传递给程序的全局对象。
2011年

6
基本上,您的main函数接受一个RealWorld参数。只有在执行时被它传入。
路易·沃瑟曼

13
您会发现,为什么他们隐藏RealWorld和仅提供微不足道的功能来进行更改,就像这样putStrLn,所以某些Haskell程序员不会RealWorld对其程序之一进行更改,以至于Haskell Curry的地址和出生日期使他们成为了邻居。长大(这可能以损害Haskell编程语言的方式破坏时空连续体。)
PyRulez 2014年

2
RealWorld -> (a, RealWorld) 只要牢记现实世界可能随时被功能(或当前过程)之外的宇宙其他部分改变,即使在并发情况下也不会分解为隐喻。因此(a)密码不分解,并且(b)每次将具有RealWorld其类型的值传递给函数时,都必须重新评估该函数,因为在此期间现实世界发生变化(就像@fuz解释的那样,每次我们与现实世界互动时都会返回不同的“令牌值”。
19qqwy

73

大多数函数式编程语言都不是纯语言,即它们允许函数不仅依赖于其值。在这些语言中,完全有可能具有一个返回当前时间的函数。从您用此问题标记的语言中,这适用于ScalaF#(以及ML的大多数其他变体)。

在像HaskellClean这样的纯语言中,情况有所不同。在Haskell中,当前时间将无法通过功能获得,而是通过所谓的IO动作获得,这是Haskell封装副作用的方式。

在Clean中,它将是一个函数,但该函数将以世界值作为其自变量,并返回新的世界值(除当前时间外)作为结果。类型系统将确保每个世界值只能使用一次(并且消耗世界值的每个函数都将产生一个新值)。这样,时间函数每次都必须使用不同的参数来调用,因此每次都允许返回不同的时间。


2
这听起来好像Haskell和Clean做了不同的事情。据我了解,它们做同样的事情,只是Haskell提供了更好的语法(?)来完成此任务。
康拉德·鲁道夫

27
@Konrad:他们都做相同的事情,因为它们都使用类型系统功能来抽象副作用,但仅此而已。请注意,用世界类型来解释IO monad很好,但是Haskell标准实际上并未定义世界类型,并且实际上不可能在Haskell中获得World类型的值(尽管这是非常可能的,而且确实是必要清洁)。此外,Haskell没有将唯一性键入作为类型系统功能,因此,如果它确实使您可以访问World,则无法确保以纯净的方式使用Clean来使用它。
sepp2k 2011年


22

绝对可以通过纯粹的功能方式来完成。有几种方法可以做到这一点,但最简单的方法是让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使用了这种技巧,然后通过使用monad隐藏了丑陋的部分。


有趣,但是i++for循环中的引用不是透明的;)
snim2 2012年

@ snim2我并不完美。:P安慰一下,脏可变性不会影响结果的参照透明性。如果两次通过相同的“ lastMeasurement”,则将获得旧的下一次测量并返回相同的结果。
Craig Gidney 2012年

@Strilanc谢谢你。我认为在命令式代码中,所以有趣的是看到以这种方式解释功能概念。然后,我可以想象一种自然而然的语法清洁的语言。
WW。

实际上,您也可以在C#中采用monad方式,从而避免了时间戳的显式传递。您需要类似的东西struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); }。但是带有此代码的代码仍不如Haskell看起来不错do
大约

@leftaroundabout可以通过将bind函数实现为称为的方法来假装您在C#中有一个monad,该方法SelectMany启用了查询理解语法。但是,您仍然不能对monad进行多态编程,因此,这与弱类型系统都是艰苦的战斗:(
sara 2016年

16

令我惊讶的是,答案或评论都没有提到结余或共生。通常,在推论无限数据结构时会提到协导,但它也适用于无休止的观察流,例如CPU上的时间寄存器。一头大哥大模仿隐藏状态;和协导模型观察该状态。(普通归纳模型构造状态。)

这是反应式功能编程中的热门话题。如果您对这种东西感兴趣,请阅读:http : //digitalcommons.ohsu.edu/csetech/91/(28 pp。)


3
那与这个问题有什么关系?
Nawaz 2014年

5
您的问题是关于以纯粹的函数方式(例如,返回当前系统时钟的函数)对时间相关行为进行建模。您可以通过所有功能及其依赖关系树对相当于IO monad的线程进行线程访问,以访问该状态。或者,您可以通过定义观察规则而不是构造性规则来对状态进行建模。这就是为什么在函数式编程中以归纳方式建模复杂状态看起来如此不自然的原因,因为隐藏状态实际上是一种共性质。
Jeffrey Aguilera 2014年

好消息来源!还有最近的吗?JS社区似乎仍在与流数据抽象斗争。
德米特里·扎伊采夫

12

是的,如果给定时间作为参数,则纯函数有可能返回时间。不同的时间参数,不同的时间结果。然后还形成其他时间函数,并将它们与功能(时间)转换(高阶)函数的简单词汇结合起来。由于该方法是无状态的,因此此处的时间可以是连续的(与分辨率无关),而不是离散的,从而极大地提高了模块性。这种直觉是功能反应式编程(FRP)的基础。


11

是! 你是对的!Now()或CurrentTime()或此类风味的任何方法签名均未以一种方式展现出引用透明性。但是通过向编译器发出指令,可以通过系统时钟输入对其进行参数化。

通过输出,Now()可能看起来好像不遵循参照透明性。但是,系统时钟及其顶部功能的实际行为是遵循引用透明性的。


11

是的,在函数式编程中可以使用对函数式编程稍加修改的版本(称为不纯函数式编程)来获取时间函数(默认或主要是纯函数式编程)。

如果需要时间(或读取文件或发射导弹),则代码需要与外部世界交互以完成工作,并且该外部世界并非基于函数式编程的纯粹基础。为了使纯函数式编程世界能够与这种不纯净的外部世界互动,人们引入了不纯净的函数式编程。毕竟,除了进行一些数学计算之外,不与外界交互的软件没有任何用处。

很少有函数式编程语言具有内置的这种杂点功能,因此很难区分出哪些代码是不纯的,哪些是纯净的(例如F#等),某些函数式编程语言会确保您在做一些不纯净的东西时与纯代码(例如Haskell)相比,该代码显然脱颖而出。

另一种有趣的观察方式是函数编程中的获取时间函数将使用一个“世界”对象,该对象具有世界的当前状态,例如时间,生活在世界上的人数等。然后从该世界获取时间对象将始终是纯净的,即,您通过相同的世界状态时,您将始终获得相同的时间。


1
“毕竟,除了进行一些数学计算之外,不与外界交互的软件没有任何用处。” 据我了解,即使在这种情况下,对计算的输入也将在程序中进行硬编码,也不是很有用。一旦您想从文件或终端读取输入数据进行数学计算,就需要不纯净的代码。
乔治

1
@Ankur:那是完全一样的东西。如果程序正在与除自身以外的其他事物交互(例如,可以说通过它们通过键盘输入的世界),那么它仍然不纯。
身份

1
@Ankur:是的,我认为你是对的!即使在命令行上传递大量输入数据可能不太实用,但这也可以是一种纯粹的方式。
乔治

2
具有“世界对象”,包括生活在世界上的人数,可以将正在执行的计算机提高到几乎无所不知的水平。我认为通常情况是,它包括HD上有多少个文件以及当前用户的主目录是什么。
ziggystar'9

4
@ziggystar-“世界对象”实际上不包含任何内容-它只是程序外部世界变化状态的代理。它的唯一目的是以一种类型系统可以识别它的方式显式标记可变状态。
Kris Nuttycombe'9

7

您的问题概括了计算机语言的两个相关度量:功能/命令式和纯/不纯。

功能语言定义功能的输入和输出之间的关系,命令式语言以特定的顺序描述要执行的特定操作。

纯语言不会产生或依赖副作用,而不纯净的语言会始终使用它们。

百分之一百的纯程序基本上是无用的。它们可能执行有趣的计算,但是由于它们没有副作用,因此没有输入或输出,因此您永远不会知道它们的计算结果。

为了完全有用,程序必须至少是一种不纯洁的东西。使纯程序有用的一种方法是将其放在薄的不纯包装物中。像这个未经测试的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

4
如果您可以解决获取时间的特定问题,并稍微解释一下我们认为IO价值观和结果纯属何种程度,那将很有帮助。
AndrewC 2012年

实际上,即使100%的纯程序也会使CPU发热,这是一个副作用。
约尔格W¯¯米塔格

3

您正在学习函数式编程中的一个非常重要的主题,即执行I / O。许多语言的实现方式是通过使用嵌入式领域特定的语言,例如,将动作编码的子语言。,它可以有结果。

例如,Haskell运行时期望我定义一个名为 main,由构成程序的所有动作组成。然后,运行时将执行此操作。在大多数情况下,它会执行纯代码。运行时将不时地使用计算出的数据执行I / O,并将数据反馈回纯代码。

您可能会抱怨,这听起来像是作弊,而且在某种程度上是:通过定义动作并期望运行时执行它们,程序员可以完成普通程序可以执行的所有操作。但是Haskell的强类型系统在程序的纯部分和“不正确”部分之间创建了一个强大的屏障:您不能简单地在当前CPU时间上增加两秒钟,然后打印出来,您必须定义一个导致当前结果的操作。 CPU时间,然后将结果传递给另一个操作,该操作将增加两秒钟并打印结果。不过,编写太多程序被认为是不好的风格,因为与告诉我们一切的 Haskell类型相比,它很难推断出是什么原因造成的,我们可以知道一个值是什么。

示例:clock_t c = time(NULL); printf("%d\n", c + 2);在C中,与main = getCPUTime >>= \c -> print (c + 2*1000*1000*1000*1000)在Haskell中。运算符>>=用于编写动作,将第一个动作的结果传递给导致第二个动作的函数。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 ()

后者看起来势在必行,不是吗?


1

如果是,那么它怎么存在?它不违反功能编程的原理吗?它特别违反了参照透明性

它不是纯粹从功能意义上存在的。

否则,如何知道函数式编程的当前时间?

首先,了解如何在计算机上检索时间可能很有用。本质上,板上电路可以跟踪时间(这就是计算机通常需要小电池的原因)。然后可能会有一些内部过程设置某个内存寄存器中的时间值。从本质上讲,这归结为可以由CPU检索的值。


对于Haskell,有一个“ IO操作”的概念,它表示可以执行某些IO流程的类型。因此,time我们引用IO Time值而不是引用值。所有这些都是纯粹的功能。我们不是在引用,time而是类似于“读取时间寄存器的值”的内容

当我们实际执行Haskell程序时,实际上会发生IO操作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.