Что (точно) являются модулями "первого класса"?
Я часто читаю, что некоторые языки программирования поддерживают модули первого класса (OCaml, Scala, TypeScript[?]), И недавно наткнулся на ответ о SO, ссылаясь на модули в качестве первоклассных граждан среди отличительных особенностей Scala.
Я думал, что очень хорошо знаю, что означает модульное программирование, но после этих случаев я начинаю сомневаться в своем понимании...
Я думаю, что модули - это не что иное, как экземпляры определенных классов, которые действуют как мини-библиотеки. Код мини-библиотеки входит в класс, объекты этого класса являются модулями. Вы можете передать их как зависимости любому другому классу, которому требуются услуги, предоставляемые модулем, поэтому любой приличный OOPL имеет модули первого класса, но, очевидно, нет!
- Что именно представляет собой модуль? Чем он отличается, скажем, от простого класса или объекта?
- Как (1) связано (или нет) с модульным программированием, которое мы все знаем?
- Что именно означает для языка наличие модулей первого класса? Каковы преимущества? Каковы недостатки, если языки не имеют такой возможности?
4 ответа
Модуль, а также подпрограмма, это способ организации вашего кода. Когда мы разрабатываем программы, мы упаковываем инструкции в подпрограммы, подпрограммы в структуры, структуры в пакеты, библиотеки, сборки, инфраструктуры, решения и так далее. Итак, оставляя все остальное в стороне, это всего лишь механизм для организации вашего кода.
Основная причина, по которой мы используем все эти механизмы, а не просто выкладываем наши инструкции линейно, заключается в том, что сложность программы растет нелинейно по отношению к ее размеру. Другими словами, программа построена из n
кусочки, каждый из которых имеет m
инструкции легче понять, чем программа, которая построена из n*m
инструкции. Это, конечно, не всегда так (иначе мы можем просто разбить нашу программу на произвольные части и быть счастливыми). Фактически, чтобы это было правдой, мы должны ввести один существенный механизм, называемый абстракцией. Мы можем извлечь выгоду из разделения программы на управляемые части, только если каждая часть обеспечивает своего рода абстракцию. Например, мы можем иметь, connect_to_database
, query_for_students
, sort_by_grade
, а также take_the_first_n
абстракции упакованы как функции или подпрограммы, и гораздо проще понять код, который выражается в терминах этих абстракций, чем пытаться понять код, в котором встроены все эти функции.
Итак, теперь у нас есть функции, и естественно ввести следующий уровень организации - коллекции функций. Обычно видно, что некоторые функции создают семейства вокруг некоторой общей абстракции, например, student_name
, student_grade
, student_courses
и т. д., все они вращаются вокруг одной и той же абстракции student
, То же самое для connection_establish
, connection_close
и т. д. Поэтому нам нужен механизм, который свяжет эти функции. Здесь мы начинаем иметь варианты. Некоторые языки выбрали путь ООП, в котором объекты и классы являются единицами организации. Где набор функций и состояния называется объектом. Другие языки пошли другим путем и решили объединить функции в статические структуры, называемые модулями. Основное отличие состоит в том, что модуль представляет собой статическую структуру времени компиляции, где объекты - это структуры времени выполнения, которые должны быть созданы во время выполнения для использования. В результате, естественно, объекты имеют тенденцию содержать состояние, а модули - нет (и содержат только код). А объекты по своей природе являются обычными значениями, которые вы можете назначать переменным, хранить их в файлах и выполнять другие манипуляции, которые вы можете делать с данными. Классические модули, в отличие от объектов, не имеют представления времени выполнения, поэтому вы не можете передавать модули в качестве параметров своим функциям, сохранять их в списке и иным образом выполнять какие-либо вычисления для модулей. Это в основном то, что люди имеют в виду, говоря о первоклассном гражданине - способность относиться к сущности как к простой ценности.
Вернуться к составным программам. Чтобы сделать объекты / модули компонуемыми, мы должны быть уверены, что они создают абстракции. Для функций граница абстракции четко определена - это кортеж параметров. Для объектов у нас есть понятие интерфейсов и классов. Пока для модулей у нас есть только интерфейсы. Поскольку модули по своей природе более просты (они не включают в себя состояние), нам не нужно иметь дело с их конструированием и деконструкцией, поэтому нам не нужно более сложное понятие класса. И классы, и интерфейсы - это способ классификации объектов и модулей по некоторым критериям, так что мы можем рассуждать о разных модулях, не рассматривая реализацию, так же, как мы это делали с connect_to_database
, query_for_students
и др. функции - мы рассуждали о них только на основании их имени и интерфейса (и, вероятно, документации). Теперь мы можем иметь класс student
или модуль Student
оба определяют абстракцию, называемую студентом, так что мы можем сэкономить много умственных способностей, не сталкиваясь с тем, как эти студенты реализуются.
Помимо упрощения понимания наших программ, абстракции дают нам еще одно преимущество - обобщение. Поскольку нам не нужно рассуждать о реализации функции или модуля, это означает, что все реализации в некоторой степени взаимозаменяемы. Поэтому мы можем написать наши программы так, чтобы они выражали свое поведение в общем виде, не нарушая абстракций, а затем выбирали конкретные экземпляры при запуске наших программ. Объекты являются экземплярами времени выполнения, и по сути это означает, что мы можем выбрать нашу реализацию во время выполнения. Что приятно. Классы, однако, редко бывают первоклассными гражданами, поэтому нам приходится изобретать разные громоздкие методы для выбора, такие как шаблоны проектирования Abstract Factory и Builder. Для модулей ситуация еще хуже, так как они по своей природе являются структурой времени компиляции, мы должны выбрать нашу реализацию во время сборки / размещения программы. Что не то, что люди хотят делать в современном мире.
И вот появляются первоклассные модули, являющиеся объединением модулей и объектов, они дают нам лучшее из двух миров - легко рассуждать о структурах без гражданства, которые в то же время являются чистыми первоклассными гражданами, которых вы Можно сохранить в переменной, положить в список и выбрать нужную реализацию во время выполнения.
Говоря об OCaml, под капотом первоклассные модули - просто запись функций. В OCaml вы даже можете добавить состояние к первоклассному модулю, делая его практически неотличимым от объекта. Это подводит нас к другой теме - в реальном мире разделение между объектами и структурами не так очевидно. Например, OCaml предоставляет как модули, так и объекты, и вы можете помещать объекты в модули и даже наоборот. В C/C++ у нас есть модули компиляции, видимость символов, непрозрачные типы данных и файлы заголовков, которые позволяют выполнять какое-то модульное программирование, а также структуры и пространства имен. Поэтому разницу иногда трудно понять.
Поэтому подведу итоги. Модули представляют собой фрагменты кода с четко определенным интерфейсом для доступа к этому коду. Модули первого класса - это модули, которыми можно манипулировать как обычным значением, например хранить в структуре данных, назначать переменную и выбирать во время выполнения.
OCaml перспектива здесь.
Модули и классы очень разные.
Прежде всего, классы в OCaml являются очень специфической (и сложной) функцией. Чтобы вдаваться в детали, классы реализуют наследование, полиморфизм строк и динамическую диспетчеризацию (или виртуальные методы). Это позволяет им быть очень гибкими за счет некоторой эффективности.
Модули, однако, это совсем другое.
Действительно, вы можете видеть модули как атомарные мини-библиотеки, и обычно они используются для определения типа и его методов доступа, но они гораздо более мощные, чем просто.
- Модули позволяют создавать несколько типов, а также типы модулей и подмодули. В основном они позволяют создавать сложные компартментализации и абстракции.
- Функторы дают вам поведение, похожее на шаблоны C++. За исключением того, что они в безопасности. По сути, они являются функциями модулей, что позволяет параметризовать структуру данных или алгоритм по сравнению с другим модулем.
Модули обычно решаются статически, и поэтому их легко встроить, что позволяет писать понятный код, не опасаясь потери эффективности.
Теперь первоклассный гражданин - это сущность, которую можно поместить в переменную, передать функции и проверить на равенство. В некотором смысле это означает, что они будут оцениваться динамически.
Например, предположим, у вас есть модуль Jpeg
и модуль Png
которые позволяют вам манипулировать различными видами фото. Статически вы не знаете, какое изображение вам нужно отобразить. Таким образом, вы можете использовать первоклассные модули:
let get_img_type filename =
match Filename.extension filename with
| ".jpg" | ".jpeg" -> (module Jpeg : IMG_HANDLER)
| ".png" -> (module Png : IMG_HANDLER)
let display_img img_type filename =
let module Handler = (val img_type : IMG_HANDLER) in
Handler.display filename
Основные различия между модулем и объектом обычно
- Модули относятся ко второму классу, т. Е. Являются довольно статичными объектами, которые нельзя передавать в виде значений, в то время как объекты могут.
- Модули могут содержать типы и все другие формы объявлений (и типы могут быть сделаны абстрактными), тогда как объекты обычно не могут.
Однако, как вы заметили, есть языки, где модули могут быть упакованы как первоклассные значения (например, Ocaml), и есть языки, где объекты могут содержать типы (например, Scala). Это немного стирает черту. По-прежнему существуют различные отклонения в отношении определенных моделей, с различными компромиссами, сделанными в системах типов. Например, объекты сосредотачиваются на рекурсивных типах, в то время как модули сосредотачиваются на абстракции типов и допускают любое определение. Это очень сложная проблема - поддерживать оба одновременно без серьезных компромиссов, поскольку это быстро приводит к неразрешимой системе типов.
Как уже упоминалось, "модули", "классы" и "объекты" больше похожи на тенденции, чем на строгие формальные определения. И если вы реализуете модули, например, как объекты, как я понимаю, в Scala, то, очевидно, между ними нет принципиальных отличий, а в основном просто синтаксические различия, которые делают их более удобными для определенных вариантов использования.
Что касается конкретно OCaml, то вот практический пример того, что вы не можете делать с модулями, что вы можете делать с классами из-за фундаментальных различий в реализации:
Модули имеют функции, которые могут ссылаться друг на друга рекурсивно, используя rec
а также and
ключевое слово. Модуль также может "наследовать" реализацию другого модуля, используя include
и переопределить его определения. Например:
module Base = struct
let name = "base"
let print () = print_endline name
end
module Child = struct
include Base
let name = "child"
end
но поскольку модули связаны с ранними данными, то есть имена разрешаются во время компиляции, получить их невозможно Base.print
ссылаться Child.name
вместо Base.name
, По крайней мере, не изменяя оба Base
а также Child
значительно, чтобы явно включить его:
module AbstractBase(T : sig val name : string end) = struct
let name = T.name
let print () = print_endline name
end
module Base = struct
include AbstractBase(struct let name = "base" end)
end
module Child = struct
include AbstractBase(struct let name = "child" end)
end
С классами, с другой стороны, переопределение тривиально и по умолчанию:
class base = object(self)
method name = "base"
method print = print_endline self#name
end
class child = object
inherit base
method! name = "child"
end
Классы могут ссылаться на себя через переменную с условным именем this
или же self
(в OCaml вы можете назвать его как хотите, но self
это соглашение). Они также имеют позднюю привязку, что означает, что они разрешаются во время выполнения и поэтому могут вызывать реализации метода, которых не было, когда он был определен. Это называется открытой рекурсией.
Так почему же модули не связаны слишком поздно? В первую очередь по соображениям производительности, я думаю. Выполнение поиска в словаре по имени каждого вызова функции, несомненно, окажет значительное влияние на время выполнения.