假设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已经显示出难度。我对依赖类型只有一个模糊的想法,但这似乎是个人情况的典型例子对应于一个参数范围的唯一子集的类型将是有用的。)
答案 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'
答案 1 :(得分:3)
@ user3237465是关于将静态大小信息附加到Array
的问题的完整而直接的答案。但是既然你提到你对依赖类型很新,我想给出一个更简单的矩阵加法例子,我觉得它可以作为对该主题的更好的介绍。下面的大部分内容都可以在the Hasochism paper中找到(更好地解释!)。
像往常一样,我们有自然数字,GHC将自动提升到类型级别。以下data
声明不仅定义了类型Nat
和两个值构造函数Z
和S
,我们还获得种 {{1 }和两个类型构造函数Nat
和Z
。
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
上的模式匹配会告诉您Natty
。 n
量词告诉您forall n. Natty n ->
在运行时使用。因此,我们的n
函数的类型为vreplicate
,Natty 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 a
与Vec 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
,我们可以免费获得我们需要的实例。
:.: