首页 文章

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

提问于
浏览
599

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

例如,考虑一下:

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)两次 .

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

  • 如果是,那它怎么可能存在?它是否违反了函数式编程的原理?它特别违反referential transparency,这是函数式编程的属性之一(如果我正确理解它) .

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

13 回答

  • 13

    您的问题混淆了计算机语言的两个相关度量:功能/命令和纯/不纯 .

    函数式语言定义函数的输入和输出之间的关系,命令式语言以特定的顺序描述特定的操作 .

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

    百分之百的纯程序基本上没用 . 他们可能会执行一个有趣的计算,但因为他们没有副作用,所以他们没有输入或输出,所以你永远不会知道他们计算了什么 .

    为了有用,一个程序必须至少是一个微不足道的 . 使纯程序有用的一种方法是将它放在一个薄的不纯包装器中 . 像这个未经测试的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
    
  • 7

    如果是,那么它如何存在?它是否违反了函数式编程的原理?它特别违反了参考透明度

    它不存在于纯粹的功能意义上 .

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

    知道如何在计算机上检索时间可能首先是有用的 . 基本上存在跟踪时间的板载电路(这是计算机通常需要小型电池的原因) . 然后可能会有一些内部进程设置某个内存寄存器的时间值 . 这基本上可以归结为CPU可以检索的值 .


    对于Haskell,有一个'IO action'的概念,它表示可以执行某些IO过程的类型 . 因此,我们不是引用 time 值而是引用 IO Time 值 . 所有这些都是纯粹的功能 . 我们没有引用 time ,而是'read the value of the time register' .

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

  • 11

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

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

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

  • 137

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

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

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

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

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

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

  • 1

    是的,它将's possible for a pure function to return the time, if it' s作为参数给出了时间 . 不同的时间参数,不同的时间结果 . 然后形成其他时间函数,并将它们与功能(-of-time) - 变换(高阶)函数的简单词汇表组合在一起 . 由于该方法是无状态的,因此这里的时间可以是连续的(与分辨率无关的)而不是离散的,非常大的boosting modularity . 这种直觉是功能反应编程(FRP)的基础 .

  • 1

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

  • 44

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

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

  • 20

    您正在进行函数式编程中的一个非常重要的主题,即执行I / O.许多纯语言的使用方式是使用嵌入式特定于域的语言,例如,子语言,其任务是编码可能有结果的动作 .

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

    你可能会抱怨这听起来像是作弊,在某种程度上它是:通过定义动作并期望运行时执行它们,程序员可以完成普通程序可以做的所有事情 . 但是Haskell的强类型系统在程序的纯部分和"impure"部分之间创建了一个强大的障碍:你不能简单地将当前CPU时间添加两秒,并打印它,你必须定义一个导致当前CPU时间的动作,并将结果传递给另一个添加两秒的操作并打印结果 . 编写过多的程序被认为是糟糕的风格,因为与Haskell类型相比,它很难推断出哪些效果会产生,而Haskell类型告诉我们关于 Value 是什么的所有知识 .

    示例: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 ()
    

    后者看起来非常迫切,不是吗?

  • 10

    是的,不是 .

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

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

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

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

    正如Jeffrey Burka在评论中提到的那样:这是来自Haskell维基的_264441 .

  • 138

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

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

  • 345

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

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

    很少有函数式编程编程语言在其中内置了这种杂质特性,因此不容易分离出哪些代码是不纯的,哪些是纯粹的(如F#等),并且一些函数式编程语言确保当你做一些不纯的东西时与纯代码(如Haskell)相比,该代码显然更加突出 .

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

  • 67

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

    在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隐藏丑陋的部分 .

  • 10

    在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 -otation编写,这对命令式程序员来说比较熟悉:

    do currTime <- now
       putStrLn currTime
    

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

相关问题