【问题标题】:Trying to understand class and instance试图理解类和实例
【发布时间】:2011-01-21 23:42:44
【问题描述】:

我已经完成了“真实世界的haskell”的一半,并认为我应该尝试编写我的第一个代码。没走多远……

基本上,我想我会尝试为双精度和双精度列表实现数组语法,这样我就可以对列表进行数值计算。我是否认为“类”就像 CLOS 中的通用函数?我用函数 arplus 创建了一个“类”来添加数组和标量,并使其适用于标量。现在我不知道如何对列表做同样的事情。 (计算很简单,我只是不知道在列表版本中用什么代替“双”)。

class Narray a where
    arplus :: a -> a -> a

instance Narray Double where
    arplus a b =  a + b

那么,我该如何做混合版本呢?数组标量和标量数组?

我需要类似的东西吗:

class Narray a where
   arplus :: a -> b -> a

改为?

作为我尝试编写的代码示例,请参见下面的 sbcl 代码: (注意 - 我已经用 lisp 中的数组完成了此操作,但我是在键入它们时在 haskell 中的列表中执行此操作的。就问题而言,这并不重要。)

;; Generic add for arrays and scalar
(defgeneric .+ (a b))

;; Scalar-scalar
(defmethod .+ ((a double-float) (b double-float)) (+ a b))

;; Array-array
(defmethod .+ ((a SB-KERNEL::SIMPLE-ARRAY-DOUBLE-FLOAT) 
       (b SB-KERNEL::SIMPLE-ARRAY-DOUBLE-FLOAT))
     (dotimes (i (array-total-size a))
        (setf (row-major-aref a i) 
        (+ (row-major-aref a i)
           (row-major-aref b i)))) a)

;; Array-scaler
(defmethod .+ ((a SB-KERNEL::SIMPLE-ARRAY-DOUBLE-FLOAT) (b double-float))
  (dotimes (i (array-total-size a))
      (setf (row-major-aref a i) 
        (+ (row-major-aref a i)
           b))) a)

;; Scalar-array
(defmethod .+ ((a double-float) (b SB-KERNEL::SIMPLE-ARRAY-DOUBLE-FLOAT))
  (dotimes (i (array-total-size b))
      (setf (row-major-aref b i) 
        (+ a 
           (row-major-aref b i)))) b)


;; Just to demo the code
(defun indgen (n) 
  (let ((r (make-array n :element-type 'double-float)))
    (dotimes (i n)
      (setf (row-major-aref r i) (coerce i 'double-float))) r))


* (load "arrays.lisp")

T
* (.+ (indgen 6) 10d0)

#(10.0d0 11.0d0 12.0d0 13.0d0 14.0d0 15.0d0)
* (.+ (indgen 6) (indgen 6))

#(0.0d0 2.0d0 4.0d0 6.0d0 8.0d0 10.0d0)
* 

【问题讨论】:

  • 附带说明,当您使用 Learn You A Haskell 来补充材料时,我发现 Real World Haskell 工作得非常好。
  • 您尝试做的事情并不能很好地与haskell 的“说出你的意思”理念相匹配。数组数组加法(压缩)是与标量数组加法(映射)不同的概念。因此,您将需要两个单独的运算符。当前约定在向量参数的一侧使用^:例如。 ^+^ 用于数组数组,+^ 用于标量数组。
  • AFAIK Haskell 不支持多种类型的分派,您可以编写类似class Narray a b c where arplus :: a -> b -> c 的内容,但您必须明确标记所有类型。
  • @adamax:没有必要明确标记所有类型;只是知道所有类型。如果你写arplus True () ++ "abc" 之类的东西,它会推断出Bool -> () -> String 就好了。问题是指定对于每个特定的a 和b,只有一个有效的c。

标签: haskell


【解决方案1】:

无法回答整个问题,但对于列表版本,我得到了这个工作:

main = [1, 2, 3] `arplus` [4, 5, 6]

class Narry a where
    arplus :: a → a → a

instance (Num a) ⇒ Narry [a] where
    arplus = zipWith (+)

我还尝试将 (Num a) => [a] 转换为 Num 的一个实例,效果不错。

instance (Num a) ⇒ Num [a] where
    (+) = zipWith (+)
    (*) = zipWith (*)
    (-) = zipWith (-)
    negate = map negate
    abs    = map abs
    signum = map signum
    fromInteger = repeat∘fromInteger

你可以这样试试

main = do print $ [1, 2, 3] * 3   -- [3, 6, 9]
          print $ 3 * [1, 2, 3]   -- [3, 6, 9]
          print $ 3 - [2, 4, 6]   -- [-1, 1, 3]
          print $ [2, 4, 6] + 7   -- [9, 11, 13]
          print $ abs [-2, 4, -3] -- [2, 4, 3]
          print $ [1, 2, 3] + [4.3, 5.5, 6.7] -- [5.3, 7.5, 9.7]
          print $ [1, 2, 3] * [3, 4, 5] -- [3, 8, 15]

当然,如果您想对列表进行双重运算,那么您将不得不为更多的东西编写定义。我不确定我在这里的定义可以做什么,尽管我怀疑它并没有比我演示的多多少。

此外,这不是使 Num 列表成为 Num 实例的唯一方法;对于 (*),可能有比 zipWith 更好的选择。

[编辑] 使列表成为 Fractional 的实例非常简单,并且增加了一些更方便的功能。

instance (Fractional a) => Fractional [a] where
    recip = map recip
    fromRational = repeat . fromRational

这使得列表与分数的交互成为可能,并且自动神奇地工作!

ghci> [3, 6, 9] / 3
[1.0,2.0,3.0]
ghci> 9 / [1, 2, 3]
[9.0,4.5,3.0]
ghci> 1.2 + [0, 1, 2]
[1.2,2.2,3.2]

权力越大,责任越大;仅在需要时才使用它们。如果您不需要这种行为,那么当您尝试在数字和列表上调用 + 时,让编译器对您大喊大叫会很好。

【讨论】:

    【解决方案2】:

    基本上,我想我会尝试为双精度和双精度列表实现数组语法,这样我就可以对列表进行数值计算。我认为“类”就像 CLOS 中的通用函数是否正确?

    Haskell 的类型类是一种实现有时称为“临时多态性”的方法,这意味着一个函数可以对多种类型进行操作,但对每种类型执行不同的操作。它们与大多数其他语言中的任何东西都不完全可比,但据我所知,CLOS 的“泛型函数”听起来很相似。

    那么,我该如何做混合版本呢?数组标量和标量数组?

    我假设你的数组和标量是不同的类型——这显然会造成问题,因为类型类只有一个类型参数。如果您使用的是 GHC,则有一个语言扩展 MultiParamTypeClasses 可以...基本上就是它所说的那样。

    不幸的是,由于各种原因,在实践中可能会有点痛苦,但在这种情况下,问题是您希望混合版本适用于任一参数顺序,但结果需要是两者的数组。最直接的方法是将结果作为第三个类型参数——但是 Haskell 无法知道 arplus 应用于数组并且标量是数组,因为类型参数彼此独立。为了解决这个问题,存在其他扩展,但对于某些任务来说,事情可能会很快变得复杂。

    现在,我对 lisps 有点生疏了,无法确保我正确阅读了您的示例,但这里是我认为您想要的内容:

    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE FunctionalDependencies #-}
    
    class Narray a b c | a b -> c where
        arplus :: a -> b -> c
    
    instance Narray Double Double Double where 
        arplus = (+)
    
    instance Narray Double [Double] [Double] where 
        arplus x = map (x +)
    
    instance Narray [Double] Double [Double] where 
        arplus x y = map (+ y) x
    
    instance Narray [Double] [Double] [Double] where 
        arplus = zipWith (+)
    

    您可以查看我在 GHC 文档中使用的扩展以了解更多详细信息。

    也就是说,请以上面为例说明如何进行简单的多调度样式重载,这在实践中不一定是一个好主意——正如@luqui 的评论所说,这个类型类将概念上不同的操作混为一谈,这样做并没有什么好处。

    我不确定关于更好的整体设计的讨论是否对您有用,但只要说在这种情况下我根本不会打扰类型类就足够了。

    【讨论】:

    • 感谢您的回答 camccann。我想也许你最后的评论说明了一切。当你学习一门新语言(当然是一种有趣的语言)时,最困难的部分通常是学习一种新的思维方式。您必须添加所有这些扩展的事实向我表明,我认为这一切都是错误的,并且我正在战斗而不是编写 haskell。在这里为标量和列表的运算符制作东西的正确思维过程是什么?
    猜你喜欢
    • 2016-07-18
    • 1970-01-01
    • 2015-01-26
    • 1970-01-01
    • 1970-01-01
    • 2013-04-13
    • 2012-03-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多