Haskell: указание ограничений равной длины списков в системе типов

в Haskell у меня часто есть такая функция, как f, который принимает список и возвращает список равной длины:

f :: [a] -> [a]  -- length f(xs) == length xs

аналогично, у меня может быть такая функция, как g, которая принимает два списка, которые должны быть одинаковой длины:

g :: [a] -> [a] -> ...

если f и g набираются, как указано выше, то ошибки во время выполнения могут привести, если их ограничения, связанные с длиной не удовлетворены. Поэтому я хотел бы кодировать эти ограничения в системе типов. Как я могу это сделать это?

обратите внимание, что я ищу практическую основу, которая может использоваться в повседневных ситуациях, добавляя как можно меньше интуитивных накладных расходов в код. Мне особенно интересно узнать, как вы справитесь с f и g себя; то есть, вы попытаетесь добавить ограничения, связанные с длиной, к их типам, как здесь задано, или вы оставите их с типами, как указано выше для простоты кода?

2 ответов


следующий код взят из книги блог Габриэля Гонсалеса в сочетании с некоторой информацией, представленной в комментариях:

{-# LANGUAGE GADTs, DataKinds #-}

data Nat = Z | S Nat

-- A List of length 'n' holding values of type 'a'
data List n a where
    Nil  :: List Z a
    Cons :: a -> List m a -> List (S m) a

-- Just for visualization (a better instance would work with read)
instance Show a => Show (List n a) where
    show Nil = "Nil"
    show (Cons x xs) = show x ++ "-" ++ show xs

g :: (a -> b -> c) -> List n a -> List n b -> List n c
g f (Cons x xs) (Cons y ys) = Cons (f x y) $ g f xs ys
g f Nil Nil = Nil

l1 = Cons 1 ( Cons 2 ( Nil ) ) :: List (S (S Z)) Int
l2 = Cons 3 ( Cons 4 ( Nil ) ) :: List (S (S Z)) Int
l3 = Cons 5 (Nil) :: List (S Z) Int

main :: IO ()
main = print $ g (+) l1 l2
       -- This causes GHC to throw an error:
       -- print $ g (+) l1 l3

это альтернативное определение списка (с использованием GADTs и DataKinds) кодирует длину списка в его типе. Если вы затем определите свою функцию g :: List n a -> List n a -> ... система типов будет жаловаться, если списки не одинаковой длины.

в случае, если это (понятно) будет слишком большим дополнительным осложнением для вас, я не уверен использование системы типов было бы способом пойти. Проще всего определить g использование монады / аппликатора (например,Maybe или Either), а g добавить элементы в список в зависимости от обоих входов и последовательности результата. Т. е.

g :: (a -> b -> c) -> [a] -> [b] -> Maybe [c]
g f l1 l2 = sequence $ g' f l1 l2
  where g' f (x:xs) (y:ys) = (Just $ f x y) : g' f xs ys
        g' f [] [] = []
        g' f _ _ = [Nothing]

main :: IO ()
main = do print $ g (+) [1,2] [2,3,4] -- Nothing
          print $ g (+) [1,2,3] [2,3,4] -- Just [3,5,7]

недостаток, который вы наблюдаете, заключается в том, что информация о длине не является частью тип списка; поскольку проверка типов предназначена для рассуждения о типах, вы не можете указать инварианты в своих функциях если инварианты в тип самих аргументов,или на ограничениях типа или семейных равенствах типа. (Однако есть некоторые предварительные процессоры haskell, такие как Liquid Haskell, которые позволяют аннотировать функции с помощью инварианты, подобные этому, будут проверены при компиляции.)

существует множество библиотек haskell, которые предлагают структуры данных типа list с длиной, закодированной в типе. Некоторые известные из них линейныйV) и основные-вектор.

интерфейс V идет что-то вроде этого:

f :: V n a -> V n a -> V n a
g :: V n a -> V n a -> [a]
-- or --
g :: V n a -> V n a -> V ?? a -- if ?? can be determined at compile-time

обратите внимание на конкретный шаблон нашей подписи первого типа для g: мы берем два типа, где мы заботимся о длинах, и верните тип, который не забота о длине, потеря информации.

во втором случае, если мы do заботьтесь о длине результата, длина должна быть известна во время компиляции, чтобы это имело смысл.

отметим, что V from linear фактически не обертывает список, А Вектор из библиотеки векторов. Это также требует объектив (линейная библиотека, то есть), что, по общему признанию, является огромной зависимостью для втягивания, если все, что вам нужно, это векторы с кодировкой длины. Я думаю, что тип вектора от fixed-vectors использует что-то более эквивалентное обычному списку haskell...но я не совсем уверен. В любом случае, у него есть Foldable экземпляра, так что вы можете преобразовать его в список.

помните, конечно, что если вы планируете кодировать длины в своих функциях, как это...Haskell / GHC должен иметь возможность видеть, что ваша реализация typechecks! Для большинства из этих библиотек Haskell сможет проверьте такие вещи (если вы придерживаетесь таких вещей, как молния и fmapping, привязка, ap-ping). Для большинства полезных случаев это верно...однако иногда ваша реализация просто не может быть "доказана" компилятором, поэтому вам придется "доказать" это себе в голове и использовать какое-то небезопасное принуждение.