Haskell 中的可变函数类型类

Variadic Function Typeclass in Haskell

是否可以在 Haskell 中定义一个类型类,使得一个函数可以执行某种行为而不管参数的数量?例如:

{-# LANGUAGE FlexibleInstances #-}

class FillZeroes ... where
  ...

f :: Num a => a -> a -> a
f x y = x+y+1

g :: Num a => a -> a
g x = 10*x

h :: (Eq a,Num a) => a -> Bool
h 0 = False
h _ = True

fillZeroes f === f 0 0 === 1
fillZeroes g === g 0 === 0
fillZeroes h === h 0 === False

可变参数函数当然是可行的,但是“填充”零有点棘手。这主要是因为GHC其实并不知道要填多少!以你的函数 f 为例。您可能认为“填零”意味着将 0 应用到 f 两次是显而易见的,但您确定吗?就 GHC 而言,也许您希望结果是来自 a -> a 的函数。或者,也许您已经使 Int -> Int 类型的函数具有一个 Num 实例 — 那么,也许 fillZeros 应该首先向 f 提供两个 0 :: Int -> Int 参数,然后0 :: Int 也是。有很多选择!

基本上这可以归结为您可以创建一个 fillZeros 函数,但您必须需要类型注释才能使用它做任何有用的事情。有了这个警告,让我们开始吧。


class 的一个很好的开始尝试是:

{-# LANGUAGE MultiParamTypeClasses #-}

class FillZeroes x y where
  fillZeroes :: x -> y

instance (Num a, FillZeroes b c) => FillZeroes (a -> b) c where
  fillZeroes f = fillZeroes (f 0)

在这里,fillZeroes 是一个接受 x 并产生 y 的函数,我们创建了一个实例,其中 x 是函数类型a -> b(其中 Num a)。注意这看起来是如何递归的,在实现中使用 FillZeroes b c 约束和对 fillZeroes (f 0) 的调用。如果这是递归情况,那么显然我们需要一个基本情况。我们怎么写这个?最简单的选择是使用重叠实例:

instance {-# OVERLAPPABLE #-} FillZeroes a a where
  fillZeroes = id

这意味着如果没有其他实例适用(即,如果 x 类型是 而不是 可以接受数字的函数),那么不要做更多的事情。让我们看看会发生什么(请注意,删除任一类型注释都会导致错误):

{-# LANGUAGE TypeApplications #-}

> fillZeroes (f @Int) :: Int
1
> fillZeroes (f @Int 4) :: Int
5
> fillZeroes (g @Int) :: Int
0
> fillZeroes (h @Int) :: Bool
False

我们可以做得更好吗?为什么我们需要告诉 GHC 函数的具体类型(我们使用 @Int 类型应用程序) 结果类型?问题是 GHC 需要具体了解 x y 类型才能获得正确的类型 class 实例。事实证明,我们可以使用类型族来绕过这个障碍。考虑以下类型族:

{-# LANGUAGE TypeFamilies #-}

type family FZResult a where
  FZResult (a -> b) = FZResult b
  FZResult a = a

这个(封闭的)类型族产生了我们正在寻找的结果类型。所以,我们可以将其分入 class 代替 y 参数:

class FillZeroes a where
  fillZeroes :: a -> FZResult a

instance {-# OVERLAPPABLE #-} FZResult a ~ a => FillZeroes a where
  fillZeroes = id

instance (Num b, FillZeroes a) => FillZeroes (b -> a) where
  fillZeroes f = fillZeroes (f 0)

现在,我们只需要告诉GHC具体的参数类型是什么,它就可以根据类型族来决定要填充多少个零:

> fillZeroes (f @Int)
1
> fillZeroes (f @Int 4)
5
> fillZeroes (g @Int)
0
> fillZeroes (h @Int)
False