【问题标题】:Why delaying evaluation can transform impure functions into pure ones?为什么延迟评估可以将不纯函数转换为纯函数?
【发布时间】:2019-08-11 21:34:18
【问题描述】:

我知道纯函数是一个不依赖系统状态、没有副作用、输出只依赖于输入的函数。

进行 http 调用被认为是副作用。因此,以下是不纯函数的示例: const httpCall = (url, params) => $.getJson(url, params)

但是,只是延迟评估,我们可以将该函数转换为纯函数,如下所示: const pureHttpCall = (url, params) => () => $.getJson(url, params)

我们不再进行 http 调用。相反,我们返回一个函数,它会在被调用时执行此操作。这个函数是纯函数,因为它总是在给定相同输入的情况下返回相同的输出:在给定 url 和参数的情况下进行特定 http 调用的函数。

但这让我很困惑。因为当我们调用这个返回的函数时,无论如何我们都会进行 http 调用。我看不出这种“延迟评估”如何消除我们系统中的杂质。

我认为我需要澄清其中的一些概念,因为我不明白副作用(如 http 调用)如何适合函数式范式。

【问题讨论】:

  • 函数纯度没有考虑更深层次的返回值。它只关心pureHttpCall 的返回值,给定相同的输入,它是相同的:发出HTTP 请求的函数。返回的函数不是纯函数。
  • @Li357 那么.. 我可以说pureHttpCall 是纯函数,它的返回值是一个不纯函数吗?
  • 是的。返回的函数是实际执行副作用的函数,而纯函数不执行。
  • @Li357 这就是你通常如何处理函数式编程中的副作用和不纯的东西?
  • 基本上是的。只需传递表达式而不对其进行评估(即惰性评估),并且仅在您处于不纯环境中时才评估它们。

标签: javascript functional-programming


【解决方案1】:

所以,你认为下面的函数是不纯的。

const pureHttpCall = (url, params) => () => $.getJson(url, params)

这是一个合理的想法。但是,您会认为以下函数是纯函数吗?

const pureHttpCall = (url, params) => ({ url, params })

第二个函数不进行任何 HTTP 调用。它只是返回一个纯计算对象。

您最终可以使用这个纯计算对象来运行不纯的代码。例如,考虑一下。

const runIO = ({ url, params }) => $.getJson(url, params)

const pureHttpCall = (url, params) => ({ url, params })

const computation = pureHttpCall("example.json", {}) // create a pure computation

runIO(computation) // use the pure computation to run impure code

将纯计算对象视为对不纯代码的描述。描述是纯粹的,但它描述的东西是不纯粹的。我们使用计算对象数据结构作为不纯代码的描述。

现在,函数也是一种数据结构。毕竟,函数是 FP 中的一等值。因此,我们可以使用空函数,而不是使用对象作为描述数据结构。

const runIO = computation => computation()

const pureHttpCall = (url, params) => () => $.getJson(url, params)

const computation = pureHttpCall("example.json", {}) // create a pure computation

runIO(computation) // use the pure computation to run impure code

总而言之,$.getJson(url, params) 是一个不纯的 HTTP 调用。但是,() => $.getJson(url, params) 是对不纯 HTTP 调用的纯粹描述。

如果描述是一个不纯函数这一事实让您感到困扰,那么请将该函数视为一个纯计算对象,就像我在上面展示的那样。

像我上面所做的那样将函数转换为数据结构实际上是一种编译器优化技术,称为defunctionalization


这就是像 Haskell 这样的纯函数式语言处理杂质的方式。他们将不纯的代码包装在纯 IO 操作中。 IO 操作是对不纯代码的描述。在内部,IO 操作由状态单子数据结构定义,其中状态为 RealWorld[1]

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

一般的想法是你构建一个不纯代码的描述(即一个IO动作),然后当你运行程序时,Haskell运行时将运行main动作。也可以使用unsafePerformIOmain旁边运行IO动作,通常用于获取配置数据。


请注意,没有像@bob 的answer 所说的“编译时纯度​​”和“运行时杂质”这样的东西。纯度和杂质的分离是由描述不纯计算的纯数据结构完成的。正如我在上面展示的那样,你也可以用 JavaScript 等解释语言来做到这一点。

编译时间和运行时间并没有将纯度与杂质分开。编译时间只是指编译器执行的操作,例如宏扩展或类型检查。同样,运行时是指程序执行的操作。程序的编译时间就是编译器的运行时间。

请注意,程序的某些部分也可以在编译时执行。例如,函数内联和宏扩展是在编译时执行的用户态代码。在依赖类型语言中,程序的某些部分也在编译时进行评估,以进行类型检查。

【讨论】:

  • Haskell 程序的求值顺序并不总是由其代码的词法结构决定,因为它是纯粹的。编译器可以执行等式推理来优化代码。这就是我所说的“编译时纯度​​”。 Haskell 中的main 是每个程序的入口点,其类型为IO ()main 在运行时与它包含的所有不纯操作一起被解释。评估顺序必须由应用函子和单子确定,因为运行时解释不纯代码。这就是我所说的“运行时杂质”。
  • 您能否更具体地说明我的结论哪里错了?
猜你喜欢
  • 1970-01-01
  • 2020-01-06
  • 2020-06-07
  • 1970-01-01
  • 2017-06-28
  • 1970-01-01
  • 2020-07-18
  • 2020-06-09
  • 2015-06-01
相关资源
最近更新 更多