这个答案的灵感来自 DDub,但我认为它更简单,它应该提供更好的类型推断和可能更好的类型错误。让我们先清清嗓子:
{-# language FlexibleContexts #-}
{-# language FlexibleInstances #-}
{-# language MultiParamTypeClasses #-}
{-# language DataKinds #-}
{-# language AllowAmbiguousTypes #-}
{-# language UndecidableInstances #-}
{-# language ScopedTypeVariables #-}
module DMap where
import Data.Kind (Type)
import GHC.TypeNats
GHC 的内置 Nats 很难使用,因为我们无法在“非 0”上进行模式匹配。所以让我们让它们只是接口的一部分,并在实现中避免它们。
-- Real unary naturals
data UNat = Z | S UNat
-- Convert 'Nat' to 'UNat' in the obvious way.
type family ToUnary (n :: Nat) where
ToUnary 0 = 'Z
ToUnary n = 'S (ToUnary (n - 1))
-- This is just a little wrapper function to deal with the
-- 'Nat'-to-'UNat' business.
dmap :: forall n s t a b. DMap (ToUnary n) s t a b
=> (a -> b) -> s -> t
dmap = dmap' @(ToUnary n)
现在我们已经摆脱了完全无聊的部分,剩下的就很简单了。
-- @n@ indicates how many 'Functor' layers to peel off @s@
-- and @t@ to reach @a@ and @b@, respectively.
class DMap (n :: UNat) s t a b where
dmap' :: (a -> b) -> s -> t
我们如何编写实例?让我们从显而易见的方式开始,然后将其转换为能够提供更好推理的方式。显而易见的方式:
instance DMap 'Z a b a b where
dmap' = id
instance (Functor f, DMap n x y a b)
=> DMap ('S n) (f x) (f y) a b where
dmap' = fmap . dmap' @n
这样写的问题是多参数实例解析的常见问题。只有当 GHC 发现第一个参数是 'Z 并且第二个和第四个参数相同并且第三个和第五个参数相同时,GHC 才会选择第一个实例.类似地,如果它看到第一个参数是'S 并且第二个参数是一个应用程序并且第三个参数是一个应用程序and第二个和第三个参数中应用的构造函数是一样的。
我们想在知道第一个参数后立即选择正确的实例。我们可以通过简单地将其他所有内容移到双箭头的左侧来做到这一点:
-- This stays the same.
class DMap (n :: UNat) s t a b where
dmap' :: (a -> b) -> s -> t
instance (s ~ a, t ~ b) => DMap 'Z s t a b where
dmap' = id
-- Notice how we're allowed to pull @f@, @x@,
-- and @y@ out of thin air here.
instance (Functor f, fx ~ (f x), fy ~ (f y), DMap n x y a b)
=> DMap ('S n) fx fy a b where
dmap' = fmap . dmap' @ n
现在,我在上面声称这提供了比 DDub 更好的类型推断,所以我最好支持它。让我拉一下GHCi:
*DMap> :t dmap @3
dmap @3
:: (Functor f1, Functor f2, Functor f3) =>
(a -> b) -> f1 (f2 (f3 a)) -> f1 (f2 (f3 b))
这正是fmap.fmap.fmap 的类型。完美的!使用 DDub 的代码,我反而得到了
dmap @3
:: (DMap (FType 3 c), DT (FType 3 c) a ~ c,
FType 3 (DT (FType 3 c) b) ~ FType 3 c) =>
(a -> b) -> c -> DT (FType 3 c) b
这……不太清楚。正如我在评论中提到的,这可以修复,但它会给已经有些复杂的代码增加一点复杂性。
只是为了好玩,我们可以用traverse 和foldMap 拉同样的把戏。
dtraverse :: forall n f s t a b. (DTraverse (ToUnary n) s t a b, Applicative f) => (a -> f b) -> s -> f t
dtraverse = dtraverse' @(ToUnary n)
class DTraverse (n :: UNat) s t a b where
dtraverse' :: Applicative f => (a -> f b) -> s -> f t
instance (s ~ a, t ~ b) => DTraverse 'Z s t a b where
dtraverse' = id
instance (Traversable t, tx ~ (t x), ty ~ (t y), DTraverse n x y a b) => DTraverse ('S n) tx ty a b where
dtraverse' = traverse . dtraverse' @ n
dfoldMap :: forall n m s a. (DFold (ToUnary n) s a, Monoid m) => (a -> m) -> s -> m
dfoldMap = dfoldMap' @(ToUnary n)
class DFold (n :: UNat) s a where
dfoldMap' :: Monoid m => (a -> m) -> s -> m
instance s ~ a => DFold 'Z s a where
dfoldMap' = id
instance (Foldable t, tx ~ (t x), DFold n x a) => DFold ('S n) tx a where
dfoldMap' = foldMap . dfoldMap' @ n