Haskell printfはどのように機能しますか?


104

Haskellの型安全性は、依存型付けされた言語だけに勝るものはありません。しかし、Text.Printfには、いくぶん崩れしているように見える深い魔法があります。

> printf "%d\n" 3
3
> printf "%s %f %d" "foo" 3.3 3
foo 3.3 3

これの背後にある深い魔法は何ですか?Text.Printf.printf関数はどのようにこのような可変引数を取ることができますか?

Haskellで可変引数を可能にするために使用される一般的なテクニックは何ですか?それはどのように機能しますか?

(補足:この手法を使用すると、タイプセーフの一部が明らかに失われます。)

> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t

15
依存型を使用してタイプセーフなprintfのみを取得できます。
8

9
レナルトはまったく正しい。Haskellの型安全性は、Haskellよりも依存型が多い言語に次ぐものです。もちろん、形式として文字列よりも有益なタイプを選択すれば、printfのようなものをタイプセーフにすることができます。
11

3
printfの複数のバリアントについては、olegを参照してください:okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv

1
@augustss依存型またはテンプレートハスケルを使用した場合のみ、タイプセーフなprintfを取得できます。;-)
MathematicalOrchid

3
@MathematicalOrchidテンプレートHaskellはカウントされません。:)
2013年

回答:


131

秘訣は型クラスを使うことです。の場合printf、キーはPrintfType型クラスです。メソッドは公開されていませんが、重要な部分はとにかく型にあります。

class PrintfType r
printf :: PrintfType r => String -> r

だから、printfオーバーロードされた戻り値の型を持っています。私たちがインスタンス化できるようにする必要がありますので、些細なケースでは、我々は、余分な引数を持っていないrIO ()。このため、インスタンスがあります

instance PrintfType (IO ())

次に、可変数の引数をサポートするために、インスタンスレベルで再帰を使用する必要があります。場合ように、特に、我々は、インスタンスが必要rであるPrintfType、関数型でx -> rもありますPrintfType

-- instance PrintfType r => PrintfType (x -> r)

もちろん、実際にフォーマットできる引数のみをサポートする必要があります。ここで2番目のタイプのクラスPrintfArgが登場します。実際のインスタンスは

instance (PrintfArg x, PrintfType r) => PrintfType (x -> r)

これは、Showクラスの任意の数の引数を取り、それらを出力するだけの単純化されたバージョンです。

{-# LANGUAGE FlexibleInstances #-}

foo :: FooType a => a
foo = bar (return ())

class FooType a where
    bar :: IO () -> a

instance FooType (IO ()) where
    bar = id

instance (Show x, FooType r) => FooType (x -> r) where
    bar s x = bar (s >> print x)

ここでbarは、引数がなくなるまで再帰的に作成されるIOアクションを実行します。その時点で、引数を実行します。

*Main> foo 3 :: IO ()
3
*Main> foo 3 "hello" :: IO ()
3
"hello"
*Main> foo 3 "hello" True :: IO ()
3
"hello"
True

QuickCheckも同じ手法を使用します。この場合、Testableクラスには基本ケースのインスタンスがあり、クラスでBool引数を取る関数には再帰インスタンスがありますArbitrary

class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r) 

すばらしい答えです。私はhaskellが適用された引数に基づいてFooのタイプを理解していることを指摘したかっただけです。これを理解するには、次のようにFooのタイプを明示的に指定することができます。λ>(foo ::(Show x、Show y)=> x-> y-> IO())3 "hello"
redfish64

1
可変長引数部分がどのように実装されているかは理解していますが、コンパイラが拒否する方法はまだわかりませんprintf "%d" True。runtime(?)値"%d"がコンパイル時に解読されてを要求するように見えるので、これは私にとって非常に神秘的ですInt。これは私にとって絶対に不可解です。。。ソースコードは次のようなものを使用しない、特に以来DataKindsTemplateHaskell(。私は、ソースコードをチェックし、それを理解していない)
トーマスEding

2
@ThomasEdingコンパイラーが拒否する理由は、printf "%d" TrueBoolインスタンスがないためですPrintfArg。あなたが間違った型の引数を渡す場合はないのインスタンスを持っているがPrintfArg、それはコンパイルを行い、実行時に例外がスローされます。例:printf "%d" "hi"
Travis Sunderland
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.