Підтипи як підмножини типів даних SML


10

Однією з небагатьох речей, які мені не подобаються у книзі Окасакі про суто функціональні структури даних, є те, що його код засмічений невичерпним узгодженням шаблону. Як приклад, я наведу його реалізацію черг у режимі реального часу (відновлено для усунення непотрібних призупинень):

infixr 5 :::

datatype 'a stream = Nil | ::: of 'a * 'a stream lazy

structure RealTimeQueue :> QUEUE =
struct
  (* front stream, rear list, schedule stream *)
  type 'a queue = 'a stream * 'a list * 'a stream

  (* the front stream is one element shorter than the rear list *)
  fun rotate (x ::: $xs, y :: ys, zs) = x ::: $rotate (xs, ys, y ::: $zs)
    | rotate (Nil, y :: nil, zs) = y ::: $zs

  fun exec (xs, ys, _ ::: $zs) = (xs, ys, zs)
    | exec args = let val xs = rotate args in (xs, nil, xs) end

  (* public operations *)
  val empty = (Nil, nil, Nil)
  fun snoc ((xs, ys, zs), y) = exec (xs, y :: ys, zs)
  fun uncons (x ::: $xs, ys, zs) = SOME (x, exec (xs, ys, zs))
    | uncons _ = NONE
end

Як видно rotate, не є вичерпним, оскільки він не охоплює випадок, коли задній список порожній. Більшість стандартних реалізацій ML будуть генерувати попередження про це. Ми знаємо, що задній список не може бути порожнім, тому rotateщо попередньою умовою є те, що задній список на один елемент довший, ніж передній потік. Але перевірка типу не знає - і, можливо, не може знати, тому що цей факт є невимовними в системі типів ML.

Зараз моє рішення придушити це попередження - це такий неелегантний злом:

  fun rotate (x ::: $xs, y :: ys, zs) = x ::: $rotate (xs, ys, y ::: $zs)
    | rotate (_, ys, zs) = foldl (fn (x, xs) => x ::: $xs) zs ys

Але те, що я дійсно хочу, - це система типів, яка може зрозуміти, що не кожна трійка є вагомим аргументом rotate. Я хотів би, щоб система типів дозволила мені визначати такі типи:

type 'a triplet = 'a stream * 'a list * 'a stream

subtype 'a queue of 'a triplet
  = (Nil, nil, Nil)
  | (xs, ys, zs) : 'a queue => (_ ::: $xs, _ :: ys, zs)
  | (xs, ys, zs) : 'a queue => (_ ::: $xs, ys, _ ::: $zs)

А потім зробити висновок:

subtype 'a rotatable of 'a triplet
  = (xs, ys, _) : 'a rotatable => (_ ::: $xs, _ :: ys, _)
  | (Nil, y :: nil, _)

subtype 'a executable of 'a triplet
  = (xs, ys, zs) : 'a queue => (xs, ys, _ ::: $zs)
  | (xs, ys, Nil) : 'a rotatable => (xs, ys, Nil)

val rotate : 'a rotatable -> 'a stream
val exec : 'a executable -> 'a queue

Однак я не хочу повномасштабних залежних типів, або навіть GADT, або будь-яких інших шалених речей, які використовують деякі програмісти. Я просто хочу визначити підтипи, «вирізаючи» індуктивно визначені підмножини існуючих типів ML. Це можливо?

Відповіді:


20

Такі типи - де ви визначаєте підтип (в основному), даючи граматику прийнятних значень - називаються уточненнями даних вибору .

  • Їх представили Тім Фріман та Френк Пфеннінг у своєму документі PLDI 1991, Види доопрацювання для ML .

  • У своїй докторській дисертації Роуан Девіс вивчав умовиводи для типів вдосконалення . Він також реалізував це як розширення до SML, але я не знаю, чи він доступний в Інтернеті.

  • Джошуа Данфілд вивчав, як поєднувати уточнення даних в асортименті з більш привабливою залежністю у своїй дисертації "Єдина система уточнення типів" . Він також реалізував це мовою Stardust , доступною в Інтернеті:

    http://www.mpi-sws.org/~joshua/stardust/


3
Реалізація Rowan Davies доступна тут: github.com/rowandavies/sml-cidre
Noam Zeilberger

1

Я можу використовувати GADT, TypeFamilies, DataKinds та TypeOperators (лише для естетики) та створювати те, що вам потрібно:

data Term0 varb lamb letb where
    Lam :: lamb -> Term0 varb lamb letb -> Term0 varb lamb letb
    Let :: letb -> Term0 varb lamb letb -> Term0 varb lamb letb -> Term0 varb lamb letb
    Var :: varb -> Term0 varb lamb letb
    App :: Term0 varb lamb letb -> Term0 varb lamb letb -> Term0 varb lamb letb

type Term b = Term0 b b b

data Terms = Lets | Lams | Vars

type family  t /// (ty :: Terms) where
    Term0 a b c /// Vars = Term0 Void b c
    Term0 a b c /// Lams = Term0 a Void c
    Term0 a b c /// Lets = Term0 a b Void

Now, I can write functions with more refined types:

unlet :: Term b -> Term b /// Lets

Дякую за вашу відповідь. Мені не подобається GHC TypeFamiliesвиключно на принципових підставах: це руйнує параметричність і вільні теореми. Я теж не дуже комфортно з GADTs, тому що дав GADT Foo a, ви можете мати два ізоморфних типу Barі Qux, таким чином, що Foo Barі Foo QuxНЕ ізоморфні. Це суперечить математичній інтуїції, що карта функцій дорівнює рівним - і, на рівні типу, ізоморфізм є правильним поняттям рівності.
піон

Я розумію ваші якості, але це дозволяє проводити спеціалізовані узагальнення, що я вважаю досить цінним на практиці.
Самуель Шлезінгер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.