Как частные типы помогают безопасно выставлять внутренние модули?

Чтение типов факторов и их использование в функциональном программировании я натолкнулся на этот пост. Автор упоминает Data.Set в качестве примера модуля, который предоставляет тонну функций, которым необходим доступ к внутренним модулям:

Data.Set имеет 36 функций, когда все, что действительно необходимо для обеспечения значения набора ( "Эти элементы разные" ), toList и fromList.

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

Затем он говорит

Мы могли бы облегчить весь этот беспорядок с типами факторов.

но не дает объяснения этому утверждению.

Итак, мой вопрос: как здесь используются типы факторов?


ИЗМЕНИТЬ

Я провел немного больше исследований и нашел статью "Построение полиморфных программ с типами факторов" . В нем подробно описывается декларирование частных контейнеров и упоминается слово "эффективный" в абстрактном и введении. Но если я не ошибаюсь, это не дает никакого примера эффективного представления "скрываться за" контейнером факторов.


РЕДАКТИРОВАТЬ 2

В "[PDF] Программирование в теории гомотопического типа" статья в главе 3. Тот факт, что тип фактора может быть реализуемый как зависимая сумма. Представлены представления о абстрактных типах (которые очень похожи на классы классов для меня) и предоставлен некоторый соответствующий код Agda. Тем не менее, эта глава посвящена рассуждениям об абстрактных типах, поэтому я не уверен, как это относится к моему вопросу.

Ответы

Ответ 1

Недавно я сделал сообщение type Natural = { n :: Integer | n >= 0 }

Мы могли бы реализовать это как абстрактный тип, используя интеллектуальный конструктор, который выдавал ошибку при задании отрицательного Integer. Этот тип говорит, что допустимо только подмножество значений типа Integer. Другим подходом, который мы могли бы использовать для реализации этого типа, является использование типа частного типа:

type Natural = Integer / ~ where n ~ m = abs n == abs m

Любая функция h :: X -> T для некоторого типа T индуцирует фактор-тип на X, относящийся к соотношению эквивалентности x ~ y = h x == h y. Котируемые типы этой формы легче кодируются как абстрактные типы данных. В общем, однако, может не быть такой удобной функции, например:

type Pair a = (a, a) / ~ where (a, b) ~ (x, y) = a == x && b == y || a == y && b == x

(Что касается того, как типы факторов относятся к setoids, тип quotient - это setoid, который обеспечивает соблюдение его отношения эквивалентности.) Это второе определение Natural имеет свойство, что есть два значения, которые представляют 2, сказать. А именно, 2 и -2. Аспект частного типа говорит, что нам разрешено делать все, что захочет, с базовым Integer, если мы никогда не создадим результат, который отличает этих двух представителей. Другим способом увидеть это является то, что мы можем кодировать тип частного, используя типы подмножеств, как:

X/~ = forall a. { f :: X -> a | forEvery (\(x, y) -> x ~ y ==> f x == f y) } -> a

К сожалению, это forEvery равносильно проверке равенства функций.

Масштабирование назад, типы подмножеств добавляют ограничения на производителей значений и типов факторов, которые добавляют ограничения для потребителей значений. Инварианты, применяемые абстрактным типом данных, могут быть их смесью. Действительно, мы можем решить представить Set следующим образом:

data Tree a = Empty | Branch (Tree a) a (Tree a)
type BST a = { t :: Tree a | isSorted (toList t) }
type Set a = { t :: BST a | noDuplicates (toList t) } / ~ 
    where s ~ t = toList s == toList t

Обратите внимание: ничего об этом никогда не требует от нас выполнения isSorted, noDuplicates или toList. Мы "просто" должны убедить проверку типа, что реализации функций этого типа удовлетворяли бы этим предикатам. Тип частного позволяет нам иметь избыточное представление при обеспечении того, чтобы мы рассматривали эквивалентные представления таким же образом. Это не означает, что мы не можем использовать конкретное представление, которое нам нужно для создания значения, это просто означает, что мы должны убедить проверку типа, что мы дали бы такое же значение, учитывая другое эквивалентное представление. Например:

maximum :: Set a -> a
maximum s = exposing s as t in go t
    where go Empty              = error "maximum of empty Set"
          go (Branch _ x Empty) = x
          go (Branch _ _     r) = go r

Обязательным доказательством этого является то, что самый правый элемент любого дерева двоичного поиска с одинаковыми элементами один и тот же. Формально она go t == go t' всякий раз, когда toList t == toList t'. Если бы мы использовали представление, гарантирующее, что дерево будет сбалансировано, например. дерево AVL, эта операция будет O(log N), в то время как преобразование в список и выбор максимума из списка будут O(N). Даже с этим представлением этот код строго эффективнее, чем преобразование в список и получение максимума из списка. Обратите внимание, что мы не смогли реализовать функцию, которая отображала древовидную структуру Set. Такая функция будет плохо напечатана.

Ответ 2

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

data Nat = Nat (Integer / abs)

Чтобы использовать это безопасно, мы должны быть уверены, что любая функция Nat -> T (с некоторым нечленом T, для простоты) не зависит от фактического целочисленного значения, а только от его абсолютного значения. Для этого не нужно полностью скрывать Integer; вам будет достаточно, чтобы вы не соответствовали ему напрямую. Вместо этого компилятор может переписать совпадения, например.

even' :: Nat -> Bool
even' (Nat 0) = True
even' (Nat 1) = False
even' (Nat n) = even' . Nat $ n - 2

можно переписать в

even' (Nat n') = case abs n' of
           [|abs 0|]  -> True
           [|abs 1|]  -> False
           n          -> even' . Nat $ n - 2

Такая перепись указывала бы на нарушения эквивалентности, например

bad (Nat 1) = "foo"
bad (Nat (-1)) = "bar"
bad _ = undefined

переписывается на

bad (Nat n') = case n' of
      1 -> "foo"
      1 -> "bar"
      _ -> undefined

который, очевидно, является перекрывающимся шаблоном.

Ответ 3

Отказ от ответственности: я только что прочитал о типах quot при чтении этого вопроса.

Я думаю, что автор просто говорит, что множества могут быть описаны как типы факторов по спискам. Т.е.: (составление некоторого синтаксиса типа haskell):

data Set a = Set [a] / (sort . nub) deriving (Eq)

То есть, a Set a является просто a [a] с равенством между двумя Set a, определяемыми тем, равны ли sort . nub базовых списков.

Мы могли бы сделать это явно так, я думаю:

import Data.List

data Set a = Set [a] deriving (Show)

instance (Ord a, Eq a) => Eq (Set a) where
  (Set xs) == (Set ys) = (sort $ nub xs) == (sort $ nub ys)

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