如何避免编写此类 Haskell 样板代码
How do I avoid writing this type of Haskell boilerplate code
我 运行 经常遇到这种情况,这很烦人。
假设我有一个 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
构造函数,所有其他构造函数都只是 "passed through"。但是当然这不会编译,因为左侧的 a
与等式右侧的 a
是不同的类型。
有什么方法可以避免为其他构造函数编写此样板文件?
对此有两个主要的简单解决方案。
首先,对于简单类型,只需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.
所以你可以多写一行来删除很多。
模式匹配不是很理想,但至少解决了这个问题。
看起来像棱镜的工作。
免责声明:我是 lens/prism 新手。
{-# 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"
我假设我们希望为 DeriveFunctor
的更改类型参数不一定位于正确位置的一般情况提供解决方案。
我们可以区分两种情况。
在简单的情况下,out 数据类型不是递归的。这里,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)
有大量关于使用仿函数定点进行泛型编程的文献,还有许多库,例如 this or this。您可能需要搜索 "recursion schemes" 以获取更多参考。
主要是为了完整性,这里还有一种方法:
import Unsafe.Coerce
instance Functor Foo where
fmap f (X x) = X (f x)
fmap _ a = unsafeCoerce a
在您描述的情况下,这实际上是 unsafeCoere
的安全使用。但有充分的理由避免这种情况:
- 安全取决于GHC如何编译数据结构和代码;普通程序员不需要掌握的知识。
- 它也不健壮:如果使用新的构造函数 X' x 扩展数据类型,则不会生成警告,因为包罗万象使这个定义变得详尽无遗,然后任何事情都会发生。 (感谢@gallais 的评论)
因此,此解决方案绝对不可取。
我 运行 经常遇到这种情况,这很烦人。
假设我有一个 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
构造函数,所有其他构造函数都只是 "passed through"。但是当然这不会编译,因为左侧的 a
与等式右侧的 a
是不同的类型。
有什么方法可以避免为其他构造函数编写此样板文件?
对此有两个主要的简单解决方案。
首先,对于简单类型,只需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.
所以你可以多写一行来删除很多。
模式匹配不是很理想,但至少解决了这个问题。
看起来像棱镜的工作。
免责声明:我是 lens/prism 新手。
{-# 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"
我假设我们希望为 DeriveFunctor
的更改类型参数不一定位于正确位置的一般情况提供解决方案。
我们可以区分两种情况。
在简单的情况下,out 数据类型不是递归的。这里,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)
有大量关于使用仿函数定点进行泛型编程的文献,还有许多库,例如 this or this。您可能需要搜索 "recursion schemes" 以获取更多参考。
主要是为了完整性,这里还有一种方法:
import Unsafe.Coerce
instance Functor Foo where
fmap f (X x) = X (f x)
fmap _ a = unsafeCoerce a
在您描述的情况下,这实际上是 unsafeCoere
的安全使用。但有充分的理由避免这种情况:
- 安全取决于GHC如何编译数据结构和代码;普通程序员不需要掌握的知识。
- 它也不健壮:如果使用新的构造函数 X' x 扩展数据类型,则不会生成警告,因为包罗万象使这个定义变得详尽无遗,然后任何事情都会发生。 (感谢@gallais 的评论)
因此,此解决方案绝对不可取。