6

在我简单的 Haskell DSL 中,我有以下函数来调用其他函数:

callF :: forall a. (Typeable a)
  => (V a) -> (V a)
callF fp@(V (FunP name)) =
  pack $ FunAppl (prettyV fp) []

callF1 :: forall a b. (Typeable a, Typeable b)
  => (V (V a -> V b)) -> V a -> (V b)
callF1 fp@(V (FunP name)) arg =
  pack $ FunAppl (prettyV fp) [prettyV arg]

callF2 :: forall a b c. (Typeable a, Typeable b, Typeable c)
  => (V (V a -> V b -> V c)) -> V a -> V b -> (V c)
callF2 fp@(V (FunP name)) arg1 arg2 =
  pack $ FunAppl (prettyV fp) [prettyV arg1, prettyV arg2]

我想将这一点推广到使用类型类的任意数量的参数。

这是我尝试过的,但它仅适用于 0 或 1 个参数,并且它不强制使用正确数量的参数调用函数。

class Callable a b | a -> b where
  call :: V a -> [String] -> b

instance Callable a (V b) where
  call fp@(V (FunP name)) x = pack $ FunAppl (prettyV fp) x

instance (Typeable c, Typeable d) => Callable (V a -> V b) (V c -> V d) where
  call fun@(V (FunP name)) list arg = call fun (list ++ [prettyV arg])
4

1 回答 1

9

具有多个参数的函数的常规技术——比如——是printf使用递归类型类。因为printf,这是通过一个名为 的类来完成的PrintfType。重要的见解是递归实例:

(PrintfArg a, PrintfType r) => PrintfType (a -> r)

这基本上是说,如果您可以返回 a PrintfType,那么您的函数也是一个实例。“基本情况”是一种类型,如String. 所以,如果你想printf一个参数调用,它会触发两个实例:PrintfType StringPrintfType (a -> r)where ris String。如果你想要两个参数,它是:String, (a -> r)where risString(a -> r)where ris the previous (a -> r)

但是,您的问题实际上要复杂一些。您希望有一个处理两个不同任务的实例。您希望您的实例适用于不同类型的函数(例如V (V a -> V b)V (V a -> V b -> V c)等等),并确保提供正确数量的参数。

这样做的第一步是停止使用[String]传递参数。该[String]类型会丢失有关它有多少值的信息,因此您无法检查是否存在适当数量的参数。相反,您应该为参数列表使用一种类型,以反映它有多少参数。

这种类型可能看起来像这样:

data a :. b = a :. b

它只是一种组合其他两种类型的类型,可以这样使用:

"foo" :. "bar"          :: String :. String
"foo" :. "bar" :. "baz" :: String :. String :. String

现在您只需要编写一个带有递归实例的类型类,该实例遍历类型级参数列表和函数本身。这是我的意思的一个非常粗略的独立草图;您必须自己采用它来解决您的特定问题。

infixr 8 :.
data a :. b = a :. b

class Callable f a b | f -> a b where
  call :: V f -> a -> b

instance Callable rf ra (V rb) => Callable (String -> rf) (String :. ra) (V rb) where
  call (V f) (a :. rest) = call (V (f a)) rest

instance Callable String () (V String) where
  call (V f) () = V f

您还必须启用一些扩展FlexibleInstancesFucntionalDepenedenciesUndecidableInstances.

然后你可以像这样使用它:

*Main> call (V "foo") ()
V "foo"
*Main> call (V (\ x -> "foo " ++ x)) ("bar" :. ())
V "foo bar"
*Main> call (V (\ x y -> "foo " ++ x ++ y)) ("bar" :. " baz" :. ())
V "foo bar baz"

如果你传入错误数量的参数,你会得到一个类型错误。诚然,这不是世界上最漂亮的错误信息!也就是说,错误 ( Couldn't match type `()' with `[Char] :. ()')的重要部分确实指出了核心问题(不匹配的参数列表),这应该很容易理解。

*Main> call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :. ())

<interactive>:101:1:
    Couldn't match type `()' with `[Char] :. ()'
    When using functional dependencies to combine
      Callable String () (V String),
        arising from the dependency `f -> a b'
        in the instance declaration at /home/tikhon/Documents/so/call.hs:16:14
      Callable [Char] ([Char] :. ()) (V [Char]),
        arising from a use of `call' at <interactive>:101:1-4
    In the expression:
      call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :. ())
    In an equation for `it':
        it = call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :. ())

请注意,这对于您的特定任务可能有点过于复杂——我不相信这是解决问题的最佳方法。但这是一个很好的练习,可以使用一些更高级的类型类特性来强制执行更复杂的类型级不变量。

于 2013-05-16T15:59:57.737 回答