问题 Lisp-family:如何逃避面向对象的java式思维? [关闭]


背景故事:我在Java中做了很多大型且相对复杂的项目,在嵌入式C编程方面有很多经验。我已经熟悉了scheme和CL语法,并用racket编写了一些简单的程序。

问题:我已经计划了一个相当大的项目,并希望在球拍中做到这一点。我听过很多“如果你”得到“lisp,你将成为一个更好的程序员”等等。但每次我尝试计划或编写一个程序时,我仍然用熟悉的有状态对象与接口“分解”任务。
是否有针对lisp的“设计模式”?如何“获得”lisp-family“mojo”?如何逃避面向对象约束你的思考?如何应用功能强大的宏观设施推动的功能性编程思想?我尝试在github上研究大项目的源代码(例如Light Table)并且更加困惑,而不是开悟。
EDIT1(不那么暧昧的问题):关于这个主题是否有很好的文献,你可以推荐或者是否有用cl / scheme / clojure编写的高质量的开源项目,可以作为一个很好的例子吗?


3332
2017-12-20 17:08


起源

这里有一些有趣的解释,为什么Lisp(类似)语言中的设计模式不像其他语言那样普遍: norvig.com/design-patterns - uselpa
一种方法是从上到下开始写,并假设你有能够做你想要的功能。一旦你将问题定义为某种计算,更多功能习语往往会失败。 - WorBlux


答案:


多年来,一些“范例”已经流行起来: 结构化编程,面向对象,功能等。会有更多。

即使在一种模式不合时宜之后,它仍然可以很好地解决首先使其受欢迎的特定问题。

因此,例如使用OOP进行GUI仍然很自然。 (大多数GUI框架都有一堆由消息/事件修改的状态。)


球拍是多范式的。它有一个 class 系统。我很少用它, 但是当OO方法对问题有意义时,它是可用的。 Common Lisp有多种方法和CLOS。 Clojure有多种方法和Java类互操作。

无论如何,基本的有状态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是关于“反OOP”还是“支持功能”。相反,它是一种很好的方式来玩(并在生产中使用)编程的基本构建块。您可以探索不同的范例。您可以尝试诸如“代码是数据,反之亦然”之类的想法。

我不认为Lisp是某种精神体验。至多,它就像Zen,而satori意识到所有这些范例都只是同一枚硬币的不同方面。他们都很精彩,他们都很糟糕。指向解决方案的范例不是解决方案。等等等等等等。 :)


我的实际建议是,听起来你想要完善函数式编程的经验。如果你必须第一次在一个大项目上这样做,这是具有挑战性的。但在这种情况下,尝试将程序分解为“维持状态”与“计算事物”的部分。后者是你可以尝试专注于“更具功能性”的地方。寻找编写纯函数的机会。将它们连在一起。了解如何使用高阶函数。最后,将它们连接到应用程序的其余部分 - 这可以继续是有状态的,OOP和命令式。那是好的,现在,也许永远。


6
2017-12-20 18:30





比较OO与Lisp中的编程(以及一般的“功能”编程)的方法是查看每个“范例”对程序员的启用。

这一推理的一个观点是查看数据的表示,OO样式使得扩展数据表示变得更容易,但却使得在数据上添加操作变得更加困难。相比之下,功能样式使添加操作更容易,但更难添加新的数据表示。

具体来说,如果有一个带有OO的Printer接口,则很容易添加一个实现该接口的新HPPrinter类,但是如果要向现有接口添加新方法,则必须编辑实现该接口的每个现有类。 ,如果类定义隐藏在库中,则更难以实现。

相比之下,使用函数样式,函数(而不是类)是代码的单位,因此可以轻松添加新操作(只需编写函数)。但是,每个函数都负责根据输入的类型进行调度,因此添加新的数据表示需要编辑对该类数据进行操作的所有现有函数。

确定哪种样式更适合您的域取决于您是否更有可能添加表示或操作。

这当然是一种高级概括,并且每种风格都已经开发出解决方案来应对所提到的权衡(例如,针对OO的mixins),但我认为它仍然在很大程度上持有。

这是一篇着名的学术论文 25年前就抓住了这个想法。

以下是最近课程的一些注释 (我教过)描述相同的哲学。

(注意课程遵循 如何设计程序 课程,最初强调功能方法,但后来过渡到OO风格。)

编辑:当然这只回答了你的部分问题,并没有解决宏的(或多或少正交)主题。为此,我指的是 Greg Hendershott的优秀教程


4
2017-12-20 19:00



这并不适用于像Common Lisp Object System这样的东西,由于混合了开放类,多重继承,普通混合,没有接口,方法组合,多方法,它很容易添加类或方法......等等 - Rainer Joswig
@RainerJoswig当然你是对的。 “表达问题”存在许多“解决方案”,但我认为对于那些不熟悉的人来说,它仍然是两种风格的有用写照。 - stchang
“表达式问题”是具有封闭类的静态类型语言的问题。大多数Lisp对象系统不属于该类别。 - Rainer Joswig
@RainerJoswig了解“问题”仍然是有益的,因此人们可以理解与“解决方案”相关的权衡。开放类当然有自己的权衡,例如它们限制静态推理的数量和可能执行的优化。 - stchang
但这与Lisp vs. OO没什么关系,因为它不是OO vs. Lisp。自40年以来,Lisp已经以各种形式吸收/增强了OO。 Lisp甚至不是特别“功能性”...... Lisp在大多数时候也以多种方式限制静态推理。静态/封闭和静态类型的OO只是OO的一种方式。它不在Smalltalk,Javascript,Python,Ruby中,也不在大多数基于Lisp的OO中。 - Rainer Joswig


个人观点:

如果您在类的名称及其方法中对对象设计进行参数化 - 就像使用C ++模板一样 - 那么您最终会得到一些看起来非常像功能设计的东西。换句话说,函数式编程不会在类似结构之间做出无用的区分,因为它们的部分使用不同的名称。

我的曝光是Clojure,它试图从对象编程中窃取好处

  • 致力于接口

同时丢弃狡猾无用的东西

  • 具体的继承
  • 传统的数据隐藏。

关于该计划取得多大成功的意见不一。

由于Clojure是用Java(或某些等价物)表示的,因此对象不仅可以执行函数可以执行的操作,而且还可以实现从一个到另一个的常规映射。

那么功能优势在哪里呢?我会说表达力。在Java中不值得捕获的程序中有很多重复的事情 - 在Java提供之前使用lambdas 紧凑的语法 对他们?但机制始终存在。

而且Lisps有宏,它们可以使所有结构成为一流的。您将享受这些方面之间的协同作用。


2
2017-12-20 18:26





“Gang of 4”设计模式适用于Lisp系列,就像它们对其他语言一样。我使用CL,所以这更像是CL的观点/评论。

这是不同之处:考虑对类型族进行操作的方法。那是什么 defgeneric 和 defmethod 都是关于。你应该用 defstruct 和 defclass 作为数据的容器,请记住,您真正得到的只是数据的访问者。 defmethod 从一组类或类型的操作符(多重继承)的角度来看,它基本上是你常用的类方法(或多或少)。

你会发现你会使用 defun 和 define 很多。这很正常。当您确实在参数列表和相关类型中看到共性时,您将优化使用 defgeneric/defmethod。 (例如,在github上查找CL四叉树代码。)

宏:当您需要在一组表单周围粘合代码时很有用。就像您需要确保使用受保护的虚拟方法回收资源(关闭文件)或C ++“协议”样式一样,以确保特定的预处理和后处理。

而且,最后,不要犹豫再回来 lambda 封装内部机器。这可能是实现迭代器的最佳方式(“let over lambda”样式。)

希望这能让你开始。


2
2017-12-20 17:38



那么谁是第五帮派成员? - uselpa
林戈斯塔,当然。 - scooter me fecit