Haskell:为什么我不能在另一个函数中使用id,其中id的域显然是函数中所需类型的超集?

时间:2016-07-10 06:56:54

标签: haskell

这是一个简单的问题。我是Haskell的新手,使用JuicyPixels包玩一些图像。我使用DynamicImage将图像加载到GHCI中的decodePng对象中。 DynamicImage类型只是包含几种不同像素类型的图像的包装:

data DynamicImage =
       -- | A greyscale image.
       ImageY8    (Image Pixel8)
       -- | A greyscale image with 16bit components
     | ImageY16   (Image Pixel16)
       -- | A greyscale HDR image
     | ImageYF    (Image PixelF)
       -- | An image in greyscale with an alpha channel.
     | ImageYA8   (Image PixelYA8)
      -- | An image in greyscale with alpha channel on 16 bits.
     | ImageYA16  (Image PixelYA16)
     ...

我想要做的就是使用dynamicMap访问基础数据并查看我正在加载的像素类型。 dynamicMap的类型签名使用Rank2Types:

dynamicMap :: (forall pixel . (Pixel pixel) => Image pixel -> a)
           -> DynamicImage -> a
dynamicMap f (ImageY8    i) = f i
dynamicMap f (ImageY16   i) = f i
dynamicMap f (ImageYF    i) = f i
dynamicMap f (ImageYA8   i) = f i
...

它需要一个从图像到任何东西的函数,一个dynamicImage,以及应用于底层数据的函数。

为什么没有

getImage :: Pixel a => DynamicImage -> Image a
getImage img = dynamicMap id img

类型检查?该错误似乎是因为id函数在其输入中过于包容。

Couldn't match type `pixel' with `a'
      `pixel' is a rigid type variable bound by
              a type expected by the context:
                Pixel pixel => Image pixel -> Image a
              at <path>:24:16
      `a' is a rigid type variable bound by
          the type signature for
            getImage :: Pixel a => DynamicImage -> Image a
          at <path>:23:13
    Expected type: Image pixel -> Image a
      Actual type: Image a -> Image a
    Relevant bindings include
      getImage :: DynamicImage -> Image a
        (bound at <path>:24:1)
    In the first argument of `dynamicMap', namely `id'
    In the expression: dynamicMap id img

2 个答案:

答案 0 :(得分:6)

假设我们有img :: Image Pixel8并且运行

getImage (ImageY8 img) :: Image Pixel16

这怎么可能被不操纵位图的代码神奇地转换?肯定有些事情是错的。实际上,如果类型系统允许这样做,它将允许来自两种不同类型的危险演员,可能导致崩溃。在实践中,类型系统是健全的,并且正确地拒绝了这一点。

关键在于:

dynamicMap :: (forall pixel . (Pixel pixel) => Image pixel -> a)
           -> DynamicImage -> a

此类型是呼叫者和被呼叫者之间的契约。来电者可以选择a。然后调用者必须传递类型为forall pixel . (Pixel pixel) => Image pixel -> a的函数参数。这必须适用于所有pixel类型。 换句话说,被叫方(dynamicMap)可以选择pixel。无法保证被叫方会选择pixel以满足Image pixel ~ a。实际上,它不会在发布的代码中。因此,编译器假定Image pixela可能不同。但是id强制它们是相同的:调用者强加了一个限制被调用者选择的约束。

因此类型错误。

一个更简单的案例:

foo :: (forall a. a -> Int) -> Int
foo f = f "hello" + f (42 :: Int) + f True

bar :: Int
bar = foo id

此处bar传递的函数id :: Int -> Int不像forall a. a->Int那样通用 - 后者承诺将任何内容转换为Int,被调用者的选择。因此,报告了类型错误。

从技术上讲,id的类型为forall b. b->b,我们的目标是forall a. a->Int。无法替换类型b(可能涉及类型变量a的类型),以便b->b变为a->Int

答案 1 :(得分:3)

您似乎认为Haskell多态类型的工作方式与OO多态方法类似。他们没有。

签名getImage :: Pixel a => DynamicImage -> Image a实际上是

的简写
getImage :: ∀ a . Pixel a => DynamicImage -> Image a

一个类似的签名,例如C#宁可意味着

getImage :: c ~ Pixel => DynamicImage -> (∃ a . c a => Image a)

(您可以分别将视为forallexists。)
区别?在Haskell中,∀ a表明此函数适用于调用者可以选择的任何类型a (在它是像素类型的限制下)。如果您选择a ~ Pixel8,那么getImage必须能够提供Pixel8的图片,而不管输入图片的像素格式是什么。

OTOH,(∃ a . c a => Image a)(实际上不是合法的Haskell)意味着该函数返回某些类型的某些图像,但调用者没有发言权它会是什么像素格式。因此,除非您特别需要异构的图像集合,否则此功能实际上不太有用。但你已经可以这样做了:只需按原样存储DynamicImage

要正确进行存在量化,定制ADT通常是最好的方法;如果你想让它对所有 Pixel个实例(甚至可能只在将来定义的实例)保持开放,那么你也可以使用适当的存在,但你仍然需要将它们包含在内专用的data类型:

{-# LANGUAGE GADTs #-}

data DynamicImage' where
  DynamicImage' :: Pixel a => Image a -> DynamicImage'

但这通常更加繁琐,因为你不能在上使用哪种特定像素类型进行模式匹配。这就是你应该做的:

Prelude> :m +Codec.Picture.Png
Prelude Codec.Picture.Png> import qualified Data.ByteString as BS
Prelude Codec.Picture.Png BS> Right i <- decodePng <$> BS.readFile "/usr/share/icons/hicolor/22x22/devices/totem-tv.png" 
Prelude Codec.Picture.Png BS> i`seq`0   -- actually evaluate `i` to the first level,
                                        -- so we can see the pixel type
0
Prelude Codec.Picture.Png BS> :sprint i
i = Codec.Picture.Types.ImageRGBA8 _
Prelude Codec.Picture.Png BS> let Codec.Picture.Types.ImageRGBA8 i8 = i

如果您已经毫无错误地达到了这一点,那么您可以完全确定i8将具有RGBA8类型,并且您可以执行任何可能的操作特定像素。