【问题标题】:Applying a fixed-length-vector-function to the inital part of a longer fixed-length-vector将固定长度向量函数应用于较长的固定长度向量的初始部分
【发布时间】:2012-08-26 14:38:36
【问题描述】:

我有以下使用 ghcs 扩展 GADTsTypeOperatorsDataKinds 的固定长度向量定义:

data Vec n a where
    T    :: Vec VZero a
    (:.) :: a -> Vec n a -> Vec (VSucc n) a 

infixr 3 :.

data VNat  =  VZero |  VSucc VNat  -- ... promoting Kind VNat

type T1 = VSucc VZero
type T2 = VSucc T1

以及TypeOperator:+的以下定义:

type family (n::VNat) :+ (m::VNat) :: VNat 
type instance VZero :+ n = n
type instance VSucc n :+ m = VSucc (n :+ m)

为了使我的整个意图库有意义,我需要将(Vec n b)->(Vec m b) 类型的固定长度向量函数应用于更长向量Vec (n:+k) b 的初始部分。让我们将该函数称为prefixApp。它应该有类型

prefixApp :: ((Vec n b)->(Vec m b)) -> (Vec (n:+k) b) -> (Vec (m:+k) b)

这是一个示例应用程序,其固定长度矢量函数change2 定义如下:

change2 :: Vec T2 a -> Vec T2 a
change2 (x :. y :. T) = (y :. x :. T)

prefixApp 应该能够将change2 应用于长度>=2 的任何向量的前缀,例如

Vector> prefixApp change2 (1 :. 2 :. 3 :. 4:. T)
(2 :. 1 :. 3 :. 4 :. T)

有人知道如何实现prefixApp吗? (问题是,必须使用固定长度向量函数的一部分类型来获取正确大小的前缀......)

编辑: Daniel Wagners(非常聪明!)解决方案似乎与 ghc 7.6 的某些候选版本(不是官方版本!)一起使用。恕我直言,它不应该工作,但是,有两个原因:

  1. prefixApp 的类型声明在上下文中缺少 VNum m(以便 prepend (f b) 正确进行类型检查。
  2. 更有问题:ghc 7.4.2 不假定 TypeOperator :+ 在其第一个参数(也不是第二个,但这里不是必需的)中是单射的,这会导致类型错误:来自类型声明,我们知道vec 必须具有Vec (n:+k) a 类型,并且类型检查器推断定义右侧的表达式split vec 类型为Vec (n:+k0) a。但是:类型检查器无法推断出 k ~ k0(因为无法保证 :+ 是单射的)。

有人知道第二个问题的解决方案吗?如何在其第一个参数中声明 :+ 是单射的和/或如何完全避免遇到这个问题?

【问题讨论】:

  • 看起来这应该可以通过类型类和功能依赖来实现。不幸的是,我的原型代码似乎在 GHC 中出现了一个错误,所以我无法对其进行测试。
  • @gereeter:是的,我同意(理论上!)函数依赖应该是可能的,这实际上是我的第一个方法。不幸的是,由于我不清楚的原因,它没有起作用。不过,我会对使用函数依赖的解决方案感兴趣......

标签: haskell ghc type-systems dependent-type type-level-computation


【解决方案1】:

这是一个 split 不在类型类中的版本。在这里,我们为自然数 (SN) 构建了一个单例类型,它可以在 split 的定义中对“n”进行模式匹配。 然后可以通过使用类型类 (ToSN) 来隐藏这个额外的参数。

类型标签用于手动指定非推断参数。

(此答案与 Daniel Gustafsson 合着)

代码如下:

{-# LANGUAGE TypeFamilies, TypeOperators, DataKinds, GADTs, ScopedTypeVariables, FlexibleContexts #-}
module Vec where
data VNat = VZero | VSucc VNat  -- ... promoting Kind VNat

data Vec n a where
    T    :: Vec VZero a
    (:.) :: a -> Vec n a -> Vec (VSucc n) a·

infixr 3 :.

type T1 = VSucc VZero
type T2 = VSucc T1

data Tag (n::VNat) = Tag

data SN (n::VNat) where
  Z :: SN VZero
  S :: SN n -> SN (VSucc n)

class ToSN (n::VNat) where
  toSN :: SN n

instance ToSN VZero where
  toSN = Z

instance ToSN n => ToSN (VSucc n) where
  toSN = S toSN

type family (n::VNat) :+ (m::VNat) :: VNat
type instance VZero :+ n = n
type instance VSucc n :+ m = VSucc (n :+ m)

split' :: SN n -> Tag m -> Vec (n :+ m) a -> (Vec n a, Vec m a)
split' Z     _ xs = (T , xs)
split' (S n) _ (x :. xs) = let (as , bs) = split' n Tag xs in (x :. as , bs)

split :: ToSN n => Tag m -> Vec (n :+ m) a -> (Vec n a, Vec m a)
split = split' toSN

append :: Vec n a -> Vec m a -> Vec (n :+ m) a
append T ys = ys
append (x :. xs) ys = x :. append xs ys

prefixChange :: forall a m n k. ToSN n => (Vec n a -> Vec m a) -> Vec (n :+ k) a -> Vec (m :+ k) a
prefixChange f xs = let (as , bs) = split (Tag :: Tag k) xs in append (f as) bs

【讨论】:

  • 绝妙的方法!效果很好,我什至可以用这种技术定义一个tailApp 函数。不幸的是,进一步的抽象似乎使 Haskell 的类型系统过度紧张,例如我不能用prefixApptailApp 定义(***) 函数,比如(***) f g = prefixApp f . tailApp g。好吧,对于像 Haskell 这样的非依赖类型语言来说,也许它只是依赖类型太多......
【解决方案2】:

创建一个类:

class VNum (n::VNat) where
    split   :: Vec (n:+m) a -> (Vec n a, Vec m a)
    prepend :: Vec n a -> Vec m a -> Vec (n:+m) a

instance VNum VZero where
    split     v = (T, v)
    prepend _ v = v

instance VNum n => VNum (VSucc n) where
    split   (x :. xs)   = case split xs of (b, e) -> (x :. b, e)
    prepend (x :. xs) v = x :. prepend xs v

prefixApp :: VNum n => (Vec n a -> Vec m a) -> (Vec (n:+k) a -> (Vec (m:+k) a))
prefixApp f vec = case split vec of (b, e) -> prepend (f b) e

【讨论】:

  • 谢谢丹尼尔,非常好的回答! (老实说,我认为prefixApp 不会有定义)。但是:你的prefixApp 类型声明的上下文是否错过了额外的VNum m(对于prepend (f b) e 进行正确的类型检查?也许还有一个额外的VNum (n:+k) 用于split vec 的正确类型检查?
  • 第二个问题是类型检查器声称存在以下问题:vec 具有类型 Vec (n:+k) a(从 prefixApp 定义的左侧开始)和类型- checker 从表达式split vec 推断出Vec (n:+k0) a 的类型。但是:类型检查器不能推断出k~k0,因为不能保证类型函数:+ 是单射的,这对我来说似乎是合理的,因为我从未声明:+ 是单射的。是否有可能告诉 ghc :+ 在其第一个和第二个参数中都应该是单射的?
  • @phynfo 嗯。我检查了 GHC 7.6 接受了这个定义并结束了,但你提出了一些非常合理的反对意见;现在我不明白为什么它被接受了。
  • 我运行 ghc 7.4.1。是ghc 7.6。正式发布?
  • @phynfo 还没有,只有一个候选版本。
【解决方案3】:

如果您可以使用稍微不同类型的 prefixApp:

{-# LANGUAGE GADTs, TypeOperators, DataKinds, TypeFamilies #-}

import qualified Data.Foldable as F


data VNat  =  VZero |  VSucc VNat  -- ... promoting Kind VNat

type T1 = VSucc VZero
type T2 = VSucc T1
type T3 = VSucc T2

type family (n :: VNat) :+ (m :: VNat) :: VNat
type instance VZero :+ n = n
type instance VSucc n :+ m = VSucc (n :+ m)

type family (n :: VNat) :- (m :: VNat) :: VNat
type instance n :- VZero = n
type instance VSucc n :- VSucc m = n :- m


data Vec n a where
    T    :: Vec VZero a
    (:.) :: a -> Vec n a -> Vec (VSucc n) a 

infixr 3 :.

-- Just to define Show for Vec
instance F.Foldable (Vec n) where
    foldr _ b T = b
    foldr f b (a :. as) = a `f` F.foldr f b as

instance Show a => Show (Vec n a) where
    show = show . F.foldr (:) []


class Splitable (n::VNat) where
    split :: Vec k b -> (Vec n b, Vec (k:-n) b)

instance Splitable VZero where
    split r = (T,r)

instance Splitable n => Splitable (VSucc n) where
    split (x :. xs) =
        let (xs' , rs) = split xs
        in ((x :. xs') , rs)

append :: Vec n a -> Vec m a -> Vec (n:+m) a
append T r = r
append (l :. ls) r = l :. append ls r

prefixApp :: Splitable n => (Vec n b -> Vec m b) -> Vec k b -> Vec (m:+(k:-n)) b
prefixApp f v = let (v',rs) = split v in append (f v') rs

-- A test
inp :: Vec (T2 :+ T3) Int
inp = 1 :. 2 :. 3 :. 4:. 5 :. T

change2 :: Vec T2 a -> Vec T2 a
change2 (x :. y :. T) = (y :. x :. T)

test = prefixApp change2 inp -- -> [2,1,3,4,5]

实际上,您的原始签名也可以使用(带有增强的上下文):

prefixApp :: (Splitable n, (m :+ k) ~ (m :+ ((n :+ k) :- n))) =>
             ((Vec n b)->(Vec m b)) -> (Vec (n:+k) b) -> (Vec (m:+k) b)
prefixApp f v = let (v',rs) = split v in append (f v') rs

适用于 7.4.1

更新:只是为了好玩,Agda 中的解决方案:

data Nat : Set where
  zero : Nat
  succ : Nat -> Nat

_+_ : Nat -> Nat -> Nat
zero + r = r
succ n + r = succ (n + r)

data _*_ (A B : Set) : Set where
  _,_ : A -> B -> A * B

data Vec (A : Set) : Nat -> Set where
  [] : Vec A zero
  _::_ : {n : Nat} -> A -> Vec A n -> Vec A (succ n)

split : {A : Set}{k n : Nat} -> Vec A (n + k) -> (Vec A n) * (Vec A k)
split {_} {_} {zero} v = ([] , v)
split {_} {_} {succ _} (h :: t) with split t
... | (l , r) = ((h :: l) , r)

append : {A : Set}{n m : Nat} -> Vec A n -> Vec A m -> Vec A (n + m)
append [] r = r
append (h :: t) r with append t r
... | tr = h :: tr

prefixApp : {A : Set}{n m k : Nat} -> (Vec A n -> Vec A m) -> Vec A (n + k) -> Vec A (m + k)
prefixApp f v with split v
... | (l , r) = append (f l) r

【讨论】:

  • 哇,使用第二个 TypeOperator :- 的解决方案非常可爱;我很喜欢!不幸的是,类型签名变得有点难以阅读。如果能够以公理化的方式教 ghc 如何使用 :+:- 进行计算,那就太好了——就像您在 prefixApp 的上下文中为单个案例所做的那样,告诉 ghc m:+k等于m:+((n:+k):-n
  • 请注意,已经为可折叠实例定义了一个toList function,因此不需要单独的F.foldr (:) []
  • @phynfo 即使有办法,也绝对不是最简单的。可能是因为 Haskell 不是依赖类型语言。另一方面,例如在 Agda 中,您的任务的解决方案很简单(请参阅我的更新)。
  • @Ed'ka:你说得对,我真的很想使用 Agda。但是,我需要 Haskell 同时定义构造器类的能力。据我了解,Adga 无法处理构造函数类。真可惜。
  • @phynfo:我相信使用最新版本的 Agda (2.3.0) 可以模拟 Haskell 的类型类:wiki.portal.chalmers.se/agda/…
猜你喜欢
  • 2023-04-08
  • 2018-01-14
  • 1970-01-01
  • 2014-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-26
相关资源
最近更新 更多