API и функциональное программирование

15

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

Например, вот одна из самых известных цитат из Rich Hickey из Clojure, в интервью по этому вопросу :

Fogus: Следуя этой идее, некоторые люди удивляются тому факту, что Clojure не занимается скрытием данных в своих типах. Почему вы решили отказаться от сокрытия данных?

Хикки: Давайте проясним, что Clojure сильно подчеркивает программирование на абстракции. Однако в какой-то момент кому-то понадобится доступ к данным. И если у вас есть понятие «частный», вам нужны соответствующие понятия о привилегиях и доверии. И это добавляет целую тонну сложности и небольшой ценности, создает жесткость в системе и часто заставляет вещи жить там, где им не следует. Это в дополнение к другой потере, которая происходит, когда простая информация помещается в классы. В той степени, в которой данные являются неизменяемыми, от предоставления доступа может быть мало вреда, кроме того, что кто-то может зависеть от того, что может измениться. Ну, ладно, люди делают это все время в реальной жизни, и когда все меняется, они адаптируются. И если они рациональны, они знают, когда принимают решение, основанное на чем-то, что может измениться, и что им может понадобиться адаптироваться в будущем. Итак, это решение по управлению рисками, которое, я думаю, программисты должны иметь право принимать. Если у людей нет чувства желания программировать на абстракции и опасаться сочетать детали реализации, то они никогда не станут хорошими программистами.

Исходя из мира ОО, это, кажется, усложняет некоторые из закрепленных принципов, которым я научился за эти годы. Среди них: сокрытие информации, закон Деметры и принцип единообразного доступа. Общий поток, заключающийся в том, что инкапсуляция позволяет нам определять API, чтобы другие знали, что им следует и чего не следует касаться. По сути, создание контракта, который позволяет сопровождающему некоторого кода свободно вносить изменения и рефакторинги, не беспокоясь о том, как он может вносить ошибки в код потребителя (принцип Open / Closed). Он также предоставляет чистый, кураторский интерфейс для других программистов, чтобы узнать, какие инструменты они могут использовать для получения или создания этих данных.

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

Как функциональные кодовые базы решают эти проблемы, которые, кажется, возникают, когда размер кодовой базы становится огромным, так что необходимо определить API-интерфейсы, а многие разработчики вовлечены в работу с конкретными частями системы? Существуют ли примеры такой ситуации, демонстрирующие, как это обрабатывается в кодовых базах такого типа?

jameslk
источник
2
Вы можете определить формальный интерфейс без понятия объектов. Просто создайте функцию интерфейса, документируя их. Не предоставляйте документацию для деталей реализации. Вы только что создали интерфейс.
Scara95
@ Scara95 Разве это не значит, что мне приходится работать над реализацией кода для интерфейса и писать достаточно документации, чтобы предупредить потребителя, что делать, а что - нет? Что если код изменится и документация устареет? По этой причине я обычно предпочитаю самодокументированный код.
jameslk
Вы должны документировать интерфейс в любом случае.
Scara95
3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.На самом деле, нет. Единственное, что меняется, это то, что изменения заканчиваются на новом объекте. Это огромная победа, когда дело доходит до рассуждений о коде; Передача изменяемых объектов означает необходимость отслеживать, кто может их видоизменить, и эта проблема возрастает в зависимости от размера кода.
Довал

Ответы:

10

Прежде всего, я перейду ко вторым комментариям Себастьяна о том, что функционально правильно, что такое динамическая типизация. В более общем смысле, Clojure - это разновидность функционального языка и сообщества, и вам не следует обобщать слишком много, основываясь на нем. Я сделаю несколько замечаний с точки зрения ML / Haskell.

Как упоминает Базиль, концепция управления доступом существует в ML / Haskell и часто используется. «Факторинг» немного отличается от традиционных языков ООП; в ООП понятие класса играет одновременно роль типа и модуля , тогда как функциональные (и традиционные процедурные) языки рассматривают их ортогонально.

Другой момент заключается в том, что ML / Haskell очень тяготеют к генерикам со стиранием типов, и что это может быть использовано для обеспечения другого вида «скрытия информации», чем инкапсуляция ООП. Когда компонент знает только тип элемента данных в качестве параметра типа, этому компоненту можно безопасно передавать значения этого типа, и тем не менее он будет лишен возможности делать с ними много, потому что он не знает и не может знать их конкретный тип ( instanceofна этих языках нет универсального или динамического приведения). Эта запись в блоге является одним из моих любимых вводных примеров этих методов.

Далее: в мире FP очень часто используются прозрачные структуры данных в качестве интерфейсов для непрозрачных / инкапсулированных компонентов. Например, шаблоны интерпретатора очень распространены в FP, где структуры данных используются в качестве синтаксических деревьев, которые описывают логику, и передаются в код, который их «выполняет». Правильно сказано, что состояние существует эфемерно, когда работает интерпретатор, который использует структуры данных. Также реализация интерпретатора может изменяться, пока он все еще связывается с клиентами в терминах одних и тех же типов данных.

Последнее и самое длинное: инкапсуляция / сокрытие информации - это техника , а не цель. Давайте немного подумаем о том, что он предоставляет. Инкапсуляция - это метод согласования контракта и реализации программного блока. Типичная ситуация такова: реализация системы допускает значения или состояния, которые, согласно ее контракту, не должны существовать.

Если вы посмотрите на это таким образом, мы можем указать, что FP предоставляет, помимо инкапсуляции, ряд дополнительных инструментов, которые можно использовать с той же целью:

  1. Неизменность как распространенное значение по умолчанию. Вы можете передать прозрачные значения данных стороннему коду. Они не могут изменять их и переводить в недопустимые состояния. (Ответ Карла подчеркивает это.)
  2. Сложные системы типов с алгебраическими типами данных, которые позволяют вам точно контролировать структуру ваших типов без написания большого количества кода. Разумно используя эти средства, вы часто можете создавать типы, в которых «плохие состояния» просто невозможны. (Слоган: «Делать недопустимые состояния непредставимыми». ) Вместо того, чтобы использовать инкапсуляцию для косвенного управления набором допустимых состояний класса, я бы лучше просто сказал компилятору, что это такое, и пусть это гарантирует их мне!
  3. Шаблон интерпретатора, как уже упоминалось. Один из ключей к разработке хорошего абстрактного синтаксического дерева - это:
    • Попробуйте разработать тип данных абстрактного синтаксического дерева, чтобы все значения были «допустимыми».
    • В противном случае заставьте интерпретатора явно обнаружить недопустимые комбинации и полностью отклонить их.

Эта серия F # "Проектирование с типами" делает довольно приличное чтение по некоторым из этих тем, в частности, № 2. (Вот откуда взялась ссылка «сделать недопустимые недопустимые состояния состояний» выше.) Если вы посмотрите внимательно, вы заметите, что во второй части они демонстрируют, как использовать инкапсуляцию, чтобы скрыть конструкторы и не дать клиентам создавать недопустимые экземпляры. Как я уже говорил выше, это является частью инструментария!

sacundim
источник
9

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

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

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

Карл Билефельдт
источник
Я хотел бы добавить одну вещь: неизменяемая переменная заставляет программистов придерживаться распределенной и разбросанной структуры данных, если такая структура вообще существует. Все данные структурированы так, чтобы создать логическую группу для удобного обнаружения и прохождения, а не для транспортировки. Это логическая последовательность действий, которую вы совершите, как только сделаете достаточно функционального программирования.
Xephon
8

Тенденция Clojure просто использовать хеши и примитивы, на мой взгляд, не является частью его функционального наследия, но является частью его динамического наследия. Я видел похожие тенденции в Python и Ruby (как объектно-ориентированные, так и императивные и динамические, хотя обе имеют довольно хорошую поддержку функций высшего порядка), но не в, скажем, Haskell (который статически типизирован, но чисто функциональный , со специальными конструкциями, необходимыми для избежания неизменности).

Поэтому вопрос, который вам нужно задать, заключается не в том, как функциональные языки обрабатывают большие API, а в том, как это делают динамические языки. Ответ: хорошая документация и много-много юнит-тестов. К счастью, современные динамические языки обычно имеют очень хорошую поддержку для обоих; например, и Python, и Clojure имеют способ встраивания документации в сам код, а не только в комментарии.

Себастьян Редл
источник
Что касается статически типизированных (чисто) функциональных языков, то нет (простого) способа передачи функции с типом данных, как в ОО-программировании. Так что документация имеет значение в любом случае. Дело в том, что вам не нужна языковая поддержка для определения интерфейса.
Scara95
5
@ Scara95 Можете ли вы уточнить, что вы подразумеваете под «переносить функцию с типом данных»?
Себастьян Редл
6

Некоторые функциональные языки дают возможность инкапсулировать или скрывать детали реализации в абстрактных типах данных и модулях .

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

Василий Старынкевич
источник