我必须承认我对函数式编程了解不多。我从这里和那里读到它,所以开始知道在函数式编程中,一个函数返回相同的输出,对于相同的输入,无论函数被调用多少次。它就像一个数学函数,对于函数表达式中包含的输入参数的相同值,计算出相同的输出。

例如,考虑这个:

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

不管你用了多少次f(10,4)它的值总是104。因此,无论你在哪里写f(10,4),你都可以用104替换它,而不改变整个表达式的值。此属性称为表达式的引用透明性。

正如维基百科所说,

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

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

如果是,那它怎么可能存在?它是否违反了函数式编程的原则?它尤其违反了引用透明性,这是函数式编程的特性之一(如果我理解正确的话)。 如果没有,那么在函数式编程中如何知道当前时间呢?


当前回答

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

其他回答

“当前时间”不是一个函数。它是一个参数。如果您的代码依赖于当前时间,这意味着您的代码是由时间参数化的。

另一种解释是:没有函数可以获得当前时间(因为它一直在变化),但一个动作可以获得当前时间。让我们说getClockTime是一个常量(或者一个零函数,如果你喜欢的话),它表示获取当前时间的动作。无论什么时候使用,这个动作每次都是一样的,所以它是一个真正的常数。

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

Now, how can you print the current time to the console? Well, you have to combine the two actions. So how can we do that? We cannot just pass getClockTime to print, since print expects a timestamp, not an action. But we can imagine that there is an operator, >>=, which combines two actions, one which gets a timestamp, and one which takes one as argument and prints it. Applying this to the actions previously mentioned, the result is... tadaaa... a new action which gets the current time and prints it. And this is incidentally exactly how it is done in Haskell.

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

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

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

是的!你说对了!Now()或CurrentTime()或任何这种风格的方法签名都没有以某种方式显示引用透明性。但是通过对编译器的指令,它是由系统时钟输入参数化的。

通过输出,Now()可能看起来没有遵循引用透明性。但是系统时钟和它上面的功能的实际行为是坚持的 引用透明性。

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

早在1988年,Dave Harrison在定义带有实时处理功能的早期函数式语言时就面临着这个问题。他为Ruth选择的解决方案可以在他的论文《功能性实时编程:Ruth语言及其语义》第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 }

除了访问当前时间(带内容)之外,每个Ruth进程还可以提供其他时钟(带左时钟和右时钟),以便在程序的其他地方使用。如果一个进程不止一次需要当前时间,那么它必须每次都使用一个新节点——实例化后,节点的内容保持不变。

这就是函数式语言中时间函数的存在方式:无论调用它的地方,它总是应用于唯一的输入值(在这里是一个时间树)。

在Haskell中,我们使用一个叫做monad的结构来处理副作用。单子基本上意味着你将值封装到容器中,并在容器中使用一些函数将值与值之间的函数链接起来。如果容器的类型为:

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

我们可以安全地实现IO动作。IO类型的动作是一个函数,它接受一个RealWorld类型的令牌,并返回一个新的令牌和结果。

这背后的想法是,每个IO操作都会改变外部状态,由神奇的令牌RealWorld表示。通过使用单子,我们可以将多个函数链接在一起,从而改变现实世界。一个单子最重要的功能是>>=,发音为bind:

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

>>=取一个动作和一个函数,该函数取这个动作的结果并由此创建一个新的动作。返回类型是新动作。例如,假设现在有一个函数::IO String,它返回一个表示当前时间的String。我们可以用putStrLn函数链接它来打印它:

now >>= putStrLn

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

do currTime <- now
   putStrLn currTime

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