Что (точно) являются модулями "первого класса"? - PullRequest
5 голосов
/ 13 июня 2019

Я часто читаю, что некоторые языки программирования поддерживают модули первого класса (OCaml, Scala, TypeScript [?]), И недавно наткнулся на ответ по SO, в котором цитируются модули в качестве граждан первого класса среди различающих особенности Scala.

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

Я думаю, что модули - это не что иное, как экземпляры определенных классов, которые действуют как мини-библиотеки Код мини-библиотеки входит в класс, объекты этого класса являются модулями. Вы можете передавать их как зависимости любому другому классу, которому требуются услуги, предоставляемые модулем , поэтому любой приличный OOPL имеет модули первого класса, но, очевидно, нет!

  1. Какой точно является модулем? Чем он отличается, скажем, от простого класса или объекта?
  2. Как (1) связано (или нет) с модульным программированием, которое мы все знаем?
  3. Что именно означает для языка наличие модулей первого класса ? Каковы преимущества? Каковы недостатки, если в языках нет такой функции?

Ответы [ 4 ]

6 голосов
/ 13 июня 2019

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

Основная причина, по которой мы используем все эти механизмы, вместо того, чтобы просто выкладывать наши инструкции линейно, заключается в том, что сложность программы возрастает нелинейно по отношению к ее размеру. Другими словами, программу, составленную из 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 и т. Д. Поэтому нам нужен механизм, который свяжет эти функции. Здесь мы начинаем иметь варианты. Некоторые языки выбрали путь ООП, в котором объекты и классы являются единицами организации. Где набор функций и состояния называется объектом. Другие языки пошли другим путем и решили объединить функции в статические структуры, называемые modules . Основное отличие состоит в том, что модуль представляет собой статическую структуру времени компиляции, где объекты - это структуры времени выполнения, которые должны быть созданы во время выполнения для использования. В результате, естественно, объекты имеют тенденцию содержать состояние, а модули - нет (и содержат только код). А объекты по своей природе являются обычными значениями, которые вы можете назначать переменным, хранить их в файлах и выполнять другие манипуляции, которые вы можете делать с данными. Классические модули, в отличие от объектов, не имеют представления времени выполнения, поэтому вы не можете передавать модули в качестве параметров своим функциям, сохранять их в списке и иным образом выполнять какие-либо вычисления для модулей. Это в основном то, что люди имеют в виду, говоря гражданин первого класса - способность рассматривать сущность как простую ценность.

Вернуться к составным программам. Чтобы сделать объекты / модули компонуемыми, мы должны быть уверены, что они создают абстракции. Для функций граница абстракции четко определена - это кортеж параметров. Для объектов у нас есть понятие интерфейсов и классов. Пока для модулей у нас есть только интерфейсы. Поскольку модули по своей природе более просты (они не включают в себя состояние), нам не нужно иметь дело с их конструированием и деконструкцией, поэтому нам не нужно более сложное понятие класса . И классы, и интерфейсы - это способ классификации объектов и модулей по некоторым критериям, так что мы можем рассуждать о разных модулях, не обращая внимания на реализацию, так же, как мы делали с connect_to_database, query_for_students и другими функциями - мы были рассуждения о них только на основании их имени и интерфейса (и, вероятно, документации). Теперь у нас может быть класс student или модуль Student, которые определяют абстракцию, называемую студентом, так что мы можем сэкономить много умственных способностей, не прибегая к тому, как эти студенты реализуются.

И, помимо упрощения понимания наших программ, абстракции дают нам еще одно преимущество - обобщение . Поскольку нам не нужно рассуждать о реализации функции или модуля, это означает, что все реализации в некоторой степени взаимозаменяемы. Поэтому мы можем написать наши программы так, чтобы они выражали свое поведение в общем виде, не нарушая абстракций, а затем выбирали конкретные экземпляры при запуске наших программ. Объекты являются экземплярами времени выполнения, и по сути это означает, что мы можем выбрать нашу реализацию во время выполнения. Что приятно. Классы, однако, редко бывают первоклассными гражданами, поэтому нам приходится изобретать разные громоздкие методы для выбора, такие как шаблоны проектирования Abstract Factory и Builder. Для модулей ситуация еще хуже, так как они по своей природе являются структурой времени компиляции, мы должны выбрать нашу реализацию во время сборки / размещения программы. Что не то, что люди хотят делать в современном мире.

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

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

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

4 голосов
/ 13 июня 2019

Перспектива 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
3 голосов
/ 16 июня 2019

Основные различия между модулем и объектом обычно составляют

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

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

1 голос
/ 16 июня 2019

Как уже упоминалось, «модули», «классы» и «объекты» больше похожи на тенденции, чем на строгие формальные определения. И если вы реализуете модули, например, как объекты, как я понимаю, в 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 - это соглашение). Они также имеют позднюю привязку, что означает, что они разрешаются во время выполнения и поэтому могут вызывать реализации метода, которых не было, когда он был определен. Это называется открытой рекурсией.

Так почему же модули не связаны так поздно? В первую очередь по соображениям производительности, я думаю. Поиск в словаре по имени каждого вызова функции, несомненно, окажет значительное влияние на время выполнения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...