【问题标题】:How to define a usable Applicative instance for a vector type enforcing length 2^n如何为强制长度为 2^n 的向量类型定义可用的 Applicative 实例
【发布时间】:2019-10-04 16:32:51
【问题描述】:

对于某些应用,我需要长度为 $2^n$ 的向量。为了强制某些操作的长度匹配,我用 ist applicative 实例定义了我的类型,如下所示:

{-# LANGUAGE GADTs, DataKinds, FlexibleInstances, FlexibleContexts #-}
data Nat = Z | N Nat
data Vector n t where
  S :: t -> Vector Z t
  V :: Vector n t -> Vector n t -> Vector (N n) t

instance Functor (Vector n) where
  fmap f (S t ) = S (f t)
  fmap f (V t t') = V (fmap f t) (fmap f t')

instance Applicative (Vector Z) where
  pure = S
  S f <*> S a = S (f a)

instance Applicative (Vector n) => Applicative (Vector (N n)) where
  pure a = let a' = pure a in V a' a'
  V f f' <*> V a a' = V (f <*> a) (f' <*> a')

我选择了 ghci 建议的语言扩展来编译代码。整个结构的灵感来自How to make fixed-length vectors instance of Applicative?。

当我尝试使用它时,问题就开始了:

instance Num t => Num (Vector n t) where
  v + v' = (+) <$> v <*> v'
  (*) = undefined
  abs = undefined
  signum = undefined
  fromInteger = undefined
  negate = undefined

添加这些行会触发以下错误:

• 无法推断(Applicative (Vector n)) 因使用“”而产生 从上下文:Num t 受 ... 的实例声明约束

• 在表达式中:(+) v > v' 在“+”的等式中: v + v' = (+) v > v' 在“Num (Vector n t)”的实例声明中

我在 Windows 7 上使用 Haskell Platform 8.0.2-a。

知道发生了什么吗?在链接的问题中,同样的技巧似乎有效!? (在第一行添加 KindSignatures 没有帮助,如果没有 FlexibleInstances/Contexts 我会收到编译器错误。)

【问题讨论】:

  • 你应该把它写成instance (Num t, Applicative (Vector n)) =&gt; Num (Vector n t) where,否则你确实不能使用(&lt;*&gt;)。
  • 非常感谢,我不知何故错误地认为编译器知道所有 (Vector n) 都是 Applicatives。
  • @JohnPoe 与直觉相反,出于非常微妙的原因,并非所有这些都是应用程序。如果F x :: Nat 是一个“卡住”的类型族(例如,F Bool 未定义),则不允许编译器解析Applicative (Vector (F Bool)),因为它无法将F Bool 评估为具体的Nat ,所以它不能选择实例。有一种Nat 的类型表达式不计算为具体的Nat 是很奇怪的,但这就是 GHC 的工作原理。
  • @chi:非常感谢您的解释,但可能超出了我目前对haskell的理解。我是否正确理解问题出在 Vector nt 中的 n,因为编译器必须能够证明它是具体 Nat x 的 Z 或 N x,但它不能,因为有人可能会插入一个“奇怪的仅“声称”为 Nat 的 n 的事物”?
  • @JohnPoe 是的,这是一个很好的表达方式。仅当类型中有明确的 Z 或 N x 时才会使用您的实例。如果有像n 这样的类型变量或“卡住”类型族F Bool,GHC 不知道要选择什么实例并且不会解决约束。有一些方法可以解决这个问题,例如利用合适的 GADT 来证明“n 确实是两种可能的形式之一”——不过这有点高级,所以我建议先熟悉 GADT。

标签: haskell


【解决方案1】:

你应该在你的Num (Vector n t)实例声明中添加一个类型约束,指定Vector n a是Applicative的一个实例,否则你不能在这里使用(&lt;*&gt;)。

因此,您可以通过以下方式解决问题:

instance (Num t, Applicative (Vector n)) => Num (Vector n t) where
  v + v' = (+) <$> v <*> v'
  -- ...

因此我们在这里说Vector n t 是Num 的一个实例,因为t 是Num 的一个实例,而Vector n 是Applicative 的一个实例。

由于您为您的Vector n 定义了您的instance Applicative 以使其适用于所有ns,因此所有Vector n ts 都是Num 的成员给定Num t,无论@ 的值如何987654338@,但它必须是 instance 声明签名的一部分。

【讨论】:

    【解决方案2】:

    我认为使用辅助类会更好一些。例如,我也更喜欢liftA2 而不是&lt;*&gt;,所以我会使用它;这不是必需的。请注意,您只需要区分 pure 的大小;压缩操作不需要那个。有一个权衡:如果您将压缩操作设为一种方法,那么它将倾向于内联,而如果它是一个函数,则通常不会。当向量足够小时,这可以平衡代码大小和速度。不过,这就是我可能会这样做的方式。

    class App' n where
      pure' :: a -> Vector n a
    
    instance App' 'Z where
      pure' = S
    
    instance App' n => App' ('N n) where
      pure' a = let a' = pure' a in V a' a'
    
    liftA2'
      :: (a -> b -> c)
      -> Vector n a
      -> Vector n b
      -> Vector n c
    liftA2' f = \xs -> go xs
      where
        go (S x) (S y) = S (f x y)
        go (V l1 r1) (V l2 r2) =
          V (go l1 l2) (go r1 r2)
    
    instance App' n => Applicative (Vector n) where
      pure = pure'
      -- import Control.Applicative to get the liftA2 method
      liftA2 = liftA2'
    

    【讨论】:

    • 感谢您的回答。我还不太清楚辅助类的优势(还),但我想如果 或 liftA2 的实现更复杂一点,那么平衡对我来说也会发生变化。
    • 功能与方法的问题:有什么区别?函数是否只是在外部级别定义的东西和在 where let 中定义的方法?
    猜你喜欢
    • 1970-01-01
    • 2019-06-30
    • 1970-01-01
    • 1970-01-01
    • 2018-05-15
    • 1970-01-01
    • 2017-01-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多