Lisp-family:如何摆脱面向对象 java-like 思维?
Lisp-family: how to escape object-oriented java-like thinking?
背景:在Java做过很多大型的、比较复杂的项目,有丰富的嵌入式C编程经验。我已经熟悉了 scheme 和 CL 语法,并用 racket 写了一些简单的程序。
问:我计划了一个相当大的项目,想在球拍中完成。我听过很多 "if you "get“lisp,你会成为更好的程序员”等等。但每次我尝试计划或编写程序时,我仍然 "decompose" 使用熟悉的有状态对象来完成任务有接口。
lisp 有 "design patterns" 吗?如何"get" lisp-family "mojo"?如何摆脱面向对象对你思维的束缚?如何应用由强大的宏工具推动的函数式编程思想?我尝试在 github(例如 Light Table)上研究大型项目的源代码,结果更加困惑,而不是开悟。
EDIT1(不太含糊的问题):是否有关于该主题的优秀文献,您可以推荐,或者是否有用 cl/scheme/clojure 编写的高质量开源项目,可以作为一个很好的例子?
"Gang of 4" 设计模式适用于 Lisp 家族,就像它们适用于其他语言一样。我使用 CL,所以这更像是一个 CL perspective/commentary.
区别在于:根据对类型族进行操作的方法来思考。这就是 defgeneric
和 defmethod
的意义所在。您应该使用 defstruct
和 defclass
作为数据的容器,请记住,您真正得到的只是数据的访问器。 defmethod
基本上是您常用的 class 方法(或多或少)从运算符的角度来看一组 classes 或类型(多重继承。)
您会发现您会经常使用 defun
和 define
。这很正常。当您确实看到参数列表和关联类型中的共性时,您将使用 defgeneric
/defmethod
进行优化。 (例如,在 github 上查找 CL 四叉树代码。)
宏:当您需要围绕一组表单粘合代码时很有用。就像当您需要确保资源被回收(关闭文件)或 C++ "protocol" 风格时使用受保护的虚拟方法来确保特定的预处理和 post- 处理。
最后,不要犹豫 return 一个 lambda
来封装内部机制。这可能是实现迭代器的最佳方式("let over lambda" 风格。)
希望这能让你入门。
个人观点:
如果您使用 classes 及其方法的名称对对象设计进行参数化 - 正如您可能对 C++ 模板所做的那样 - 那么您最终会得到看起来很像功能设计的东西。换句话说,函数式编程不会因为相似结构的各个部分使用不同的名称而对其进行无用的区分。
我接触过 Clojure,它试图窃取对象编程的优点
- 使用接口
同时丢弃不可靠和无用的位
- 具体继承
- 传统数据隐藏。
对于该计划的成功程度众说纷纭。
由于 Clojure 是用 Java(或某些等价物)表示的,因此对象不仅可以做函数可以做的事情,而且还存在从一个对象到另一个对象的常规映射。
那么功能上的优势在哪里呢?我会说表现力。你在程序中做的很多重复的事情不值得在 Java 中捕获 - 在 Java 为他们提供 紧凑语法 之前谁使用过 lambda?然而,该机制始终存在。
而且 Lisps 有宏,具有使所有结构在前的效果 class。您会喜欢这些方面之间的协同作用。
许多 "paradigms" 这些年来已经流行起来:
结构化编程、面向对象、函数式等。还会有更多。
即使范式过时了,它仍然可以很好地解决最初使其流行的特定问题。
因此,例如将 OOP 用于 GUI 仍然很自然。 (大多数 GUI 框架都有一堆由 messages/events 修改的状态。)
Racket 是多范式的。它有一个 class
系统。我很少用,
但是当 OO 方法对问题有意义时它是可用的。
Common Lisp 有多种方法和 CLOS。 Clojure 有多种方法和 Java class 互操作。
无论如何,基本的有状态 OOP ~= 在闭包中改变一个变量:
#lang racket
;; My First Little Object
(define obj
(let ([val #f])
(match-lambda*
[(list) val]
[(list 'double) (set! val (* 2 val))]
[(list v) (set! val v)])))
obj ;#<procedure:obj>
(obj) ;#f
(obj 42)
(obj) ;42
(obj 'double)
(obj) ;84
这是一个很棒的对象系统吗?不,但它可以帮助您了解 OOP 的本质是用修改状态的函数封装状态。您可以在 Lisp 中轻松做到这一点。
我的意思是:我不认为使用 Lisp 是要成为 "anti-OOP" 或 "pro-functional"。相反,它是玩弄(并在生产中使用)编程基本构建块的好方法。您可以探索不同的范例。您可以尝试 "code is data and vice versa".
这样的想法
我不认为 Lisp 是某种精神体验。至多,它就像禅宗,悟就是认识到所有这些范式只是同一枚硬币的不同面。他们都很棒,他们都很糟糕。指向解决方案的范式不是解决方案。等等等等等等。 :)
我的实用建议是,听起来您想完善您在函数式编程方面的经验。如果您必须在一个大项目中第一次这样做,那将是具有挑战性的。但在那种情况下,请尝试将您的程序分成 "maintain state" 与 "calculate things" 的部分。后者是您可以尝试关注 "being more functional" 的地方。寻找机会编写纯函数。把它们连在一起。了解如何使用高阶函数。最后,将它们连接到您的应用程序的其余部分——它们可以继续是有状态的、面向对象的和命令式的。没关系,暂时,也许永远。
比较 OO 和 Lisp 编程(以及一般的 "functional" 编程)的一种方法是查看每个 "paradigm" 为程序员提供的功能。
这一推理过程中的一个观点(着眼于数据表示)是 OO 风格使得扩展数据表示更容易,但更难以添加对数据的操作。相比之下,函数式风格使得添加操作更容易,但更难添加新的数据表示。
具体来说,如果有一个 Printer 接口,使用 OO,添加一个实现该接口的新 HPPrinter class 非常容易,但是如果你想向现有接口添加一个新方法,你必须编辑每个实现该接口的现有 class,如果 class 定义隐藏在库中,这将更加困难并且可能是不可能的。
相比之下,在函数式风格中,函数(而不是classes)是代码单元,因此可以轻松添加新操作(只需编写一个函数)。但是,每个函数都负责根据输入的种类进行调度,因此添加新的数据表示需要编辑所有对这种数据进行操作的现有函数。
确定哪种样式更适合您的域取决于您是否更有可能添加表示或操作。
当然这是一个高层次的概括,每种风格都开发了解决方案来应对提到的权衡(例如 OO 的 mixins),但我认为它在很大程度上仍然适用。
Here is a well-known academic paper 在 25 年前抓住了这个想法。
Here are some notes from a recent course(我教过)描述了同样的哲学。
(请注意,该课程遵循 How to Design Programs 课程,最初强调功能方法,但后来过渡到 OO 风格。)
编辑:当然,这只能回答您的部分问题,并没有解决(或多或少正交的)宏主题。为此,我指的是 Greg Hendershott's excellent tutorial.
背景:在Java做过很多大型的、比较复杂的项目,有丰富的嵌入式C编程经验。我已经熟悉了 scheme 和 CL 语法,并用 racket 写了一些简单的程序。
问:我计划了一个相当大的项目,想在球拍中完成。我听过很多 "if you "get“lisp,你会成为更好的程序员”等等。但每次我尝试计划或编写程序时,我仍然 "decompose" 使用熟悉的有状态对象来完成任务有接口。
lisp 有 "design patterns" 吗?如何"get" lisp-family "mojo"?如何摆脱面向对象对你思维的束缚?如何应用由强大的宏工具推动的函数式编程思想?我尝试在 github(例如 Light Table)上研究大型项目的源代码,结果更加困惑,而不是开悟。
EDIT1(不太含糊的问题):是否有关于该主题的优秀文献,您可以推荐,或者是否有用 cl/scheme/clojure 编写的高质量开源项目,可以作为一个很好的例子?
"Gang of 4" 设计模式适用于 Lisp 家族,就像它们适用于其他语言一样。我使用 CL,所以这更像是一个 CL perspective/commentary.
区别在于:根据对类型族进行操作的方法来思考。这就是 defgeneric
和 defmethod
的意义所在。您应该使用 defstruct
和 defclass
作为数据的容器,请记住,您真正得到的只是数据的访问器。 defmethod
基本上是您常用的 class 方法(或多或少)从运算符的角度来看一组 classes 或类型(多重继承。)
您会发现您会经常使用 defun
和 define
。这很正常。当您确实看到参数列表和关联类型中的共性时,您将使用 defgeneric
/defmethod
进行优化。 (例如,在 github 上查找 CL 四叉树代码。)
宏:当您需要围绕一组表单粘合代码时很有用。就像当您需要确保资源被回收(关闭文件)或 C++ "protocol" 风格时使用受保护的虚拟方法来确保特定的预处理和 post- 处理。
最后,不要犹豫 return 一个 lambda
来封装内部机制。这可能是实现迭代器的最佳方式("let over lambda" 风格。)
希望这能让你入门。
个人观点:
如果您使用 classes 及其方法的名称对对象设计进行参数化 - 正如您可能对 C++ 模板所做的那样 - 那么您最终会得到看起来很像功能设计的东西。换句话说,函数式编程不会因为相似结构的各个部分使用不同的名称而对其进行无用的区分。
我接触过 Clojure,它试图窃取对象编程的优点
- 使用接口
同时丢弃不可靠和无用的位
- 具体继承
- 传统数据隐藏。
对于该计划的成功程度众说纷纭。
由于 Clojure 是用 Java(或某些等价物)表示的,因此对象不仅可以做函数可以做的事情,而且还存在从一个对象到另一个对象的常规映射。
那么功能上的优势在哪里呢?我会说表现力。你在程序中做的很多重复的事情不值得在 Java 中捕获 - 在 Java 为他们提供 紧凑语法 之前谁使用过 lambda?然而,该机制始终存在。
而且 Lisps 有宏,具有使所有结构在前的效果 class。您会喜欢这些方面之间的协同作用。
许多 "paradigms" 这些年来已经流行起来: 结构化编程、面向对象、函数式等。还会有更多。
即使范式过时了,它仍然可以很好地解决最初使其流行的特定问题。
因此,例如将 OOP 用于 GUI 仍然很自然。 (大多数 GUI 框架都有一堆由 messages/events 修改的状态。)
Racket 是多范式的。它有一个 class
系统。我很少用,
但是当 OO 方法对问题有意义时它是可用的。
Common Lisp 有多种方法和 CLOS。 Clojure 有多种方法和 Java class 互操作。
无论如何,基本的有状态 OOP ~= 在闭包中改变一个变量:
#lang racket
;; My First Little Object
(define obj
(let ([val #f])
(match-lambda*
[(list) val]
[(list 'double) (set! val (* 2 val))]
[(list v) (set! val v)])))
obj ;#<procedure:obj>
(obj) ;#f
(obj 42)
(obj) ;42
(obj 'double)
(obj) ;84
这是一个很棒的对象系统吗?不,但它可以帮助您了解 OOP 的本质是用修改状态的函数封装状态。您可以在 Lisp 中轻松做到这一点。
我的意思是:我不认为使用 Lisp 是要成为 "anti-OOP" 或 "pro-functional"。相反,它是玩弄(并在生产中使用)编程基本构建块的好方法。您可以探索不同的范例。您可以尝试 "code is data and vice versa".
这样的想法我不认为 Lisp 是某种精神体验。至多,它就像禅宗,悟就是认识到所有这些范式只是同一枚硬币的不同面。他们都很棒,他们都很糟糕。指向解决方案的范式不是解决方案。等等等等等等。 :)
我的实用建议是,听起来您想完善您在函数式编程方面的经验。如果您必须在一个大项目中第一次这样做,那将是具有挑战性的。但在那种情况下,请尝试将您的程序分成 "maintain state" 与 "calculate things" 的部分。后者是您可以尝试关注 "being more functional" 的地方。寻找机会编写纯函数。把它们连在一起。了解如何使用高阶函数。最后,将它们连接到您的应用程序的其余部分——它们可以继续是有状态的、面向对象的和命令式的。没关系,暂时,也许永远。
比较 OO 和 Lisp 编程(以及一般的 "functional" 编程)的一种方法是查看每个 "paradigm" 为程序员提供的功能。
这一推理过程中的一个观点(着眼于数据表示)是 OO 风格使得扩展数据表示更容易,但更难以添加对数据的操作。相比之下,函数式风格使得添加操作更容易,但更难添加新的数据表示。
具体来说,如果有一个 Printer 接口,使用 OO,添加一个实现该接口的新 HPPrinter class 非常容易,但是如果你想向现有接口添加一个新方法,你必须编辑每个实现该接口的现有 class,如果 class 定义隐藏在库中,这将更加困难并且可能是不可能的。
相比之下,在函数式风格中,函数(而不是classes)是代码单元,因此可以轻松添加新操作(只需编写一个函数)。但是,每个函数都负责根据输入的种类进行调度,因此添加新的数据表示需要编辑所有对这种数据进行操作的现有函数。
确定哪种样式更适合您的域取决于您是否更有可能添加表示或操作。
当然这是一个高层次的概括,每种风格都开发了解决方案来应对提到的权衡(例如 OO 的 mixins),但我认为它在很大程度上仍然适用。
Here is a well-known academic paper 在 25 年前抓住了这个想法。
Here are some notes from a recent course(我教过)描述了同样的哲学。
(请注意,该课程遵循 How to Design Programs 课程,最初强调功能方法,但后来过渡到 OO 风格。)
编辑:当然,这只能回答您的部分问题,并没有解决(或多或少正交的)宏主题。为此,我指的是 Greg Hendershott's excellent tutorial.