如何创建一个本身使用另一个多变量函数(例如 printf)的多变量函数?

rex*_*rex 5 printf haskell polyvariadic

我正在尝试编写以下函数,以便可以将其与Text.Printf.printf的可变数量的参数一起使用。我尝试使用类型类,但找不到任何不逐个字符和逐个参数处理格式字符串的示例。我希望能够捕获所有参数并将其立即传递给 printf 。

-- panic :: PrintfType r -> Int -> String -> r

panic lvl fmt =
    let fail :: String -> String -> a
        fail s s' = error $ printf "%s: %s" s s'
    in flip fail (show lvl) . printf fmt
Run Code Online (Sandbox Code Playgroud)

我想使用这样的函数:

panic 0 "Err"                   -- Exception: "Err: 0"
panic 1 "Err %s %d" "fatal" 42  -- Exception: "Err fatal 42: 1"
Run Code Online (Sandbox Code Playgroud)

我应该如何继续实施?

Sil*_*olo 6

这是可以做到的,但正如评论中所指出的,一切Text.Printf都是黑客。黑客是通过类型类解析来完成的。printf有一个名为的类型类(其实现未公开),PrintfType它为基本类型(IO ()String)实现,也为函数类型实现a -> r,这允许我们假装 Haskell 有可变参数。

\n

就像我说的,类型类实现没有公开。但幸运的是,我们可以自己编写。首先,让我们把标题移开。

\n
{-# LANGUAGE FlexibleInstances, FlexibleContexts, UndecidableInstances, TypeFamilies #-}\n
Run Code Online (Sandbox Code Playgroud)\n

与高级类型类功能一样,我们需要一些编译器扩展。现在这是我们要处理的类。

\n
class PanicType t where\n    type Prev t\n    spr :: (String -> Void) -> Prev t -> t\n
Run Code Online (Sandbox Code Playgroud)\n

我们的函数spr采用后处理函数,String -> Void该函数将通过调用 来获取“已编译”的字符串并触底error。然后我们进行计算的“上一步”并生成 a t,它可能需要一些参数并最终也会生成 a Void。(我们不会Void在类型类中对“最终”约束进行编码,但我们编写的实例将迫使我们无论如何都这样做)。

\n

作为一个插曲,这是我们必须做出的一个让步。理想情况下,您的恐慌函数最终会返回forall b. b。我不知道有什么好方法可以让可变参数函数做到这一点,因为会有大量重叠的类型类(每个类型类实例都必然与我们的“包罗万象”的类型类实例重叠)。这也许是可行的,但每次我在 Haskell 的OverlappingInstances一边闲逛时,都会以泪水结束。

\n

现在我们需要这个类型类的两个实例。我正在使用所有不错的“灵活”GHC 扩展,因此我们不必像printf以前那样做太多的黑客工作。一个例子是 for Void,我们在传递参数结束时遇到的基本术语。另一个将支持a -> r并承认另一个论点。

\n
instance PanicType Void where\n    type Prev Void = String\n    spr post k = post k\n
Run Code Online (Sandbox Code Playgroud)\n

当我们触及基础术语时,我们可以假设“我们已经建立的东西”延续参数是一个Stringerror因此,我们只需调用我们的后处理器(实际上,这将是-with-benefits)。

\n
instance (PrintfArg a, PanicType r, PrintfType (Prev r)) => PanicType (a -> r) where\n    type Prev (a -> r) = (a -> Prev r)\n    spr post k a = spr post (k a)\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们接受另一个论点。for 的“前一个”延续a -> r只是递归地定义为一个接受 ana并产生 a 的函数Prev r。然后我们的spr代表sprPanicType r

\n

请注意,最困难的部分是确定类型。spr一旦我们决定了和的类型Prev,函数实现就主要是技术性的,只需自己编写即可。

\n

现在对于panic. 我们需要调用spr结果r类型,并使用将调用的后处理参数error和启动printf. 像这样。

\n
panic :: (PanicType r, PrintfType (Prev r)) => Int -> String -> r\npanic lvl fmt = spr (\\s -> error (printf "%s: %s" s (show lvl))) (printf fmt)\n
Run Code Online (Sandbox Code Playgroud)\n

就像我说的,我们最终确实得到了 a Void,而不是 a forall b. b,所以你必须在实践中得到absurd结果。

\n
main :: IO ()\nmain = absurd (panic 0 "Err %s %d" "fatal" (42 :: Int))\n
Run Code Online (Sandbox Code Playgroud)\n

欢迎来到 Haskell 的阴暗面!在门口检查你的夹克,桌子上有饼干。

\n

在线尝试一下!

\n