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

例如,考虑这个:

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

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

正如维基百科所说,

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

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

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


当前回答

在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令牌是不同的。

其他回答

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

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

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

在Haskell和Clean等纯语言中,情况就不同了。在Haskell中,当前时间不是通过函数,而是通过所谓的IO操作,这是Haskell封装副作用的方式。

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

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

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

早在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令牌是不同的。