Как решить, следует ли параметризовать на уровне типа или на уровне модуля при проектировании модулей?
я работаю над глубоким пониманием модулей ML-стиля: я думаю, что концепция важна, и мне нравится, какое мышление они поощряют. Я просто теперь обнаружение напряжения, которое может возникнуть между параметрическими типами и параметрические модули. Я ищу инструменты для размышлений о том, что будет помогите мне принимать умные дизайнерские решения, когда я создаю свои программы.
кулак я постараюсь описать свой вопрос в целом. Тогда я предоставлю конкретный пример из учебный проект, над которым я работаю. Наконец, я вернитесь к общему вопросу, чтобы подвести его к сути.
(извините, что я еще не знаю достаточно, чтобы поставить этот вопрос более лаконично.)
в общих чертах, напряжение, которое я обнаружил, таково: функции наиболее гибкий, и открытый к самому широкому повторному использованию, когда мы обеспечиваем их с параметрическим тип подписи (в соответствующих случаях). Однако модули являются наиболее гибкими и открытыми к самому широкому повторному использованию когда мы запечатываем параметризация функций внутри модуль, а вместо этого параметризовать весь модуль по заданному типу.
готовый пример этой разницы можно найти в сравнении модулей, которые
реализовать LIST
подпись с теми, что реализуют
ORD_SET
. Модуль List:LIST
предоставляет кучу полезных функций,
параметризованный на любом типе. После того, как мы определили или загрузили , мы можем
легко применить любую из функций, которые она предоставляет строить, манипулировать, или
изучите списки любого типа. Например, если мы работаем с обеими строками и
целые числа, мы можем использовать один и тот же модуль для построения и управления
значения обоих типов:
val strList = List.@ (["a","b"], ["c","d"])
val intList = List.@ ([1,2,3,4], [5,6,7,8])
С другой стороны, если мы хотим иметь дело с упорядоченными множествами, вопросы разные:
упорядоченные множества требуют, чтобы отношение упорядочения сохранялось над всеми их элементами,
и не может быть ни одной конкретной функции compare : 'a * 'a -> order
производить это отношение для каждого типа. Следовательно, мы требовать другое
удовлетворяя модуль ORD_SET
подпись для каждого типа, который мы хотим поместить в
упорядоченное множество. Таким образом, для построения или управления упорядоченными наборами строк
и целых чисел, мы должны реализовать различные модули для каждого типа[1]:
structure IntOrdSet = BinarySetFn ( type ord_key = int
val compare = Int.compare )
structure StrOrdSet = BinarySetFn ( type ord_key = string
val compare = String.compare )
и мы должны затем использовать функцию подгонки из соответствующего модуля, когда мы пожелайте работать на данном типе:
val strSet = StrOrdSet.fromList ["a","b","c"]
val intSet = IntOrdSet.fromList [1,2,3,4,5,6]
здесь есть довольно простой компромисс:LIST
модули обеспечивают функции
этот диапазон по любому типу вам нравится, но они не могут воспользоваться любыми отношениями
которые удерживаются между значениями любого конкретного типа;ORD_SET
модули обеспечивают
функции, которые обязательно ограничены типом, указанным в функторах
параметр, но через ту же параметризацию, они способны
включение конкретной информации о внутренней структуре и отношениях
их типов.
легко представить случаи, когда мы хотим создание альтернативной семьи модулей списка, используя функторы для параметризации типов и других значений к предоставление типов данных, подобных списку, с более сложной структурой: например, для указания тип данных для упорядоченного списка или для представления списков с помощью самобалансирующегося двоичного файла дерево поиска.
при создании модуля, я думаю, также довольно легко распознать, когда он будет уметь предоставлять полиморфные функции и когда это нужно будет параметризовать на некоторых типах. Что кажется больше для меня сложно понять, что именно. модули, от которых вы должны зависеть при работе над чем-то дальше по потоку.
в общих чертах, мой вопрос таков: когда я проектирую систему различные связанные модули, как я могу понять, следует ли проектировать вокруг модулей обеспечение полиморфных функций или модулей, генерируемых с помощью функторов параметризовано по типам и значениям?
я надеюсь проиллюстрировать дилемму и почему это имеет значение со следующим образец, взято из игрушечного проекта, над которым я работаю.
у меня есть functor PostFix (ST:STACK) : CALCULATOR_SYNTAX
. Это требует
реализация структуры данных стека и создает парсер, который считывает
конкретный постфикс ("обратный польский") нотация в абстрактный синтаксис (быть
оценивается модулем калькулятора вниз по потоку), и наоборот. Теперь я был
использование стандартного интерфейса стека, который предоставляет полиморфный тип стека и
количество функций, для работы с:
signature STACK =
sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
end
это работает отлично, и дает мне некоторую гибкость, так как я могу использовать стек на основе списка
или векторный стек, или что угодно. Но, скажем, я хочу добавить простое ведение журнала
функция к модулю стога, так, что каждый раз элемент будет нажат К, или
выскочив из стопки, он распечатывает текущее состояние стопки. Теперь я буду
нужен fun toString : 'a -> string
для типа, собранного стеком, и
это, как я понимаю, не может быть включено в STACK
модуль. Теперь Я
нужно загерметизировать тип в модуль, и parameterize модуль по типу
собраны в стопку и a toString
функция, которая позволит мне произвести
печатное представление собранного типа. Поэтому мне нужно что-то вроде
functor StackFn (type t
val toString: t -> string ) =
struct
...
end
и это не создайте модуль, соответствующий STACK
подпись, так как это
не обеспечивает полиморфный тип. Таким образом, я должен изменить подпись
для PostFix
функтор. Если у меня есть много других модулей, я должен изменить
и все они тоже. Это может быть неудобно, но настоящая проблема в том, что я
больше не может использовать мой простой список на основе или вектор на основе STACK
модули в
PostFix
функтор, когда я не требуется ведение журнала. Теперь, кажется, я должен вернуться.
и переписать эти модули имеют герметичный типа.
Итак, чтобы вернуться, расширить и (милосердно) закончить мой вопрос:
- есть ли способ указать подпись модулей, производимых
StackFn
так что они закончат как " специальные случаи"STACK
? - кроме того, есть ли способ написать подпись для
PostFix
модуль это позволило бы для обоих модулей, производимыхStackFn
и те, которые удовлетворитьSTACK
? - вообще говоря, есть ли способ думать об отношении между модули, которые помогут мне поймать / предвидеть такие вещи в будущем?
(если вы читали это далеко. Большое спасибо!)
1 ответов
Как вы обнаружили, существует напряжение между параметрическим полиморфизмом и функторами/модулем в SML и OCaml. Это в основном из-за "два языка" природы модулей и отсутствия специального полиморфизма. 1 мл и модульная неявные преобразования предлагают различные решения этой проблемы. Во-первых, объединяя два вида параметризма, позже, позволяя искрить некоторый ad-hoc полиморфизм, когда это необходимо.
вернуться к практике рассмотрения. С функторами довольно легко (но многословно/раздражает) мономорфизировать данную структуру данных. Вот пример (В вида OCaml). При этом вы все равно можете писать общие реализации и специализировать их позже (предоставляя функцию печати).
module type POLYSTACK = sig
type 'a stack
exception EmptyStack
val empty : 'a stack
val isEmpty : 'a stack -> bool
val push : ('a * 'a stack) -> 'a stack
val pop : 'a stack -> 'a stack
val top : 'a stack -> 'a
val popTop : 'a stack -> 'a stack * 'a
val toString : ('a -> string) -> 'a stack -> string
end
module type STACK = sig
type elt
type t
exception EmptyStack
val empty : t
val isEmpty : t -> bool
val push : (elt * t) -> t
val pop : t -> t
val top : t -> elt
val popTop : t -> t * elt
val toString : t -> string
end
module type PRINTABLE = sig
type t
val toString : t -> string
end
module Make (E : PRINTABLE) (S : POLYSTACK)
: STACK with type elt = E.t and type t = E.t S.stack
= struct
type elt = E.t
type t = E.t S.stack
include S
let toString = S.toString E.toString
end
module AddLogging (S : STACK)
: STACK with type elt = S.elt and type t = S.t
= struct
include S
let push (x, s) =
let s' = S.push (x, s) in print_string (toString s') ; s'
end