数组和类型提升(和依赖类型?)

时间:2016-10-10 07:24:45

标签: haskell typeclass dependent-type

假设b类型是Monoid的实例,并且(v1,v2) :: (i,i)的固定索引范围属于类型类i Ix我想将相应的Data.Array类型定义为Monoid。如何才能做到这一点?在这里,mempty应该是包含条目mempty::b的数组,而mappend数组应该是mappend - 操作组件。

(例如,如果i=(Int,Int)类型Data.Array i b表示具有不同大小(以及索引的不同范围)的所有(二维)基质。仅适用于固定大小,例如Monoid -declaration是有道理的。实际上,我对矢量空间案例感兴趣而不是Monoids,但是Monoid已经显示出难度。我对依赖类型只有一个模糊的想法,但这似乎是个人情况的典型例子对应于一个参数范围的唯一子集的类型将是有用的。)

2 个答案:

答案 0 :(得分:4)

一种常见的方法是将一个非常类型的表示形式包装成一个更类似的表示形式:

data Nat = Z | S Nat
newtype Vec (n :: Nat) a = Vec [a]
newtype Sized m (ns :: [Nat]) a = Sized { getArray :: Array (Vec m Int) a }

此处ns是宣传的(请参阅Giving Haskell a Promotion)幻像(请参阅Motivation behind Phantom Types?)值 - 维度大小列表,m是此列表的长度(促进和幻影)。因此,假设Sized包装器下的任何数组都是一个多维矩阵,ns表示其维度。然后Monoid实例如下所示:

instance (SingI m, SingI ns, Monoid a) => Monoid (Sized m ns a) where
  mempty                      = listSized $ repeat mempty
  Sized as `mappend` Sized bs = listSized $ zipWith mappend (elems as) (elems bs)

SingI内容来自singletons库。单例允许将值提升到类型级别,因此我们可以模拟依赖类型,SingI允许通过fromSing函数将提升值返回到值级别。 listSized基本上是listArray,但对于具有静态已知维度的数组,因此它要求所有SingI都在范围内。这是它的定义:

toInt :: Nat -> Int
toInt = go 0 where
  go !a  Z    = a
  go  a (S n) = go (1 + a) n

vecBounds :: forall m (ns :: [Nat]). (SingI m) => Sing ns -> (Vec m Int, Vec m Int)
vecBounds singNs = (Vec $ replicate m 0, Vec ns') where
    m   = toInt $ fromSing (sing :: Sing m)
    ns' = map (pred . toInt) $ fromSing singNs

listSized :: forall m (ns :: [Nat]) a. (SingI m, SingI ns) => [a] -> Sized m ns a
listSized = Sized . listArray (vecBounds (sing :: Sing ns))

vecBounds计算给定的维度维度列表的边界。它返回一个元组,第一个组件是最低的索引,它始终是[0,0..0]形式(存在与维度一样多的零,即m)。第二个组件是最大的索引,所以如果你是有一个尺寸列表,如[2, 1, 3](表示为[S (S Z), S Z, S (S (S Z))]),则最大索引为[1, 0, 2]

仅为Ix提供Vec n a个实例,这是the product instances的直接概括:

instance Ix a => Ix (Vec n a) where
  range   (Vec ns, Vec ms)          = map Vec . sequence $ zipWith (curry range) ns ms
  index   (Vec ns, Vec ms) (Vec ps) = foldr (\(i, r) a -> i + r * a) 0 $
    zipWith3 (\n m p -> (index (n, m) p, rangeSize (n, m))) ns ms ps
  inRange (Vec ns, Vec ms) (Vec ps) = and $ zipWith3 (curry inRange) ns ms ps

我们可以写一些测试:

type M  = S (S (S Z))
type Ns = [S (S Z), S Z, S (S (S Z))]

arr1 :: Sized M Ns (Sum Int)
arr1 = listSized $ map Sum [5,3,6,7,1,4]

arr2 :: Sized M Ns (Sum Int)
arr2 = listSized $ map Sum [8,2,9,7,3,6]

main = mapM_ (print . getArray) $ [arr1, arr2, arr1 `mappend` arr2 `mappend` mempty]

打印

array (Vec [0,0,0],Vec [1,0,2]) [(Vec [0,0,0],Sum {getSum = 5}),(Vec [0,0,1],Sum {getSum = 6}),(Vec [0,0,2],Sum {getSum = 1}),(Vec [1,0,0],Sum {getSum = 3}),(Vec [1,0,1],Sum {getSum = 7}),(Vec [1,0,2],Sum {getSum = 4})]
array (Vec [0,0,0],Vec [1,0,2]) [(Vec [0,0,0],Sum {getSum = 8}),(Vec [0,0,1],Sum {getSum = 9}),(Vec [0,0,2],Sum {getSum = 3}),(Vec [1,0,0],Sum {getSum = 2}),(Vec [1,0,1],Sum {getSum = 7}),(Vec [1,0,2],Sum {getSum = 6})]
array (Vec [0,0,0],Vec [1,0,2]) [(Vec [0,0,0],Sum {getSum = 13}),(Vec [0,0,1],Sum {getSum = 15}),(Vec [0,0,2],Sum {getSum = 4}),(Vec [1,0,0],Sum {getSum = 5}),(Vec [1,0,1],Sum {getSum = 14}),(Vec [1,0,2],Sum {getSum = 10})]

即。元素按要求逐点求和。如果您不小心尝试对具有不同维度的数组求和,则会出现类型错误:

type Ns  = [S (S Z), S Z, S (S (S Z))]
type Ns' = [S (S (S Z)), S Z, S (S Z)]

arr1 :: Sized M Ns (Sum Int)
arr1 = listSized $ map Sum [5,3,6,7,1,4]

arr2 :: Sized M Ns' (Sum Int)
arr2 = listSized $ map Sum [8,2,9,7,3,6]

main = print . getArray $ arr1 `mappend` arr2

-- Couldn't match type 'S 'Z with 'Z …
-- Expected type: Sized M Ns (Sum Int)
--   Actual type: Sized M Ns' (Sum Int)
-- In the second argument of `mappend', namely `arr2'
-- In the first argument of `mappend', namely `arr1 `mappend` arr2'

Full code

答案 1 :(得分:3)

@ user3237465是关于将静态大小信息附加到Array的问题的完整而直接的答案。但是既然你提到你对依赖类型很新,我想给出一个更简单的矩阵加法例子,我觉得它可以作为对该主题的更好的介绍。下面的大部分内容都可以在the Hasochism paper中找到(更好地解释!)。

像往常一样,我们有自然数字,GHC将自动提升到类型级别。以下data声明不仅定义了类型Nat和两个值构造函数ZS,我们还获得 {{1 }和两个类型构造函数NatZ

S

我将定义习惯的 vector GADT,它在操作上是一个链接列表,其中包含其长度的静态知识。

data Nat = Z | S Nat

type One = S Z
type Two = S (S Z)
type Three = S (S (S Z))

以下是一些示例向量。

infixr 5 :>
data Vec n a where
    VNil :: Vec Z a
    (:>) :: a -> Vec n a -> Vec (S n) a
deriving instance Show a => Show (Vec n a)

instance Functor (Vec n) where
    fmap f VNil = VNil
    fmap f (x :> xs) = f x :> fmap f xs

我们需要对类型级别的数字进行运行时分析。例如,我们想编写一个函数v1 :: Vec Two String v1 = "foo" :> "bar" :> VNil v2 :: Vec Two String v2 = "baz" :> "quux" :> VNil v3 :: Vec One String v3 = "nabble" :> VNil ,它重复给定元素vreplicate :: n -> a -> Vec n a次。 n必须在运行时知道 要创建的值的副本数量!但是,上述类型签名无效,因为Haskell维护运行时值和编译时类型之间的分离。属于vreplicate种类的类型无法在运行时传递。输入单例值

Nat

(这或多或少是data Natty n where Zy :: Natty Z Sy :: Natty n -> Natty (S n) 库为您生成的代码。)对于singletons类的给定(明确定义的)n,确切地说类型Nat的一个(明确定义的)值。 Natty n上的模式匹配会告诉您Nattyn量词告诉您forall n. Natty n ->在运行时使用。因此,我们的n函数的类型为vreplicateNatty n -> a -> Vec n a作为Natty n的运行时替身。 (一种真正依赖类型的语言不会让你跳过这样的箍!)

正如我所提到的,如果你知道n的价值,你知道它的Natty。我们还可以使用以下hacky类型,从类型到值以另一种方式使信息流动:

n

class NATTY n where natty :: Natty n instance NATTY Z where natty = Zy instance NATTY n => NATTY (S n) where natty = Sy natty 字典是NATTY n的单身n的隐式副本。

行。 Natty Applicative实例将两个向量压缩在一起,逐点组合它们的内容。

Vec

因此,对于vzip :: Vec n a -> Vec n b -> Vec n (a, b) vzip VNil VNil = VNil vzip (x :> xs) (y :> ys) = (x, y) :> vzip xs ys vreplicate :: Natty n -> a -> Vec n a vreplicate Zy _ = VNil vreplicate (Sy n) x = x :> vreplicate n x instance NATTY n => Applicative (Vec n) where pure = vreplicate natty fs <*> xs = fmap (uncurry ($)) (vzip fs xs) 的向量,我们可以将Monoid a Monoid提升为a。这是将Applicative转换为Monoid的标准技巧。

instance Monoid a => Monoid (Vec n a) where
    mempty = pure mempty
    mappend = liftA2 mappend

收益:您只能mappend长度匹配的向量。将其与列表zip进行比较,该列表截断了两个被压缩列表中较长的列表。

ghci> v1 `mappend` v2
"foobaz" :> ("barquux" :> VNil)

-- ┌       ┐     ┌        ┐     ┌           ┐
-- | "foo" |  +  | "baz"  |  =  | "foobar"  |
-- | "bar" |     | "quux" |     | "bazquux" |
-- └       ┘     └        ┘     └           ┘

ghci> v1 `mappend` v3
<interactive>:35:14: error:
    • Couldn't match type ‘'Z’ with ‘'S 'Z’
      Expected type: Vec Two String
        Actual type: Vec One String
    • In the second argument of ‘mappend’, namely ‘v3’
      In the expression: v1 `mappend` v3
      In an equation for ‘it’: it = v1 `mappend` v3

-- ┌       ┐     ┌          ┐
-- | "foo" |  +  | "nabble" |  =  ?
-- | "bar" |     └          ┘
-- └       ┘

现在让我们使用2D矩阵。诀窍是用较小的可重用位构建它们。矩阵是向量的向量,两个向量的类型级组成。

newtype (f :.: g) a = Compose { getCompose :: f (g a) } deriving Show

type Mat n m = Vec n :.: Vec m

也就是说,Mat n m aVec n (Vec m a)同构。

通过构图保留了Functoriality和应用程序,

instance (Functor f, Functor g) => Functor (f :.: g) where
    fmap f = Compose . fmap (fmap f) . getCompose
instance (Applicative f, Applicative g) => Applicative (f :.: g) where
    pure = Compose . pure . pure
    Compose fgf <*> Compose fgx = Compose (liftA2 (<*>) fgf fgx)

我们可以再次使用标准技巧将Monoid提升为Applicative的{​​{1}}个。{/ p>

Applicative

现在我们免费获得矩阵!

instance (Monoid a, Applicative f, Applicative g) => Monoid ((f :.: g) a) where
    mempty = pure mempty
    mappend = liftA2 mappend

还有另一个有效矩阵m1 :: Mat Two Two String m1 = Compose (v1 :> v2 :> VNil) m2 :: Mat Two Two String m2 = Compose (v2 :> v1 :> VNil) ghci> m1 `mappend` m2 Compose {getCompose = ("foobaz" :> ("barquux" :> VNil)) :> (("bazfoo" :> ("quuxbar" :> VNil)) :> VNil)} -- ┌ ┐ ┌ ┐ ┌ ┐ -- | "foo" "bar" | + | "baz" "quux" | = | "foobaz" "barquux" | -- | "baz" "quux" | | "foo" "bar" | | "bazfoo" "quuxbar" | -- └ ┘ └ ┘ └ ┘ (对于方矩阵,Monoid),它执行矩阵乘法,单位矩阵为newtype Square n = Square (Mat n n)。我不会在这里展示它。你可以自己搞清楚。

最后让我们添加张量,它们是 n 维矩阵。张量是一系列由维度列表索引的类型。也就是说,mempty是从维度列表到类型构造函数Tensor函数。向列表添加新维度会添加一层* -> * s。

Vec

因此,type family Tensor (dims :: [Nat]) :: * -> * where Tensor '[d] = Vec d Tensor (d ': ds) = Vec d :.: Tensor ds ,一个二乘三张量,Tensor '[One, Two, Three] a - 与newtype同构。

再一次,通过Vec One (Vec Two (Vec Three a))Tensor来定义Vec,我们可以免费获得我们需要的实例。

:.: