Как сделать экземпляр векторов фиксированной длины Applicative?
недавно я узнал о продвижении и решил попробовать написать векторы.
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
module Vector where
data Nat = Next Nat | Zero
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
пока все работает. Но я столкнулся с проблемой при попытке сделать Vector
экземпляр Applicative
.
instance Applicative (Vector n) where
a <*> b =
case a of
Construct f c ->
case b of
Construct x d -> Construct (f x) (c <*> d)
Empty -> Empty
pure x = _
я понятия не имел, как это сделать pure
. Я попробовал:
case n of
Next _ -> Construct x (pure x)
Zero -> Empty
но есть Variable not in scope: n :: Nat
ошибка для первой строки и Couldn't match type n with 'Zero
для третьей строки этого выражения.
Итак, я использовал следующее мотыга.
class Applicative' n where
ap' :: Vector n (t -> u) -> Vector n t -> Vector n u
pure' :: t -> Vector n t
instance Applicative' n => Applicative' ('Next n) where
ap' (Construct f a) (Construct x b) = Construct (f x) (ap' a b)
pure' x = Construct x (pure' x)
instance Applicative' 'Zero where
ap' Empty Empty = Empty
pure' _ = Empty
instance Applicative' n => Applicative (Vector n) where
(<*>) = ap'
pure = pure'
это делает работу, но это не очень красиво. Он вводит бесполезный класс Applicative'
. И каждый раз, когда я хочу использовать Applicative
на Vector
в любой функции я должен предоставить дополнительное бесполезное ограничение Applicative' n
что на самом деле справедливо для любого n
.
что было бы лучшим, более чистым способом сделать это?
4 ответов
вы можете сделать то же самое напрямую:
instance Applicative (Vector Zero) where
a <*> b = Empty
pure x = Empty
instance Applicative (Vector n) => Applicative (Vector (Next n)) where
a <*> b =
case a of
Construct f c ->
case b of
Construct x d -> Construct (f x) (c <*> d)
pure x = Construct x (pure x)
как я могу рассуждать об этом: для разных типов класса, код должен быть типа-знать. Если бы у вас было несколько экземпляров, разные типы получили бы другую реализацию, и она была бы легко разрешена. Но, если вы попытаетесь сделать это с одним нерекурсивным экземпляром, в основном нет информации о типе во время выполнения, и код, который всегда один и тот же, все еще должен решить, какой тип обрабатывать. Когда у вас есть вход параметры, вы можете использовать GADTs, чтобы предоставить вам информацию о типе. Но для pure
нет входных параметров. Поэтому у вас должен быть некоторый контекст для Applicative
экземпляра.
это (комментарии) альтернатива, которая использует singletons
пакета.
очень грубо, Haskell не позволяет нам сопоставлять шаблоны на значениях уровня типа, таких как n
в коде выше. С singletons
, мы можем, ценой требования и предоставления нескольких экземпляров SingI
здесь и там.
{-# LANGUAGE GADTs , KindSignatures, DataKinds, TemplateHaskell,
TypeFamilies, ScopedTypeVariables #-}
{-# OPTIONS -Wall #-}
import Data.Singletons.TH
-- Autogenerate singletons for this type
$(singletons [d|
data Nat = Next Nat | Zero
|])
-- as before
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
-- as before
instance Functor (Vector n) where
fmap _ Empty = Empty
fmap f (Construct x b) = Construct (f x) (fmap f b)
-- We now require n to carry its own SingI instance.
-- This allows us to pattern match on n.
instance SingI n => Applicative (Vector n) where
Empty <*> Empty = Empty
-- Here, we need to access the singleton on n, so that later on we
-- can provide the SingI (n-1) instance we need for the recursive call.
-- The withSingI allows us to use m :: SNat (n-1) to provide the instance.
(Construct f c) <*> (Construct x d) = case sing :: SNat n of
SNext m -> withSingI m $ Construct (f x) (c <*> d)
-- Here, we can finally pattern match on n.
-- As above, we need to provide the instance with withSingI
-- to the recursive call.
pure x = case sing :: SNat n of
SZero -> Empty
SNext m -> withSingI m $ Construct x (pure x)
использование этого потребует предоставления SingI n
экземпляр при каждом использовании, что немного неудобно, но не слишком много (IMO). Самое печальное, что <*>
на самом деле не нужно SingI n
, так как, в принципе, он может пересчитать это из двух векторов под рукой. Однако,pure
не имеет входного вектора, поэтому он может соответствовать только шаблону с предоставленным синглтоном.
в качестве другой альтернативы, аналогичной исходному коду, можно написать
instance Applicative (Vector Zero) where
...
instance Applicative (Vector n) => Applicative (Vector (Next n)) where
...
это не полностью эквивалентно, и потребуется добавить контексты Applicative (Vector n) =>
во всех функциях позже, где n
- это неизвестно, но может быть достаточно для многих цели.
считай, что это дополнение к ответу @чи, чтобы обеспечить дополнительное объяснение синглтон подход...
я бы предложил чтение бумага Hasochism если вы еще не сделали этого. В частности, в разделе 3.1 этой статьи они занимаются именно этой проблемой и используют ее в качестве мотивирующего примера для неявных одноэлементных параметров (SingI
ответа @chi и NATTY
класс типа в статье Hasochism) необходимы, скорее чем просто удобно.
поскольку это относится к вашему коду, основная проблема заключается в том, что pure
требуется представление во время выполнения длины вектора, который он должен генерировать, и переменной уровня типа n
не подходит для счета. Решение состоит в том, чтобы ввести новый GADT, "синглтон", который предоставляет значения времени выполнения, которые соответствуют непосредственно продвигаемым типам Next
и Zero
:
data Natty (n :: Nat) where
ZeroTy :: Natty Zero
NextTy :: Natty n -> Natty (Next n)
я попытался использовать примерно то же соглашение об именах как бумага:Natty
то же, и ZeroTy
и NextTy
соответствуют газета Zy
и Sy
.
сам по себе этот явный синглтон полезно. Например, см. Определение vchop
в газете. Кроме того, мы можем легко написать вариант pure
для этого требуется явный синглтон, чтобы выполнить свою работу:
vcopies :: Natty n -> a -> Vector n a
vcopies ZeroTy _ = Empty
vcopies (NextTy n) x = Construct x (vcopies n x)
мы еще не можем использовать это для определения pure
, потому что pure
's подпись определяется Applicative
тип класса, и у нас нет способа сжать явный синглтон Natty n
там.
решение состоит в том, чтобы ввести неявные синглтоны, которые позволяют нам извлекать явный синглтон, когда это необходимо через natty
функция в контексте следующего типа class:
class NATTY n where
natty :: Natty n
instance NATTY Zero where
natty = ZeroTy
instance NATTY n => NATTY (Next n) where
natty = NextTy natty
теперь, при условии, что мы в NATTY n
контекст, мы можем назвать vcopies natty
поставить vcopies
с явными natty
параметр, который позволяет нам написать:
instance NATTY n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies natty
использование определения vcopies
и natty
выше, и определение vapp
ниже:
vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
обратите внимание на одну странность. Мы должны были представить это vapp
вспомогательная функция по неясной причине. Следующий экземпляр без NATTY
соответствует case
на основе определения и проверки штрафа:
instance Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = Construct (f x) (c <*> d)
pure = error "Argh! No NATTY!"
если мы добавляем NATTY
ограничение для определения pure
:
instance NATTY n => Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = Construct (f x) (c <*> d)
pure = vcopies natty
определение (<*>)
не печатает check any больше. Проблема в том, что NATTY n
ограничение на левой стороне второго (<*>)
случай автоматически не подразумевает NATTY n1
ограничение с правой стороны (где Next n ~ n1
), поэтому GHC не хочет позволять нам звонить (<*>)
С правой стороны. В этом случае, поскольку ограничение фактически не требуется после его первого использования, вспомогательная функция без NATTY
ограничения, а именно vapp
, работает вокруг проблемы.
@chi использует случай соответствие на natty
и вспомогательная функция withSingI
другое решение. Эквивалентный код здесь будет использовать вспомогательную функцию, которая превращает явный синглтон в неявный NATTY
контекст:
withNATTY :: Natty n -> (NATTY n => a) -> a
withNATTY ZeroTy a = a
withNATTY (NextTy n) a = withNATTY n a
позволяет нам написать:
instance NATTY n => Applicative (Vector n) where
Empty <*> Empty = Empty
Construct f c <*> Construct x d = case (natty :: Natty n) of
NextTy n -> withNATTY n $ Construct (f x) (c <*> d)
pure x = case (natty :: Natty n) of
ZeroTy -> Empty
NextTy n -> Construct x (withNATTY n $ pure x)
для этого потребуется как ScopedTypeVariables
и RankNTypes
.
в любом случае, придерживаясь вспомогательных функций, полная программа выглядит так:
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}
module Vector where
data Nat = Next Nat | Zero
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
data Natty (n :: Nat) where
ZeroTy :: Natty Zero
NextTy :: Natty n -> Natty (Next n)
class NATTY n where
natty :: Natty n
instance NATTY Zero where
natty = ZeroTy
instance NATTY n => NATTY (Next n) where
natty = NextTy natty
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
instance NATTY n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies natty
vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
vcopies :: Natty n -> a -> Vector n a
vcopies ZeroTy _ = Empty
vcopies (NextTy n) x = Construct x (vcopies n x)
переписка с singletons
библиотеки заключается в том, что:
$(singletons [d|
data Nat = Next Nat | Zero
|])
автоматически генерирует синглеты (с конструкторами SZero
и SNat
вместо ZeroTy
и NatTy
; и с типом SNat
вместо Natty
) и неявный одноэлементный класс (называемый SingI
вместо NATTY
и, используя функцию sing
вместо natty
), давая полную программу:
{-# LANGUAGE DataKinds, GADTs, KindSignatures, TemplateHaskell, TypeFamilies #-}
module Vector where
import Data.Singletons
import Data.Singletons.TH
$(singletons [d|
data Nat = Next Nat | Zero
|])
data Vector :: Nat -> * -> * where
Construct :: t -> Vector n t -> Vector ('Next n) t
Empty :: Vector 'Zero t
instance Functor (Vector n) where
fmap f a =
case a of
Construct x b -> Construct (f x) (fmap f b)
Empty -> Empty
instance SingI n => Applicative (Vector n) where
(<*>) = vapp
pure = vcopies sing
vapp :: Vector n (a -> b) -> Vector n a -> Vector n b
vapp Empty Empty = Empty
vapp (Construct f c) (Construct x d) = Construct (f x) (vapp c d)
vcopies :: SNat n -> a -> Vector n a
vcopies SZero _ = Empty
vcopies (SNext n) x = Construct x (vcopies n x)
подробнее о том, что singletons
библиотека делает и как она построена, я бы предложил прочитать введение в синглтоны.
несколько других ответов ввели Natty
или SNat
типа для реализации pure
. Действительно, наличие такого типа значительно уменьшает потребность в одноразовых классах типов. Потенциальный недостаток традиционного Natty
/SNat
GADT, однако, заключается в том, что ваша программа будет фактически строить представление, а затем использовать его, даже если Nat
известно во время компиляции. Это вообще не произойдет с подходом вспомогательного класса. Вы можете обойти это с помощью различные представления.
я собираюсь использовать эти имена:
data Nat = Z | S Nat
Предположим, мы определяем обычный
data Natty n where
Zy :: Natty 'Z
Sy :: Natty n -> Natty ('S n)
мы можем написать свой элиминатор (принцип индукции) таким образом:
natty :: p 'Z -> (forall k. p k -> p ('S k)) -> Natty n -> p n
natty z _ Zy = z
natty z s (Sy n) = s (natty z s n)
для нашей цели нам действительно не нужно Natty
; нам нужен только его принцип индукции! Давайте определим другую версию. Я предполагаю, что для этой кодировки есть собственное имя, но я понятия не имею, что это может быть.
newtype NatC n = NatC
{ unNatC :: forall p.
p 'Z -- base case
-> (forall k. p k -> p ('S k)) -- inductive step
-> p n }
это изоморфно Natty
:
nattyToNatC :: Natty n -> NatC n
nattyToNatC n = NatC (\z s -> natty z s n)
natCToNatty :: NatC n -> Natty n
natCToNatty (NatC f) = f Zy Sy
теперь мы можем написать класс для Nat
s Мы знаем, как устранить:
class KnownC n where
knownC :: NatC n
instance KnownC 'Z where
knownC = NatC $ \z _ -> z
instance KnownC n => KnownC ('S n) where
knownC = NatC $ \z s -> s $ unNatC knownC z s
теперь вот тип вектора (я переименовал вещи в соответствии с моим собственным вкусом):
infixr 4 :<
data Vec :: Nat -> * -> * where
(:<) :: t -> Vec n t -> Vec ('S n) t
Nil :: Vec 'Z t
, потому что Vec
параметр длины не является последним, нам придется перевернуть его, чтобы использовать с NatC
:
newtype Flip f a n = {unFlip :: f n a}
induct2 :: f 'Z a
-> (forall k. f k a -> f ('S k) a)
-> NatC n -> f n a
induct2 z s n = unFlip $ unNatC n (Flip z) (\(Flip r) -> Flip (s r))
replC :: NatC n -> a -> Vec n a
replC n a = induct2 Nil (a :<) n
instance KnownC n => Applicative (Vec n) where
pure = replC knownC
(<*>) = ...
теперь, если длина вектора известна во время компиляции,pure
вектор будет построен сразу, без промежуточного звена необходима структура.