【问题标题】:Can everything be modelled as functions?一切都可以建模为函数吗?
【发布时间】:2012-11-02 11:44:39
【问题描述】:

让我们考虑一个例子:我想打开/关闭一个灯泡。在 C 中,我可以写:

struct lightbulb {
    int is_turned_on;
    /* from 1 to 10 */
    int light_intensity;
};

每当我想打开或关闭灯泡时,我都会将 is_turned_on 更改为 1,并通过将 light_intensity 从 1(最暗)设置为 10(最亮)来更改它的亮度。

如何在函数式编程中做同样的事情?我想我必须创建一个列表来保存这些值,创建一个函数 ONOFF 来“打开”/关闭灯泡,以及一个返回灯泡光强度的函数。每次调用该函数时,都会返回一个新灯泡:

(defun turn-on()
  '(1 0))
(defun turn-off()
  '(0 0))
(defun light-intensity (x)
  `(0 ,(eval x)))

我可以看到像光强度这样的函数是一个类似于线性函数的连续函数。无论我们为每个 x 传递相同的参数 x 多少次,它都会评估相同的结果。每个函数的结果是一个具有不同状态的新灯泡。

问题是,我怎样才能持久化状态?显然,我必须通过变量将它存储在我记忆中的某个地方。

更新:我通过c2 Wiki - Functional Programming找到了上述问题的答案

数据项如何保留?

在堆栈上。在顺序批处理程序中,数据被初始化并 转换为顶级函数。在一个长期存在的程序中,比如 服务器,递归调用顶层循环函数,传递 从一次调用到下一次调用的全局状态。

我还得在每次调用函数的时候创建一个新对象(list),如何销毁之前的旧对象呢?

仅仅通过defparametersetf 改变变量不是更高效更简单吗?想象一下,如果它不是一个灯泡,而是一个包含更多信息的更复杂的物体?如何将其建模为函数?

【问题讨论】:

  • 您的 C 代码和您的 Common Lisp 代码都不正确。在 C 中,您不能在 struct 声明中分配字段。在 Lisp 中你不能申请0。而且我不明白你在问什么......可变数据并不是真正的函数式编程。
  • 哦,我忘了。我打算写一个主函数,但不知何故决定不写,并与结构声明混为一谈。我会修复的。但是关于 Lisp 代码,这就是我所理解的。我研究了一段时间的 Common Lisp,而不是 Scheme。
  • 我在问如何使用纯函数式编程有效地处理状态,而不需要另一个副本只是为了使其“引用透明”?通过阅读论文“为什么函数式编程很重要”,该论文表明没有赋值不是 FP 的优势。
  • Btw Lisp 代码在 CLISP 解释器中正确执行。

标签: functional-programming


【解决方案1】:

问题是,我怎样才能持久化状态?显然,我必须通过变量将它存储在内存中的某个地方。

我认为您正在从命令式的角度看待函数式编程,这种情况经常发生并且可能会令人困惑。在函数式编程中,它不是将程序表示为一系列修改状态的步骤(例如,通过设置变量),而是表示为一组相互依赖的数学风格的函数,每个函数只包含一个表达式。因为在函数中包含多行的唯一原因是修改状态,所以在纯函数式编程中,所有函数都是单行的;这意味着代码作为一系列级联函数调用运行。程序往往被视为对问题的描述,而不是分步说明。

我还得在每次调用函数的时候创建一个新的对象(list),如何销毁之前的旧对象呢?

我认为所有函数式编程语言都使用垃圾收集。更少的副作用和内存混叠意味着当内存不再被使用时更容易解决。

通过defparameter和setf来改变变量不是更高效更简单吗?想象一下,如果它不是一个灯泡,而是一个包含更多信息的更复杂的物体?如何将其建模为函数?

我不确定你在这里问什么。

【讨论】:

  • 最后一个问题的意思是通过简单地改变一个变量的值而不是经过所有的求值步骤,我们仍然可以得到我们想要的。想象一下,我有一个结构很重的对象(即树),我不能为了修改一个值而复制整个对象。
  • @Amumu 您不要复制整个内容,因为在纯函数式风格中,共享数据总是安全的。例如,如果您需要在左分支中某处具有不同值的新树,则可以直接在新树中使用对右分支的引用。当您稍后在其他人仍在使用原始值的情况下修改部分树时,尝试在命令式设置中执行此操作可能会混淆错误(请参阅标题中带有“列表”的 Python 标记中的每隔一个 SO 问题)。
  • 啊,我明白了。因此,我们可以重用现有数据而不必担心被破坏。但是,必须创建一个分支仍然很重。例如,我删除了二叉树中的一个节点。在 C 中,我只需将父节点的指针重新连接到已删除节点的子节点。根据您的建议,似乎我必须从已删除的节点开始更新整个分支,因为我无法更新父级的指针。
  • 虽然过度使用可变数据结构,但我遇到了令人沮丧的错误。我曾经在C中实现Karger的最小切割算法,并让节点链表中的每个节点通过指针引用该确切列表中的元素。结果,当谈到节点合并时,我遇到了各种各样的错误,我必须为图中的每个节点维护一个头节点列表,以降低在同一个列表上操作的复杂性。但是,我从来没有想过像不可变数据结构这样的极端对立面。
  • 补充 Ben:这也是编译器在 Haskell 等函数式编程中如此重要的原因。正如 Ben 所说,要确保函数式编程中不会出现“空点异常”之类的事情,不能“指向”数据,而只能复制它。当数据实际上不需要复制时,编译器会尽可能多地找出,这比让用户自己找出来要安全得多。因此,在某种程度上,安全地复制和操作数据的责任从用户转移到了编译器。
【解决方案2】:

我在问如何使用纯函数有效地处理状态 编程,没有另一个副本只是为了使其“参考” 透明度”?

只要语言支持线性类型,就可以在函数式编程中有效地处理状态。也就是说,每个可变单元格都被赋予一个线性类型,并且类型检查器会确保任何线性类型的变量都不会按照程序员的意愿被丢弃或复制。例如,这是不允许的:

val x = make_reference (5) // [x] is a mutable cell
val y = x
val res = !x + !y // the syntax [!x] is for reading a value of a cell

这是不允许的,因为 [x] 具有线性类型,并且线性类型的值不能重复(这本质上是我们在下一行所做的,将 [y] 绑定到 [x] 时) .这种重复也称为“别名”(或“共享”),而别名又使状态操作程序更难推理(例如,通过破坏引用透明度)。因此,线性类型限制了别名,这有助于推理程序。大多数情况下,具有线性类型的程序在引用上是透明的,并且与纯函数式程序有一些相似之处。

这是 ATS 中的一个示例,它使用线性类型来处理(可变)状态。

typedef lightbulb (b: bool) = @{is_turned_on= bool b, light_intensity= intBtw (1, 10)}

fn lightbulb_make {b:bool} (state: bool b, intensity: intBtw (1, 10)) :<> lightbulb b =
  @{is_turned_on= state, light_intensity= intensity}

// The [&T1 >> T2] notation means that function expects to be given
// a term of type T1, and then on exit, the type of the term will
// change to T2.
// In our case, the function expects a lightbulb either turned on or off,
// but on exit, the lightbulb will be turned off.
fn lightbulb_turn_on {b:bool} (x: &lightbulb b >> lightbulb true) :<> void =
  x.is_turned_on := true

fn lightbulb_change_intensity {b:bool} (x: &lightbulb b, y: intBtw (1, 10)) :<> void =
  x.light_intensity := y

implement main () = let
  var bulb = lightbulb_make (false, 5)
  val () = lightbulb_turn_on (bulb)
  val () = lightbulb_change_intensity (bulb, 3)
in
  printf ("intensity is now: %d\n", @(bulb.light_intensity))
end

【讨论】:

  • 如果您能将您的示例转换为 Common Lisp 或 Scheme,我将不胜感激。不过还是谢谢。
  • @Amumu:我没想到你对 CL 或 Scheme 感兴趣,抱歉。 (您能否在您的问题中澄清这一点?)但是,两种语言都有可变(无限制)引用的功能。所以这应该(大部分)足以有效地处理状态(除了处理稀缺资源,即生命周期有限的资源,例如文件句柄、套接字或手动内存管理)。如果您对纯粹的功能性方式感兴趣,那么我建议您研究镜头(这些在 Haskell 中经常出现)。另一件事是拉链数据结构。
  • 谢谢。我更新了问题。当我试图在 Common Lisp 中发挥作用时,我经常发现自己又回到了命令式风格。我想我会学习 Haskell 以更好地理解这个概念。
猜你喜欢
  • 2011-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-10
  • 2020-03-21
  • 1970-01-01
  • 2011-01-28
相关资源
最近更新 更多