ChatGPT解决这个技术问题 Extra ChatGPT

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

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

例如,考虑一下:

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

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

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

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

我认为大多数(或所有)函数式语言并不那么严格,并且结合了函数式和命令式编程。至少,这是我对 F# 的印象。
@Adam:调用者首先如何知道当前时间?
@Adam:实际上它在纯函数式语言中是非法的(如:不可能)。
@亚当:差不多。纯粹的通用语言通常提供一些工具来获取“世界状态”(即当前时间、目录中的文件等),而不会破坏引用透明度。在 Haskell 中这是 IO monad,在 Clean 中是世界类型。因此,在那些语言中,需要当前时间的函数要么将其作为参数,要么需要返回一个 IO 操作而不是其实际结果(Haskell),或者将世界状态作为其参数(Clean)。
在考虑 FP 时很容易忘记:计算机是一大块可变状态。 FP不会改变它,它只是隐藏它。

P
Peter Mortensen

是和不是。

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

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

您可以将其视为在您的函数(世界状态)中获得另一个输入(和输出),或者更容易将其视为发生“不纯”(例如不断变化的时间)的地方。

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

正如 Jeffrey Burka 在他的评论中提到的:这里是直接来自 Haskell wiki 的 the nice introduction to the I/O Monad


关于 Haskell 中的 IO monad,需要意识到的关键是,它不仅仅是解决这个问题的 hack; monad 是在某些上下文中定义一系列动作的问题的一般解决方案。一种可能的上下文是现实世界,我们有 IO monad。另一个上下文是在原子事务中,我们有 STM monad。另一个上下文是作为纯函数的程序算法(例如 Knuth shuffle)的实现,为此我们有 ST monad。你也可以定义你自己的单子。 Monad 是一种可重载的分号。
我发现不要将诸如获取当前时间的“函数”之类的东西称为“程序”之类的东西很有用(尽管可以说 Haskell 解决方案是一个例外)。
从 Haskell 的角度来看,经典的“过程”(具有像 '... -> ()' 这样的类型的东西)有点微不足道,因为带有 ... -> () 的纯函数根本不能做任何事情。
典型的 Haskell 术语是“动作”。
@dawid 是的-您可以将列表视为一个序列,而该操作同样可以简单地产生元素值。您可以将选项视为单个元素或空列表。 - 在 Haskell 中更重要的是(一种语言,您没有逐个执行的语句 - 您有无法保证评估顺序的表达式 - 有些可能根本不会在 Haskell 中进行评估,因为它很懒惰)。带有绑定的 Monad 允许您表达按顺序处理某些内容的操作。
P
Peter Mortensen

另一种解释方式是:没有 function 可以获取当前时间(因为它一直在变化),但是 action 可以获取当前时间。假设 getClockTime 是一个常量(或者你喜欢的空函数),它表示获取当前时间的动作。这个action无论什么时候使用,每次都是一样的,所以它是一个实常数。

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

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

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

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

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


这对我没有说服力。您方便地将 getClockTime 称为操作而不是函数。好吧,如果你这样调用,然后调用每个函数action,那么即使是命令式编程也会变成函数式编程。或者,您可能希望将其称为actional 编程。
@Nawaz:这里要注意的关键是您不能从函数内执行操作。您只能将动作和功能组合在一起以进行新动作。执行操作的唯一方法是将其组合到您的 main 操作中。这允许将纯函数式代码与命令式代码分离,并且这种分离由类型系统强制执行。将动作视为第一类对象还允许您传递它们并构建您自己的“控制结构”。
并非 Haskell 中的所有内容都是函数——这完全是胡说八道。函数是其类型包含 -> 的东西 - 这就是标准定义术语的方式,这实际上是 Haskell 上下文中唯一合理的定义。所以类型为 IO Whatever 的东西不是一个函数。
@sepp2k 那么, myList :: [a -> b] 是一个函数吗? ;)
@ThomasEding 我真的迟到了,但我只想澄清一下:putStrLn 不是一个动作——它是一个返回一个动作的函数。 getLine 是一个包含动作的变量。动作是值,变量和函数是我们赋予这些动作的“容器”/“标签”。
c
chamini2

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

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

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

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

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

>>= 执行一个操作和一个函数,该函数获取该操作的结果并由此创建一个新操作。返回类型是新的操作。例如,假设有一个函数 now :: IO String,它返回一个表示当前时间的字符串。我们可以将它与函数 putStrLn 链接起来打印出来:

now >>= putStrLn

或者用命令式程序员更熟悉的 do-Notation 编写:

do currTime <- now
   putStrLn currTime

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


-1:我对 RealWorld 烟幕不满意。然而,最重要的是这个所谓的对象是如何在链中传递的。缺少的部分是它开始的地方,与现实世界的源或连接所在的地方——它从运行在 IO monad 中的 main 函数开始。
@kaizer.se 您可以考虑在程序启动时传递给程序的全局 RealWorld 对象。
基本上,您的 main 函数采用 RealWorld 参数。只有在执行时才会传入。
你看,他们隐藏 RealWorld 并且只提供像 putStrLn 一样更改它的微不足道的函数的原因是,一些 Haskell 程序员不会用他们的程序之一更改 RealWorld,例如 Haskell Curry 的地址和出生 -日期是这样的,他们长大后成为隔壁邻居(这可能会破坏时空连续体,从而损害 Haskell 编程语言。)
RealWorld -> (a, RealWorld) 即使在并发情况下也不会作为隐喻分解,只要您记住现实世界可能始终被您的函数(或当前进程)之外的宇宙的其他部分改变。所以(a)隐喻不会分解,并且(b)每次将具有 RealWorld 作为其类型的值传递给函数时,都必须重新评估该函数,因为现实世界 同时发生了变化(正如@fuz 解释的那样建模,每次我们与现实世界交互时都会返回不同的“令牌值”)。
P
Peter Mortensen

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

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

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


这听起来好像 Haskell 和 Clean 做了不同的事情。据我了解,它们也是这样做的,只是 Haskell 提供了更好的语法(?)来实现这一点。
@Konrad:从某种意义上说,它们都使用类型系统功能来抽象副作用,但仅此而已。请注意,用世界类型来解释 IO monad 非常好,但是 Haskell 标准实际上并没有定义世界类型,并且实际上不可能在 Haskell 中获得 World 类型的值(虽然这很有可能而且确实清洁时必需的)。此外,Haskell 没有唯一性类型作为类型系统功能,因此,如果它确实让您可以访问 World,它无法确保您像 Clean 那样以纯粹的方式使用它。
V
Vlad Patryshev

“当前时间”不是函数。它是一个参数。如果您的代码取决于当前时间,则意味着您的代码是按时间参数化的。


C
Craig Gidney

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


有趣,但 for 循环中的 i++ 不是引用透明的;)
@snim2 我并不完美。 :P 令人欣慰的是,肮脏的可变性不会影响结果的引用透明度。如果你两次传递相同的“lastMeasurement”,你会得到一个陈旧的下一个测量值并返回相同的结果。
@Strilanc 谢谢你。我认为在命令式代码中,因此以这种方式解释功能概念很有趣。然后,我可以想象一种语言自然且语法更清晰。
实际上,您也可以在 C# 中采用 monad 方式,从而避免显式传递时间戳。您需要像 struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); } 这样的东西。但是这样的代码看起来仍然不如使用 do 语法的 Haskell 好。
@leftaroundabout 您可以通过将绑定函数实现为称为 SelectMany 的方法来假装您在 C# 中有一个 monad,从而启用查询理解语法。尽管如此,你仍然不能在 monad 上进行多态编程,所以这对弱类型系统来说都是一场艰苦的战斗:(
W
Will Ness

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

这是响应式函数式编程中的热门话题。如果您对这类内容感兴趣,请阅读以下内容:http://digitalcommons.ohsu.edu/csetech/91/(28 页)

Kieburtz, Richard B.,“反应式函数式编程”(1997 年)。 CSE技术。论文 91(链接)


这与这个问题有什么关系?
您的问题是关于以纯功能方式建模与时间相关的行为,例如,返回当前系统时钟的函数。您可以通过所有函数和它们的依赖树将与 IO monad 等效的东西线程化,以访问该状态;或者您可以通过定义观察规则而不是构造规则来对状态进行建模。这就是为什么在函数式编程中对复杂状态进行归纳建模看起来如此不自然,因为隐藏状态实际上是一种互归纳属性。
伟大的来源!有没有更近期的? JS 社区似乎仍在为流数据抽象而苦苦挣扎。
C
Conal

是的,如果将时间作为参数给出,纯函数可以返回时间。不同的时间论证,不同的时间结果。然后也形成其他时间函数,并将它们与函数(时间)转换(高阶)函数的简单词汇结合起来。由于该方法是无状态的,因此这里的时间可以是连续的(与分辨率无关)而不是离散的,非常boosting modularity。这种直觉是函数响应式编程(FRP)的基础。


M
MduSenthil

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

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


P
Peter Mortensen

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

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

很少有函数式编程语言内置了这种杂质特性,因此很难区分哪些代码是不纯的,哪些是纯的(如 F# 等),并且一些函数式编程语言确保当你做一些不纯的事情时与 Haskell 之类的纯代码相比,该代码显然很突出。

另一种有趣的方式是,函数式编程中的获取时间函数将采用一个“世界”对象,该对象具有世界的当前状态,如时间、世界上生活的人数等。然后从哪个世界获取时间对象将始终是纯的,即您以相同的世界状态传递,您将始终获得相同的时间。


“毕竟,一个不与外界交互的软件除了做一些数学计算之外没有任何用处。”据我了解,即使在这种情况下,计算的输入也会在程序中进行硬编码,也不是很有用。一旦你想从文件或终端读取输入数据到你的数学计算中,你就需要不纯的代码。
@Ankur:那是一回事。如果程序与其他东西交互而不只是它自己(例如,通过他们的键盘,可以这么说)它仍然是不纯的。
@Ankur:是的,我认为你是对的!尽管在命令行上传递大量输入数据可能不是很实用,但这可能是一种纯粹的方式。
拥有包括生活在世界上的人数在内的“世界对象”将执行计算机提高到几乎无所不知的水平。我认为正常情况是它包括诸如您的 HD 上有多少文件以及当前用户的主目录是什么之类的内容。
@ziggystar - “世界对象”实际上不包含任何东西 - 它只是程序外部世界变化状态的代理。它的唯一目的是以类型系统可以识别的方式显式标记可变状态。
N
NovaDenizen

您的问题将计算机语言的两个相关度量混为一谈:功能性/命令性和纯/不纯。

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

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

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

要真正有用,程序必须至少有一点不纯。使纯程序有用的一种方法是将其放入一个薄的不纯包装器中。就像这个未经测试的 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

如果您能解决获得时间的具体问题,并稍微解释一下我们在多大程度上认为 IO 值和结果是纯粹的,那将会很有帮助。
事实上,即使是 100% 纯程序也会使 CPU 发热,这是一个副作用。
P
Peter Mortensen

您正在讨论函数式编程中一个非常重要的主题,即执行 I/O。许多纯语言的处理方式是使用嵌入式领域特定语言,例如,一种任务是编码动作的子语言,它可以产生结果。

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

你可能会抱怨这听起来像是作弊,在某种程度上确实如此:通过定义动作并期望运行时执行它们,程序员可以做任何普通程序可以做的事情。但是 Haskell 的强类型系统在程序的纯部分和“不纯”部分之间创建了一个强大的屏障:你不能简单地在当前 CPU 时间上添加两秒,然后打印它,你必须定义一个导致当前 CPU 时间的动作CPU 时间,并将结果传递给另一个增加两秒并打印结果的操作。但是,编写过多的程序被认为是一种不好的风格,因为与告诉我们关于值是什么的所有信息的 Haskell 类型相比,它很难推断导致了哪些影响。

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

后者看起来非常必要,不是吗?


C
Chris Stryczynski

如果是,那它怎么可能存在?不违反函数式编程的原则吗?它特别违反了参考透明度

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

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

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

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

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


d
darw

不用介绍FP的其他概念就可以回答。

可能性一:时间作为函数参数

一种语言由

语言核心和标准库。

引用透明性是语言核心的属性,而不是标准库的属性。它绝不是用该语言编写的程序的属性。

使用OP的符号,应该有一个功能

f(t) = t*v0 + x0; // mathematical function that knows current time

他们会要求标准库获取当前时间,例如 1.23,并使用该值作为参数 f(1.23)(或只是 1.23*v0 + x0,引用透明度!)来计算函数。这样代码就可以知道当前时间。

可能性2:时间作为返回值

回答OP的问题:

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

是的,但是该函数必须有一个参数,并且您必须使用不同的输入来计算它,以便它返回不同的当前时间,否则它将违反 FP 的原则。

f(s) = t(s)*v0 + x0; // mathematical function t(s) returns current time

这是我上面描述的另一种方法。但话又说回来,首先获得那些不同的输入 s的问题仍然归结为标准库。

可能性三:函数式反应式编程

这个想法是函数 t() 与函数 t2 配对计算当前时间。当一个人稍后再次需要当前时间时,他们将调用 t2(),然后它会给出函数 t3 等等

(x, t2) = t(); // it's x o'clock now
...
(x2, t3) = t2(); // now it's already x2 o'clock
...
t(); x; // both evaluate to the initial time, referential transparency!

FP 还有更多内容,但我相信它超出了 OP 的范围。例如,如何要求标准库计算一个函数并以纯函数的方式对其返回值进行操作:这与其说是引用透明度,不如说是关于副作用。


a
atravers

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

早在 1988 年,Dave Harrison 在定义具有实时处理设施的早期函数式语言时就面临着这个问题。他为 Ruth 选择的解决方案可以在他的论文 Functional Real-Time Programming: The Language Ruth And Its Semantics 的第 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 }

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

这就是时间函数在函数式语言中的存在方式:无论何时调用它,始终应用于唯一的输入值(在这种情况下为时间树)。