Как решить, следует ли параметризовать на уровне типа или на уровне модуля при проектировании модулей?

я работаю над глубоким пониманием модулей 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 функтор, когда я не требуется ведение журнала. Теперь, кажется, я должен вернуться. и переписать эти модули имеют герметичный типа.

Итак, чтобы вернуться, расширить и (милосердно) закончить мой вопрос:

  1. есть ли способ указать подпись модулей, производимых StackFn так что они закончат как " специальные случаи"STACK?
  2. кроме того, есть ли способ написать подпись для PostFix модуль это позволило бы для обоих модулей, производимых StackFn и те, которые удовлетворить STACK?
  3. вообще говоря, есть ли способ думать об отношении между модули, которые помогут мне поймать / предвидеть такие вещи в будущем?

(если вы читали это далеко. Большое спасибо!)

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