这是一个简单的问题。我是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
答案 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 pixel
和a
可能不同。但是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)
(您可以分别将∀
和∃
视为forall
和exists
。)
区别?在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
类型,并且您可以执行任何可能的操作特定像素。