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
是否可以在 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