如何避免编写这种类型的Haskell样板代码

时间:2015-06-16 06:06:39

标签: haskell boilerplate

我经常遇到这种情况,因为它很烦人。

假设我有一个sum类型,它可以包含x的实例或一堆与x无关的其他内容 -

data Foo x = X x | Y Int | Z String | ...(other constructors not involving x)

要声明一个Functor实例,我必须这样做 -

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ (Y y) = Y y
    fmap _ (Z z) = Z z
    ... And so on

而我想做的就是这个 -

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ a = a

即。我只关心X构造函数,所有其他构造函数都只是“通过”。但当然这不会编译,因为左侧的a与等式右侧的a不同。

有没有办法可以避免为其他构造函数编写这个样板文件?

4 个答案:

答案 0 :(得分:12)

这有两个主要的简单解决方案。

首先,对于简单类型,只需deriving (Functor)使用必要的扩展名。

另一种解决方案是定义另一种数据类型:

data Bar = S String | B Bool | I Int  -- "Inner" type
data Foo a = X a | Q Bar              -- "Outer" type

instance Functor Foo where
    fmap f (X a) = X (f a)
    fmap _ (Q b) = Q b -- `b' requires no type change. 

所以你可以再写一行来删除很多。

它不是模式匹配的理想选择,但至少可以解决这个问题。

答案 1 :(得分:10)

我认为我们希望针对一般情况提供解决方案,其中更改类型参数不一定在DeriveFunctor的正确位置。

我们可以区分两种情况。

在简单的情况下,输出数据类型不是递归的。在这里,prisms是一个合适的解决方案:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Foo x y = X x | Y y | Z String

makePrisms ''Foo

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX = over _X

如果我们的数据是递归的,那么事情会变得更复杂。现在makePrisms不会创建类型更改棱镜。我们可以通过将其分解为显式修复点来消除定义中的递归。这样我们的棱镜仍然可以改变类型:

import Control.Lens

newtype Fix f = Fix {out :: f (Fix f)}

-- k marks the recursive positions
-- so the original type would be "data Foo x y = ... | Two (Foo x y) (Foo x y)"
data FooF x y k = X x | Y y | Z String | Two k k deriving (Functor)

type Foo x y = Fix (FooF x y)

makePrisms ''FooF

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX f = 
   Fix .               -- rewrap 
   over _X f .         -- map f over X if possible
   fmap (mapOverX f) . -- map over recursively
   out                 -- unwrap

或者我们可以将自下而上的转型分解出来:

cata :: (Functor f) => (f a -> a) -> Fix f -> a
cata f = go where go = f . fmap go . out

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX f = cata (Fix . over _X f)

有大量关于使用仿函数的修复点进行泛型编程的文献,以及一些库,例如thisthis。您可能想要搜索"递归方案"供进一步参考。

答案 2 :(得分:8)

看起来像是棱镜的工作。

免责声明:我是镜头/棱镜新手。

{-# LANGUAGE TemplateHaskell   #-}

import Control.Lens
import Control.Lens.Prism

data Foo x = X x | Y Int | Z String deriving Show

makePrisms ''Foo

instance Functor Foo where
   -- super simple impl, by András Kovács
   fmap = over _X
   -- My overly complicated idea
   --    fmap f = id & outside _X .~ (X . f)
   -- Original still more complicated implementation below
   --     fmap f (X x) = X (f x)
   --     fmap _ a = id & outside _X .~ undefined $ a

用法:

*Main> fmap (++ "foo") (Y 3)
Y 3
*Main> fmap (++ "foo") (X "abc")
X "abcfoo"

答案 3 :(得分:3)

主要是为了完整性,这还是一种方法:

import Unsafe.Coerce

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ a = unsafeCoerce a

在您描述的情况下,这实际上是对unsafeCoere的安全使用。但有充分的理由避免这种情况:

  • 安全性取决于GHC如何编译数据结构和代码;正常程序员不应该知道的知识。
  • 它也不健壮:如果使用新构造函数X'x扩展数据类型,则不会生成警告,因为catch-all使此定义详尽无遗,然后任何事情都会发生。 (请注意@gallais)

因此,这个解决方案肯定是可取。