Я хотел бы иметь тип, который может содержать значения от 0 до n, где n живет на уровне типа.
Я пытался что-то вроде:
import GHC.TypeLits
import Data.Proxy
newtype FiniteNat n = FiniteNat { toInteger :: Integer }
smartConstructFiniteNat :: (KnownNat n) => Proxy n -> Integer -> Maybe (FiniteNat (Proxy n))
smartConstructFiniteNat pn i
| 0 <= i && i < n = Just (FiniteNat i)
| otherwise = Nothing
where n = natVal pn
который работает в основном, но он действительно не удовлетворяет каким-то образом. Существует ли "стандартное" решение или даже библиотека для достижения этого? Существует много суеты о зависимых типизированных списках, но я не смог найти что-то в этом роде. Кроме того, я предполагаю, что использование GHC.TypeLits
необходимо, потому что мой n
может принимать довольно большие значения, поэтому индуктивное определение, вероятно, будет очень медленным.
Ответ 1
Вы можете напрямую перевести Idris Fin
в обычный Hashell-мишмар с характерными для него функциями.
data Fin n where
FZ :: Fin (S n)
FS :: Fin n -> Fin (S n)
(!) :: Vec n a -> Fin n -> a
(x :> xs) ! FZ = x
(x :> xs) ! (FS f) = xs ! f
С TypeInType
вы можете даже иметь singleton Fin
s!
data Finny n (f :: Fin n) where
FZy :: Finny (S n) FZ
FSy :: Finny n f -> Finny (S n) (FS f)
Это позволяет вам подделывать зависимую количественную оценку по сравнению со средствами выполнения, например,
type family Fin2Nat n (f :: Fin n) where
Fin2Nat (S _) FZ = Z
Fin2Nat (S n) (FS f) = S (Fin2Nat n f)
-- tighten the upper bound on a given Fin as far as possible
tighten :: Finny n f -> Fin (S (Fin2Nat n f))
tighten FZy = FZ
tighten (FSy f) = FS (tighten f)
но, должно быть, это отвратительно, чтобы дублировать все на уровне значения и типа, и выписать все ваши переменные вида (n
) может стать довольно утомительным.
Если вы действительно уверены, что вам нужно эффективное представление времени выполнения Fin
, вы можете сделать в основном то, что вы сделали в своем вопросе: введите машину Int
в newtype
и используйте тип phantom для его размер. Но бремя ответственности на вас, библиотекатор, чтобы убедиться, что Int
соответствует границе!
newtype Fin n = Fin Int
-- fake up the constructors
fz :: Fin (S n)
fz = Fin 0
fs :: Fin n -> Fin (S n)
fs (Fin n) = Fin (n+1)
В этой версии отсутствуют реальные конструкторы GADT, поэтому вы не можете манипулировать равенствами типов, используя сопоставление шаблонов. Вы должны сделать это самостоятельно, используя unsafeCoerce
. Вы можете предоставить клиентам безопасный тип интерфейса в форме fold
, но они должны быть готовы написать весь свой код в стиле более высокого порядка, и (поскольку fold
является катаморфизмом), становится труднее смотреть на более чем одном уровне за раз.
-- the unsafeCoerce calls assert that m ~ S n
fold :: (forall n. r n -> r (S n)) -> (forall n. r (S n)) -> Fin m -> r m
fold k z (Fin 0) = unsafeCoerce z
fold k z (Fin n) = unsafeCoerce $ k $ fold k z (Fin (n-1))
О, и вы не можете выполнить вычисление уровня уровня (как это было сделано с Fin2Nat
выше) с этим представлением Fin
, потому что тип уровня Int
не допускает индукции.
Для чего это стоит, Idris Fin
столь же неэффективен, как и GADT выше. Документы содержат следующее предостережение:
Вероятно, не стоит использовать Fin
для арифметики, и они будут чрезвычайно неэффективны во время выполнения.
Я слышал звуки о будущей версии Idris, которая могла бы определить "Nat
с типами" -типы типов данных (например, Fin
) и автоматически стирать доказательства и упаковывать значения в числовые числа машин, но как я знаю, мы еще не там.
Ответ 2
rampion предложил синонимы шаблонов, и я согласился, но, по общему признанию, не совсем тривиально разработать, как правильно структурировать свои подписи. Таким образом, я решил, что напишу правильный ответ, чтобы дать полный код.
Во-первых, обычный шаблон:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE Trustworthy #-}
module FakeFin (Nat (..), Fin (FZ, FS), FinView (..), viewFin) where
import Numeric.Natural
import Unsafe.Coerce
Теперь основные типы:
data Nat = Z | S Nat
-- Fin *must* be exported abstractly (or placed in an Unsafe
-- module). Users can use its constructor to implement
-- unsafeCoerce!
newtype Fin (n :: Nat) = Fin Natural
deriving instance Show (Fin n)
Гораздо проще работать с помощью типа вида, а не напрямую, поэтому пусть определите его:
data FinView n where
VZ :: FinView ( n)
VS :: !(Fin n) -> FinView ( n)
deriving instance Show (FinView n)
Важно отметить, что мы могли бы определить FinView
с использованием явных ограничений равенства, потому что нам нужно будет думать в этих терминах, чтобы дать правильные сигнатуры шаблонов:
data FinView n where
VZ :: n ~ m => FinView n
VS :: n ~ m => !(Fin m) -> FinView n
Теперь фактическая функция просмотра:
viewFin :: Fin n -> FinView n
viewFin (Fin 0) = unsafeCoerce VZ
viewFin (Fin n) = unsafeCoerce (VS (Fin (n - 1)))
Подписи шаблонов точно отражают сигнатуры конструкторов FinView
.
pattern FZ :: () => n ~ m => Fin n
pattern FZ <- (viewFin -> VZ) where
FZ = Fin 0
pattern FS :: () => n ~ m => Fin m -> Fin n
pattern FS m <- (viewFin -> VS m) where
FS (Fin m) = Fin (1 + m)
-- Let GHC know that users need only match on `FZ` and `FS`.
-- This pragma only works for GHC 8.2 (and presumably future
-- versions).
{-# COMPLETE FZ, FS #-}
Для полноты (потому что мне потребовалось больше усилий, чтобы написать это, чем я ожидал), здесь один из способов написать unsafeCoerce
, если этот модуль случайно экспортирует конструктор данных Fin
. Я полагаю, что есть, вероятно, более простые способы.
import Data.Type.Equality
type family YahF n a b where
YahF 'Z a _ = a
YahF _ _ b = b
newtype Yah n a b = Yah (YahF n a b)
{-# NOINLINE finZBad #-}
finZBad :: 'Z :~: n -> Fin n -> a -> b
finZBad pf q =
case q of
FZ -> blah (trans pf Refl)
FS _ -> blah (trans pf Refl)
where
blah :: forall a b m. 'Z :~: m -> a -> b
blah pf2 a = getB pf2 (Yah a)
{-# NOINLINE getB #-}
getB :: n :~: m -> Yah n a b -> b
getB Refl (Yah b) = b
myUnsafeCoerce :: a -> b
myUnsafeCoerce = finZBad Refl (Fin 0)
finZBad
- это то, где происходит все действие, но он ничего не делает удаленно неправильно. Если кто-то действительно дает нам не-нижнее значение типа Fin 'Z, то что-то уже пошло ужасно неправильно. Явные доказательства равенства типов здесь необходимы, потому что если GHC увидит код, требующий 'Z ~ m
, он просто отвергнет его из-под контроля; GHC действительно не любит гипотетические рассуждения в ограничениях. Аннотации NOINLINE
необходимы, поскольку сам GHC-модуль упрощения использует информацию о типе; обработка доказательств того, что он знает очень хорошо, невозможно сбивает с толку, с чрезвычайно произвольными результатами. Поэтому мы блокируем его и успешно реализуем функцию "Зло".