Как чисто функциональные языки обрабатывают модульность?

23

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

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

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

Электрический кофе
источник
1
Почему вы думаете, что функционал не означает классы? Некоторые из первых классов пришли из LISP - CLOS Посмотрите на пространства имен clojure и типы или модули и haskell .
Я сослался на функциональные языки, которые не имеют классов, я очень хорошо осведомлен о тех немногих, которые ДЕЛАЮТ
Electric Coffee
1
Caml, например, его родственный язык OCaml добавляет объекты, но сам Caml их не имеет.
Электрический кофе
12
Термин «чисто функциональный» относится к функциональным языкам, которые поддерживают ссылочную прозрачность и не связаны с тем, имеет ли язык какие-либо объектно-ориентированные функции.
sepp2k
2
Торт ложь, повторное использование кода в ОО гораздо сложнее, чем в ФП. Несмотря на то, что ОО требовал многократного использования кода на протяжении многих лет, я видел, как он выполнялся минимум раз. (не стесняйтесь просто сказать, что я, должно быть, делаю это неправильно, я доволен тем, насколько хорошо я пишу ОО-код, так как я годами проектировал и обслуживал ОО-системы, я знаю качество своих собственных результатов)
Джимми Хоффа,

Ответы:

19

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

JavaScript является хорошим примером. В JavaScript функции используются как для реализации модулей, так и даже для объектно-ориентированной инкапсуляции. В Scheme, которая была основным источником вдохновения для JavaScript, есть только функции. Функции используются для реализации практически всего: объектов, модулей (называемых модулями в Racket), даже структур данных.

OTOH, Haskell и семейство ML имеют явную модульную систему.

Ориентация на объект - это абстракция данных. Вот и все. Модульность, Наследование, Полиморфизм, даже изменяемое состояние являются ортогональными проблемами.

Йорг Миттаг
источник
8
Не могли бы вы объяснить, как эти вещи работают, более подробно по отношению к oop? Вместо того, чтобы просто заявить, что понятия существуют ...
Electric Coffee
Sidenote - Модули и модули - это две разные конструкции в Racket - модули сопоставимы с пространствами имен, а модули являются своего рода посередине между пространствами имен и интерфейсами OO. В документах подробно рассматриваются различия
Джек,
@Jack: я не знал, что Racket также имеет концепцию под названием module. Я думаю, к сожалению, у Racket есть концепция, moduleкоторая называется не модулем, и концепция, которая является модулем, но не называется module. Как бы то ни было, вы написали: «модули - это нечто среднее между пространствами имен и интерфейсами OO». Ну, разве это не такое определение модуля?
Jörg W Mittag
Модули и единицы - это группы имен, связанных со значениями. Модули могут зависеть от других конкретных наборов привязок, в то время как модули могут зависеть от некоторого общего набора привязок, который должен обеспечивать любой другой код, использующий модуль. Единицы параметрируются через привязки, модули - нет. Модуль, зависящий от привязки, mapи модуль, зависящий от привязки map, отличаются тем, что модуль должен ссылаться на некоторую конкретную mapпривязку, например, от racket/base, в то время как разные пользователи блока могут давать разные определения mapдля блока.
Джек,
4

Похоже, вы задаете два вопроса: «Как достичь модульности в функциональных языках?» о чем говорилось в других ответах и ​​"как вы можете создавать абстракции в функциональных языках?" на что я отвечу.

В ОО-языках вы склонны концентрироваться на существительном, «животном», «почтовом сервере», «его садовой вилке» и т. Д. Функциональные языки, напротив, подчеркивают глагол «ходить», «получать почту» , "подталкивать" и т. д.

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

Другим конкретным примером этого является проект, над которым я работал не так давно. Я писал базу данных на Хаскеле. У меня был один «встроенный язык» для определения операций на самом низком уровне; например, это позволило мне писать и читать вещи с носителя. У меня был другой, отдельный «встроенный язык» для определения операций на самом высоком уровне. Тогда у меня был, по сути, переводчик, для преобразования операций с более высокого уровня на более низкий уровень.

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

dan_waterworth
источник
4

Хотя «функциональное программирование» не дает далеко идущих последствий для проблем модульности, определенные языки решают проблему программирования в целом по-разному. Повторное использование кода и абстракция взаимодействуют в том смысле, что чем меньше вы выставляете, тем сложнее повторно использовать код. Отложив абстракцию, я рассмотрю два вопроса повторного использования.

В статически типизированных языках ООП традиционно использовались номинальные подтипы, что означает, что код, разработанный для класса / модуля / интерфейса A, может иметь дело только с классом / модулем / интерфейсом B, когда B явно упоминает A. В языках семейства функционального программирования в основном используется структурный подтип, что означает этот код, разработанный для A, может обрабатывать B всякий раз, когда B имеет все методы и / или поля из A. B мог быть создан другой командой, прежде чем возникла необходимость в более общем классе / интерфейсе A. Например, в OCaml, структурный подтип применяется к модульной системе, ООП-подобной объектной системе и ее совершенно уникальным полиморфным вариантным типам.

Наиболее заметная разница между ООП и ФП по сравнению с Модульность заключается в том, что «единица» по умолчанию в ООП связывает воедино как объект различные операции над одним и тем же регистром значений, тогда как «единица» по умолчанию в ФП связывает воедино как функция одна и та же операция для различных случаев значений. В FP все еще очень легко объединять операции, например, в виде модулей. (Кстати, ни у Haskell, ни у F # нет полноценной системы модулей семейства ML.) Проблема выраженияэто задача постепенного добавления как новых операций, работающих со всеми значениями (например, присоединение нового метода к существующим объектам), так и новых случаев значений, которые должны поддерживать все операции (например, добавление нового класса с тем же интерфейсом). Как обсуждалось в первой лекции Ральфа Леммела ниже (которая содержит обширные примеры на C #), добавление новых операций проблематично в языках ООП.

Сочетание ООП и FP в Scala может сделать его одним из самых мощных языков. модульность. Но OCaml по-прежнему мой любимый язык, и, по моему личному, субъективному мнению, он не уступает Scala. В двух лекциях Ральфа Леммела ниже обсуждается решение проблемы выражения в Хаскеле. Я думаю, что это решение, хотя и прекрасно работает, затрудняет использование полученных данных с параметрическим полиморфизмом. Решение проблемы выражения с полиморфными вариантами в OCaml, объясненное в статье Jaques Garrigue, приведенной ниже, не имеет этого недостатка. Я также ссылаюсь на главы из учебников, в которых сравнивается использование не-ООП и ООП-модульности в OCaml.

Ниже приводятся ссылки, специфичные для Haskell и OCaml, на проблему выражения :

lukstafi
источник
2
Вы не могли бы объяснить больше о том, что делают эти ресурсы и почему вы рекомендуете их как ответы на заданный вопрос? «Ответы только на ссылки» не очень приветствуются на Stack Exchange
gnat
2
Я только что представил реальный ответ, а не просто ссылки, как редактирование.
lukstafi
0

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

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

Другой вид повторного использования кода - создание новых структур данных для работы с существующими функциями. Это обрабатывается на функциональных языках с использованием таких функций, как обобщенные типы и классы типов. Например, класс типов Ord в Haskell позволяет использовать sortфункцию для любого типа с Ordэкземпляром. Экземпляры легко создавать, если они еще не существуют.

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

Однако, когда дело доходит до деталей, все становится сложнее. AnimalОбъект естественно знает , какую пищу он ест, и сколько это необходимо для того , чтобы чувствовать себя полностью. Естественно, он не знает, где хранится еда и сколько ее имеется, поэтому FoodStoreобъект только что стал зависимым от каждого Animal, либо как поле Animalобъекта, либо передан в качестве параметра feedметода. С другой стороны, чтобы сохранить Animalкласс более сплоченным, вы можете перейти feed(animal)к FoodStoreобъекту, или вы можете создать мерзость класса, называемого AnimalFeederили что-то подобное.

В FP нет склонности к тому, чтобы поля Animalвсегда оставались сгруппированными, что имеет некоторые интересные последствия для повторного использования. Скажем , у вас есть список Animalзаписей, с такими областями , как name, species, location, food type, food amountи т.д. У вас также есть список FoodStoreзаписей с полями , как location, food typeи food amount.

Первым шагом в кормлении может быть сопоставление каждого из этих списков записей со списками (food amount, food type)пар с отрицательными числами для количества животных. Затем вы можете создавать функции для выполнения всех этих действий с этими парами, например, суммировать количество продуктов каждого типа. Эти функции не принадлежат ни к одному, Animalни к FoodStoreмодулю, но их можно использовать многократно.

В итоге вы получаете кучу функций, которые делают полезные вещи, [(Num A, Eq B)]которые можно использовать многократно и модульно, но у вас не получается понять, где их разместить или как их называть группой. В результате модули FP сложнее классифицировать, но классификация гораздо менее важна.

Карл Билефельдт
источник
-1

Одно из популярных решений - разбить код на модули, вот как это делается в JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

Статья полностью объяснить эту модель в JavaScript , кроме этого есть множество других способов определить модуль, например RequireJS , CommonJS , Google Closure. Другим примером является Erlang, где у вас есть как модули, так и поведения, которые обеспечивают API и шаблон, играющие роль, аналогичную интерфейсам в ООП.

Давид Сергей
источник