Как, почему и когда использовать шаблон ".Internal"?
Я видел пару пакетов на hackage, которые содержат имена модулей с .Internal
в качестве своего последнего компонента (например, Data.ByteString.Internal
)
Эти модули, как правило, не могут быть просмотрены в браузере (но они все равно могут отображаться) в Haddock и не должны использоваться клиентским кодом, но содержат определения, которые либо реэкспортируются из открытых модулей, либо просто используются внутри.
Теперь мой вопрос к этому шаблону организации библиотеки:
- Какие проблемы решаются этими модулями
.Internal
?
- Есть ли другие предпочтительные способы решения этих проблем?
- Какие определения следует перенести в те модули
.Internal
?
- Какая рекомендуемая практика в отношении организации библиотек с помощью таких модулей
.Internal
?
Ответы
Ответ 1
Internal
модули, как правило, представляют собой модули, которые выставляют внутренние компоненты пакета, которые разрушают инкапсуляцию пакетов.
Чтобы взять ByteString
в качестве примера: когда вы обычно используете ByteString
s, они используются как непрозрачные типы данных; Значение a ByteString
является атомарным, и его представление неинтересно. Все функции из Data.ByteString
принимают значения ByteString
и никогда не сырые Ptr CChar
или что-то еще.
Это хорошо. это означает, что авторам ByteString
удалось сделать представление достаточно абстрактным, чтобы все детали о ByteString
могли быть полностью скрыты от пользователя. Такая конструкция приводит к инкапсуляции функциональности.
Модули Internal
предназначены для людей, которые хотят работать с внутренними элементами инкапсулированной концепции, чтобы расширить инкапсуляцию.
Например, вы можете создать новый тип данных BitString
, и вы хотите, чтобы пользователи могли конвертировать ByteString
в BitString
без копирования какой-либо памяти. Чтобы сделать это, вы не можете использовать opaque ByteString
s, потому что это не дает вам доступ к памяти, которая представляет ByteString
. Вам нужен доступ к необработанному указателю памяти к байтовым данным. Это то, что обеспечивает модуль Internal
для ByteString
.
Затем вы должны сделать свой тип данных BitString
инкапсулированным, тем самым расширяя инкапсуляцию, не нарушая ее. Затем вы можете предоставить свой собственный модуль BitString.Internal
, подвергая внутренности вашего типа данных пользователям, которые могут захотеть проверить его представление по очереди.
Если кто-то не предоставляет модуль Internal
(или аналогичный), вы не можете получить доступ к внутреннему представлению модуля, а пользовательская запись, например. BitString
вынужден (ab) использовать такие вещи, как unsafeCoerce
, чтобы накладывать указатели на память, и все становится некрасиво.
Определения, которые должны быть помещены в модуль Internal
, являются фактическими объявлениями данных для ваших типов данных:
module Bla.Internal where
data Bla = Blu Int | Bli String
-- ...
module Bla (Bla, makeBla) where -- ONLY export the Bla type, not the constructors
import Bla.Internal
makeBla :: String -> Bla -- Some function only dealing with the opaque type
makeBla = undefined
Ответ 2
@dflemstr прав, но не явным образом о следующем. Некоторые авторы помещают внутреннюю часть пакета в модуль .Internal
, а затем не выставляют этот модуль через cabal, тем самым делая его недоступным для клиентского кода. Это плохая вещь 1.
Открытые модули .Internal
помогают сообщать различные уровни абстракции, реализованные модулем. Альтернативы:
- Экспортировать детали реализации в том же модуле, что и абстракция.
- Скрыть детали реализации, не подвергая их экспорту модулей или через cabal.
(1) делает документацию запутанной и затрудняет пользователю рассказывать о переходе между его кодом, относящимся к абстракции модуля и его разбиению. Этот переход важен: он аналогичен удалению параметра функции и замене ее вхождений на константу, потерю общности.
(2) делает вышеуказанный переход невозможным, а препятствует повторному использованию кода. Мы хотели бы сделать наш код как можно более абстрактным, но (см. Einstein) не более того, и у автора модуля не так много информации, как у пользователя модуля, поэтому он не в состоянии решить, какой код должен быть недоступным, См. Ссылку больше для этого аргумента, так как это несколько своеобразно и противоречиво.
Экспозиция модулей .Internal
обеспечивает счастливую среду, которая связывает барьер абстракции, не применяя ее, позволяя пользователям легко ограничиться абстрактным кодом, но позволяя им "бета-расширять" модуль, если абстракция ломается или неполна.
1 Есть, конечно, осложнения для этого пуристского суждения. Внутреннее изменение теперь может разорвать клиентский код, и теперь у авторов больше обязательств по стабилизации их реализации, а также их интерфейса. Даже если он должным образом отвергнут, пользователи являются пользователями и получают поддержку, поэтому есть призыв к скрытию внутренних компонентов. Он запрашивает политику пользовательской версии, которая различает .Internal
и изменения интерфейса, но, к счастью, это согласуется с (но не явным) политикой управления версиями . "Реальный код" также известен как ленивый, поэтому разоблачение модуля .Internal
может обеспечить простоту, когда существует абстрактный способ определения кода, который был просто "сложнее" (но в конечном итоге поддерживает повторное использование сообщества). Он также может препятствовать отчетности об упущении в абстрактном интерфейсе, который действительно должен быть отброшен автору для исправления.
Ответ 3
Идея состоит в том, что вы можете иметь "правильный", стабильный API, который вы экспортируете из MyModule
, и это предпочтительный и документированный способ использования библиотеки.
В дополнение к общедоступному API ваш модуль, вероятно, имеет частные конструкторы данных и внутренние вспомогательные функции и т.д. Подмодуль MyModule.Internal
может использоваться для экспорта этих внутренних функций, а не для их полной блокировки внутри модуля.
- Это позволяет пользователям вашей библиотеки получать доступ к внутренним компонентам, если они нуждаются в том, чего вы не ожидали, но при том понимании, что они обращаются к внутреннему API, который не имеет таких же скрытых гарантий, как и общедоступный.
- Он позволяет вам обращаться к внутренним функциям и конструкторам, например. единицы тестирования.
Ответ 4
Одно расширение (или, возможно, разъяснение) на то, что сказал shang и dflemstr: если у вас есть внутренние определения (типы данных, конструкторы которых не экспортируются и т.д.), которые вы хотите получить из нескольких экспортируемых модулей, тогда вы обычно создайте такой модуль .Internal
, который вообще не отображается (т.е. указан в Other-Modules
в файле .cabal
).
Однако иногда это происходит при выполнении типов в ghci (например, при использовании функции, но где некоторые из типов, к которым она относится, не входят в область видимости), не может думать о экземпляре, где это происходит в верхней части моя голова, но это так).