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). Для большинства полезных случаев это верно...однако иногда ваша реализация просто не может быть "доказана" компилятором, поэтому вам придется "доказать" это себе в голове и использовать какое-то небезопасное принуждение.