Lisp -семейство: как избежать объектно-ориентированного java-подобного мышления?

Backstory: Я сделал много больших и относительно сложных проектов на Java, имею большой опыт программирования в встроенном программировании. Я познакомился с синтаксисом схемы и CL и написал несколько простых программ с ракеткой.

Вопрос: Я запланировал довольно большой проект и хочу сделать это в ракетке. Я слышал много "если вы" получите "lisp, вы станете лучшим программистом" и т.д. Но каждый раз, когда я пытаюсь планировать или писать программу, я все еще "разлагаю" задачу знакомыми объектами с сохранением состояния интерфейсы.
Существуют ли "шаблоны проектирования" для lisp? Как "получить" lisp -семейство "mojo"? Как избежать объектно-ориентированного ограничения на ваше мышление? Как применять идеи функционального программирования, усиленные мощными макроуровнями? Я попытался изучить исходный код больших проектов на github (например, Light Table) и стал более запутанным, а не просвещенным.
EDIT1 (менее двусмысленные вопросы): есть ли хорошая литература по этой теме, которую вы можете рекомендовать или есть хорошие проекты с открытым исходным кодом, написанные в cl/schem/ clojure, которые имеют высокое качество и могут служить хорошим примером?

Ответ 1

На протяжении многих лет в моду входило множество "парадигм": структурированное программирование, объектно-ориентированный, функциональный и т.д. Далее придет.

Даже после того, как парадигма выпадает из моды, она все же может помочь решить конкретные проблемы, которые в первую очередь сделали ее популярной.

Так, например, использование ООП для графического интерфейса является естественным. (В большинстве инфраструктур графического интерфейса есть группа состояний, модифицированных сообщениями/событиями.)


Ракетка - это мультипарадигма. Он имеет систему class. Я редко использую его, но он доступен, когда подход OO имеет смысл для проблемы. Общий Lisp имеет несколько методов и CLOS. Clojure имеет несколько методов и интерфейс класса Java.

И вообще, базовый ООП с состоянием состояния ~ = мутация переменной в замыкании:

#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

Это отличная объектная система? Нет. Но это помогает понять, что суть ООП заключается в инкапсулировании состояния с функциями, которые его модифицируют. И вы можете сделать это в Lisp, легко.


Что я получаю: я не думаю, что использование Lisp - это "анти-ООП" или "про-функциональный". Вместо этого, это отличный способ играть с (и использовать в производстве) основные строительные блоки программирования. Вы можете исследовать различные парадигмы. Вы можете экспериментировать с такими идеями, как "код - это данные и наоборот".

Я не вижу Lisp как своего рода духовный опыт. В лучшем случае это похоже на Дзэн, а сатори - это осознание того, что все эти парадигмы - это разные стороны одной и той же монеты. Они все замечательные, и все они сосут. Парадигма, указывающая на решение, не является решением. Бла бла бла.:)


Мои практические советы: похоже, вы хотите объединить свой опыт с функциональным программированием. Если вы должны сделать это в первый раз в большом проекте, это сложно. Но в этом случае попробуйте разбить свою программу на части, которые "поддерживают состояние" и "вычисляют вещи". Последние - это то, где вы можете попытаться сосредоточиться на "более функциональном". Ищите возможности писать чистые функции. Соедините их вместе. Узнайте, как использовать функции более высокого порядка. И, наконец, подключите их к остальной части вашего приложения - которая может продолжать быть сдержанной и ООП и обязательна. Это нормально, на данный момент и, возможно, навсегда.

Ответ 2

Способ сравнения программирования в OO vs Lisp (и "функциональное" программирование в целом) заключается в том, чтобы посмотреть, что каждая "парадигма" позволяет программисту.

Одна точка зрения в этой строке рассуждений, которая рассматривает представления данных, заключается в том, что стиль OO упрощает расширение представлений данных, но затрудняет добавление операций с данными. Напротив, функциональный стиль упрощает добавление операций, но сложнее добавлять новые представления данных.

Конкретно, если есть интерфейс принтера, с OO, очень просто добавить новый класс HPPrinter, который реализует интерфейс, но если вы хотите добавить новый метод к существующему интерфейсу, вы должны отредактировать каждый существующий класс, который реализует интерфейс, который сложнее и может быть невозможным, если определения классов скрыты в библиотеке.

В отличие от функционального стиля, функции (вместо классов) являются единицей кода, поэтому можно легко добавить новую операцию (просто написать функцию). Однако каждая функция отвечает за диспетчеризацию в соответствии с типом ввода, поэтому добавление нового представления данных требует редактирования всех существующих функций, которые работают с такими данными.

Определение того, какой стиль более подходит для вашего домена, зависит от того, будет ли вы более вероятно добавлять представления или операции.

Это, конечно, обобщение на высоком уровне, и каждый стиль разработал решения, чтобы справиться с упомянутыми компромиссами (например, mixins для OO), но я думаю, что это все еще имеет место в значительной степени.

Вот известная научная статья, которая захватила идею 25 лет назад.

Вот некоторые примечания из недавнего курса (я преподавал), описывающие ту же философию.

(Обратите внимание, что курс следует Учебное пособие по разработке программ, которое первоначально подчеркивает функциональный подход, но затем переходит к стилю OO. )

edit: Конечно, это только ответы на часть вашего вопроса и не затрагивает (более или менее ортогональную) тему макросов. Для этого я обращаюсь к превосходному учебнику Greg Hendershott.

Ответ 3

Шаблоны дизайна "Банда 4" относятся к семейству Lisp так же, как и к другим языкам. Я использую CL, так что это скорее перспектива CL/комментарий.

Здесь разница: Подумайте о методах, которые работают с семействами типов. Это то, что defgeneric и defmethod. Вы должны использовать defstruct и defclass в качестве контейнеров для своих данных, имея в виду, что все, что вы действительно получаете, являются аксессуарами к данным. defmethod - это в основном ваш обычный метод класса (более или менее) с точки зрения оператора в группе классов или типов (множественное наследование.)

Вы обнаружите, что вы будете использовать defun и define много. Это нормально. Когда вы видите общность в списках параметров и связанных с ними типах, вы оптимизируете с помощью defgeneric/defmethod. (Посмотрите, например, код квадранта CL на github).

Макросы: полезно, когда вам нужно склеить код вокруг набора форм. Например, когда вам нужно обеспечить восстановление ресурсов (закрытие файлов) или стиль "С++" протокола С++ с использованием защищенных виртуальных методов для обеспечения конкретной предварительной и последующей обработки.

И, наконец, не стесняйтесь возвращать lambda для инкапсуляции внутренних механизмов. Вероятно, это лучший способ реализовать итератор ( "let over lambda" style.)

Надеюсь, вам это поможет.

Ответ 4

Личный вид:

Если вы параметрируете дизайн объекта в именах классов и их методах - как это можно сделать с шаблонами С++ - тогда вы получите нечто похожее на функциональный дизайн. Другими словами, функциональное программирование не создает бесполезных различий между подобными структурами, потому что их части имеют разные названия.

Моя экспозиция была Clojure, которая пытается украсть хороший бит из программирования объектов

  • работает с интерфейсами

отбрасывая извольные и бесполезные биты

  • Наследование бетона
  • скрытие традиционных данных.

Мнения различаются о том, насколько успешной была эта программа.

Так как Clojure выражается в Java (или каком-то эквиваленте), объекты могут не только делать то, что могут выполнять функции, но есть регулярное отображение от одного к другому.

Итак, где может быть любое функциональное преимущество? Я бы сказал, выразительность. Есть много повторяющихся вещей, которые вы делаете в программах, которые не стоит записывать на Java, - кто использовал лямбды, прежде чем Java предоставил для них компактный синтаксис? Но механизм всегда был там.

И у Lisps есть макросы, которые влияют на создание всех структур первого класса. И есть синергия между этими аспектами, которыми вы будете наслаждаться.