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

Naw*_*waz 636 f# haskell functional-programming scala clean-language

我承认我对功能编程知之甚少.我从这里和那里读到它,因此我们知道在函数式编程中,无论函数被调用多少次,函数都会为相同的输入返回相同的输出.它就像一个数学函数,对于函数表达式中涉及的输入参数的相同值,计算相同的输出.

例如,考虑一下:

f(x,y) = x*x + y; // It is a mathematical function
Run Code Online (Sandbox Code Playgroud)

无论你使用多少次f(10,4),它的价值永远都是104.因此,无论您在何处编写f(10,4),都可以替换它104,而无需更改整个表达式的值.此属性称为表达式的引用透明度.

正如维基百科所说(链接),

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

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

  • 如果是,那么它如何存在?它是否违反了函数式编程的原理?它特别违反了引用透明性,这是函数式编程的一个属性(如果我正确理解它).

  • 或者如果不是,那么如何才能知道函数式编程中的当前时间?

Car*_*ten 354

是的,不是.

不同的函数式编程语言以不同方式解

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

您可以将其视为将另一个输入(和输出)添加到您的函数(世界状态)中,或者更容易地将"不纯"视为获得更改时间的地方.

像F#这样的其他语言只是内置了一些不纯的东西,所以你可以拥有一个为同一输入返回不同值的函数 - 就像普通的命令式语言一样.

正如Jeffrey Burka在评论中提到的:这是Haskell wiki直接对I/O Monad的精彩介绍.

  • 在Haskell中实现IO monad的关键是要解决这个问题不仅仅是一个黑客攻击.monad是在某些上下文中定义一系列动作的问题的一般解决方案.一个可能的背景是现实世界,我们有IO monad.另一个上下文是在原子事务中,我们有STM monad.另一个背景是在程序算法(例如Knuth shuffle)的实现中作为纯函数,我们有ST monad.你也可以定义自己的monad.Monads是一种可重载的分号. (220认同)
  • “单子是一种可重载的分号。” +1 (5认同)
  • 典型的Haskell术语是"动作". (3认同)
  • 我发现不要调用诸如获取当前时间"函数"之类的东西,但是像"过程"这样的东西是有用的(虽然可以说Haskell解决方案是一个例外). (2认同)

dai*_*chi 171

解释它的另一种方法是:没有函数可以获得当前时间(因为它不断变化),但是一个动作可以获得当前时间.假设这getClockTime是一个常数(或者是一个Nullary函数,如果你愿意的话),它表示获取当前时间的动作.无论何时使用,这个动作都是一样的,所以它是一个真正的常数.

同样,假设print是一个需要一些时间表示并将其打印到控制台的函数.由于函数调用不能在纯函数式语言的副作用,而不是我们想象,它是一个函数,它接受一个时间戳和返回的动作将它打印到控制台.同样,这是一个真正的函数,因为如果你给它相同的时间戳,它将返回每次打印它的相同动作.

现在,如何将当前时间打印到控制台?好吧,你必须结合这两个动作.那我们怎么做呢?我们不能只传递getClockTimeprint,因为print需要一个时间戳,而不是一个动作.但我们可以想象有一个运算符,>>=结合了两个动作,一个获取时间戳,另一个作为参数并打印它.将此应用于前面提到的操作,结果是... tadaaa ...获取当前时间并打印它的新动作.顺便说一句,这恰好是在Haskell中完成的.

Prelude> System.Time.getClockTime >>= print
Fri Sep  2 01:13:23 ?? (???) 2011
Run Code Online (Sandbox Code Playgroud)

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

我不知道这是否比其他解释更清楚,但它有时帮助我这样想.

  • @Nawaz:这里要注意的关键是你不能在函数中执行动作.您只能将操作和功能组合在一起才能执行新操作.执行动作的唯一方法是将其组合成"main"动作.这允许将纯功能代码与命令式代码分开,并且这种分离由类型系统强制执行.将动作视为第一类对象还允许您传递它们并构建自己的"控制结构". (89认同)
  • 并非Haskell中的所有功能都是功能 - 这完全是胡说八道.函数的类型包含` - >` - 这就是标准定义术语的方式,而且它实际上是Haskell上下文中唯一合理的定义.所以类型为"IO Whatever"的东西不是**函数. (36认同)
  • 这对我来说并不令人信服.你方便地将`getClockTime`称为动作而不是函数.好吧,如果你这样调用,那么调用每个函数*action*,那么即使命令式编程也会成为函数式编程.或许,你想称之为*actional*programmming. (30认同)
  • @ sepp2k那么,myList :: [a - > b]是一个函数吗?;) (8认同)
  • 我个人不喜欢(讨厌)人们在谈论monad时经常使用的"行动"一词.它听起来像某些函数(是的,函数)不是函数,而是操作.行动只是人们(不是语言)对功能施加的标签.这就像调用我的`int boolVal`一个布尔值.不是不是.这是一个`int`.您只是将它用作布尔值.总而言之,Haskell中的所有内容(也许并非一切......*cough*unsafePerformIO和朋友)都是该术语真正数学意义上的一个功能. (6认同)
  • @ sepp2k:也许我正在扩展这个定义,因此我在括号中的注释.但我并不是第一个提出缺乏功能概念的人,所以不要严厉地评判我.那么,那些类型包含`=>`的值呢?也许你(或标准)不考虑它们的功能,除非它们也包含` - >`,但我不觉得我必须以你和标准的方式谈论Haskell,如只要我解释我的术语. (6认同)
  • @ThomasEding我参加派对的时间已经很晚了,但我只想澄清一下:`putStrLn`不是一个动作 - 它是一个*返回*动作的函数.`getLine`是*包含*动作的变量.动作是值,变量和函数是我们给出这些动作的"容器"/"标签". (6认同)
  • @trithithis好吧,也许我应该更清楚一点,动作也是函数(就像Haskell中的所有值都是函数一样,至少如果你调用常量的nullary函数),但是_not所有函数都是actions_.但我不认为_action_一词具有误导性或不精确性.动作是一种值,你可以通过它的类型来区分,就像你可以通过它们的类型来区分`Int`s.我没有在类型参数中钻取太多,部分是因为它在其他回复中有所涉及,部分原因是我想关注我的观点. (5认同)
  • @Omar:也许"同形函数"可能是一个更好的短语.我或多或少得到了参考透明度. (3认同)
  • @dainichi`=>`用于添加约束:例如,如果你创建了一个泛型排序函数,你可以在你的类型之前加上'Ord a =>`,以确保传入的所有东西都有一个比较值的方法.它与函数类型无关. (2认同)
  • @Nawaz:这就是能够编写“动作”的关键所在。您*可以*原则上将任何命令式程序转换为Monadic Haskell代码。区别在于Haskell的类型检查器强制将IO操作与纯功能代码分开。 (2认同)
  • @trinithis:并不是Haskell中的所有功能都是函数!就像普通的编程语言一样,它具有数字和字符串之类的东西,例如,它们不是函数,不能应用于参数。Haskell比大多数人似乎想的要少很多。 (2认同)
  • 虽然我同意“不纯的东西发生在单子中”的答案,但这让我很感兴趣。也帮助我理解为什么`>>=`是必要的 (2认同)
  • 这是一个很好的方式来解释IO monad的功能并帮助澄清我的理解,谢谢:) (2认同)
  • @sara 不完全是;范畴论学家会说,“Int”和“() -> Int”之间存在同构,其中一个元素的每个元素都可以与另一个元素的元素标识。这并不意味着一个元素*是*另一个元素。 (2认同)

fuz*_*fuz 147

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

data IO a = IO (RealWorld -> (a,RealWorld))
Run Code Online (Sandbox Code Playgroud)

我们可以安全地实施IO操作.此类型表示:类型的操作IO是一个函数,它接受类型的标记RealWorld并返回一个新标记以及结果.

这背后的想法是每个IO动作都会改变外部状态,由魔法标记表示RealWorld.使用monad,可以链接多个函数,一起改变现实世界.monad最重要的功能是>>=,发音为bind:

(>>=) :: IO a -> (a -> IO b) -> IO b
Run Code Online (Sandbox Code Playgroud)

>>=采取一个动作和一个获取此动作结果的函数,并从中创建一个新动作.返回类型是新操作.例如,让我们假装有一个函数now :: IO String,它返回一个代表当前时间的String.我们可以将它与函数链接putStrLn起来打印出来:

now >>= putStrLn
Run Code Online (Sandbox Code Playgroud)

或者用do-Notation 编写,这对命令式程序员来说比较熟悉:

do currTime <- now
   putStrLn currTime
Run Code Online (Sandbox Code Playgroud)

所有这一切都是纯粹的,因为我们将变异和外部世界的信息映射到RealWorld令牌.因此,每次运行此操作时,您当然会得到不同的输出,但输入不一样:RealWorld令牌不同.

  • 你看,他们之所以隐藏"RealWorld"并且只提供微不足道的功能来改变它就像`putStrLn`一样,是因为一些Haskell程序员不会用他们的一个程序改变'RealWorld`,这样Haskell Curry的地址和诞生 - 日期是这样的,他们成为了隔壁邻居的成长(这可能会损害时空连续体,从而伤害Haskell编程语言.) (13认同)
  • 基本上,你的`main`函数需要一个'RealWorld`参数.只有在执行时才会传入. (6认同)
  • -1:我对'RealWorld`烟幕不满意.然而,最重要的是这个声称的对象如何在链中传递.缺少的部分是它开始的地方,其中源或现实世界的连接是 - 它以在IO monad中运行的main函数开始. (2认同)
  • @ kaizer.se你可以想到一个全局的'RealWorld`对象,它在启动时传递给程序. (2认同)
  • “ RealWorld-&gt;(a,RealWorld)”即使在并发情况下也不会分解为隐喻,只要您牢记现实世界可能会被功能(或当前过程)之外的宇宙其他部分改变每时每刻。因此(a)密码不分解,并且(b)每次将具有RealWorld作为其类型的值传递给函数时,都必须重新评估该函数,因为真实世界*将*具有同时发生变化(建模为@fuz解释,每次我们与现实世界互动时都会返回不同的“令牌值”)。 (2认同)

sep*_*p2k 73

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

在像HaskellClean这样纯粹的语言中,情况就不同了.在Haskell中,当前时间不能通过函数获得,而是所谓的IO操作,这是Haskell封装副作用的方式.

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

  • @Konrad:他们做同样的事情是因为两者都使用类型系统特征来抽象副作用,但那是关于它的.请注意,根据世界类型来解释IO monad非常好,但是Haskell标准实际上并没有定义世界类型,并且实际上不可能在Haskell中获得World类型的值(尽管它很可能并且确实必要的清洁).进一步的Haskell没有作为类型系统功能的唯一性类型,因此如果它确实允许您访问World,则无法确保以纯粹的方式使用Clean. (27认同)
  • 这听起来好像Haskell和Clean做了不同的事情.根据我的理解,他们也这样做,只是Haskell提供了一个更好的语法(?)来实现这一目标. (2认同)

Vla*_*hev 50

"当前时间"不是一个功能.这是一个参数.如果您的代码取决于当前时间,则表示您的代码按时间参数化.


Cra*_*ney 22

它绝对可以以纯粹的功能方式完成.有几种方法可以做到这一点,但最简单的方法是让时间函数不仅返回时间,还要返回下次测量时必须调用的函数.

在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

(请记住,这是一个简单,不实用的示例.特别是,列表节点不能被垃圾收集,因为它们是由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
}
Run Code Online (Sandbox Code Playgroud)

当然,必须将最后一次测量输入和输出,输入和输出,输入和输出都有点不方便.隐藏样板的方法有很多种,特别是在语言设计层面.我认为Haskell使用这种技巧,然后使用monad隐藏丑陋的部分.


Jef*_*era 16

我很惊讶没有一个答案或评论提到代数或共同引导.通常,在推理无限数据结构时会提到共同诱导,但它也适用于无休止的观察流,例如CPU上的时间寄存器.代数模型隐藏状态; 和coinduction模型观察该状态.(正常感应模型构建状态.)

这是Reactive Functional Programming的热门话题.如果你对这类东西感兴趣,请阅读:http://digitalcommons.ohsu.edu/csetech/91/(28页)

  • 您的问题是以纯函数方式建模与时间相关的行为,例如,返回当前系统时钟的函数.您可以通过所有函数及其依赖树来创建与IO monad等效的东西,以访问该状态; 或者你可以通过定义观察规则而不是建设性规则来建模状态.这就是为什么在函数式编程中对复杂状态_inductively_进行建模似乎非常不自然,因为隐藏状态实际上是一个_coinductive_属性. (5认同)
  • 那与这个问题有什么关系呢? (3认同)

Con*_*nal 12

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


Ank*_*kur 11

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

在获得时间(或读取文件或发射导弹)的情况下,代码需要与外部世界交互以完成工作,并且这个外部世界不是基于函数式编程的纯粹基础.为了让纯粹的函数式编程世界与这个不纯洁的外部世界进行交互,人们已经引入了不纯的函数式编程.毕竟,除了做一些数学计算之外,不与外界交互的软件没有任何用处.

很少有函数式编程的编程语言中都有这种杂质的功能内置,使得它不容易分离出其中的代码是不纯的,并且是纯粹的(如F#等)和一些函数式编程语言确保当你做一些不纯的东西与纯代码(如Haskell)相比,该代码显然更加突出.

另一个有趣的方法是,你在函数式编程中获取时间函数将采用一个"世界"对象,它具有世界当前状态,如时间,生活在世界上的人数等等.然后从哪个世界获取时间对象总是纯粹的,即你传递的是同一个世界状态,你将永远得到同一时间.

  • @ziggystar - "世界对象"实际上并不包含任何内容 - 它只是代表程序之外的世界变化状态.它的唯一目的是以类型系统可以识别它的方式明确标记可变状态. (4认同)
  • “毕竟,不与外界交互的软件除了进行一些数学计算之外没有任何用处。” 据我了解,即使在这种情况下,计算的输入也会在程序中进行硬编码,也不是很有用。一旦您想从文件或终端读取数学计算的输入数据,您就需要不纯的代码。 (2认同)
  • 拥有包括生活在世界上的人数的"世界对象"将执行计算机提升到几乎无所不知的水平.我认为通常的情况是它包含了HD上有多少文件以及当前用户的主目录. (2认同)

Mdu*_*hil 11

是! 你是对的!Now()或CurrentTime()或这种风格的任何方法签名都没有以一种方式表现出参考透明度.但是通过对编译器的指令,它由系统时钟输入参数化.

通过输出,Now()看起来可能不遵循引用透明度.但是系统时钟的实际行为及其上面的功能都遵循参考透明度.


Nov*_*zen 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
Run Code Online (Sandbox Code Playgroud)

  • 如果你能解决获得时间的具体问题,将会有所帮助,并解释一下我们认为"IO"值和结果的纯粹程度. (4认同)