Bagaimana cara kerja printf Haskell?

104

Keamanan tipe Haskell tidak ada duanya hanya untuk bahasa yang diketik secara dependen. Tapi ada beberapa keajaiban mendalam yang terjadi dengan Text.Printf yang sepertinya agak miring.

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

Apa keajaiban di balik ini? Bagaimana Text.Printf.printffungsi tersebut menerima argumen variadic seperti ini?

Apa teknik umum yang digunakan untuk memungkinkan argumen variadic di Haskell, dan bagaimana cara kerjanya?

(Catatan tambahan: beberapa jenis keamanan tampaknya hilang saat menggunakan teknik ini.)

> :t printf "%d\n" "foo"
printf "%d\n" "foo" :: (PrintfType ([Char] -> t)) => t
Dan Burton
sumber
15
Anda hanya bisa mendapatkan tipe safe printf menggunakan tipe dependen.
Agustus
9
Lennart benar. Keamanan jenis Haskell adalah yang kedua setelah bahasa dengan jenis yang lebih bergantung daripada Haskell. Tentu saja, Anda dapat membuat jenis hal seperti printf aman jika Anda memilih jenis yang lebih informatif daripada String untuk formatnya.
pigworker
3
lihat oleg untuk beberapa varian printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv
1
@augustss Anda hanya bisa mendapatkan cetakan yang aman jenis menggunakan tipe dependen ATAU TEMPLATE HASKELL! ;-)
MathematicalOrchid
3
@MathematicalOrchid Template Haskell tidak masuk hitungan. :)
Agustus

Jawaban:

131

Triknya adalah dengan menggunakan kelas tipe. Dalam kasus printf, kuncinya adalah PrintfTypekelas tipe. Itu tidak mengekspos metode apa pun, tetapi bagian yang penting adalah pada jenisnya.

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

Jadi printfmemiliki tipe pengembalian yang kelebihan beban. Dalam kasus sepele, kami tidak memiliki argumen tambahan, jadi kami harus dapat membuat contoh rke IO (). Untuk ini, kami memiliki contoh

instance PrintfType (IO ())

Selanjutnya, untuk mendukung sejumlah variabel argumen, kita perlu menggunakan rekursi di tingkat instance. Secara khusus kita membutuhkan sebuah contoh sehingga jika radalah a PrintfType, jenis fungsi x -> rjuga a PrintfType.

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

Tentu saja, kami hanya ingin mendukung argumen yang sebenarnya bisa diformat. Di situlah kelas tipe kedua PrintfArgmasuk Jadi contoh sebenarnya adalah

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

Berikut adalah versi sederhana yang mengambil sejumlah argumen di Showkelas dan hanya mencetaknya:

{-# 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)

Di sini, barmengambil tindakan IO yang dibangun secara rekursif hingga tidak ada lagi argumen, di mana kita cukup menjalankannya.

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

QuickCheck juga menggunakan teknik yang sama, di mana Testablekelas memiliki instance untuk kasus dasar Bool, dan rekursif untuk fungsi yang mengambil argumen di Arbitrarykelas.

class Testable a
instance Testable Bool
instance (Arbitrary x, Testable r) => Testable (x -> r) 
hammar
sumber
Jawaban yang bagus. Saya hanya ingin menunjukkan bahwa haskell sedang mencari tahu jenis Foo berdasarkan argumen yang diterapkan. Untuk memahami ini, Anda mungkin ingin menentukan tipe dari penjelasan Foo sebagai berikut: λ> (foo :: (Show x, Show y) => x -> y -> IO ()) 3 "hello"
redfish64
1
Sementara saya memahami bagaimana bagian argumen panjang variabel diimplementasikan, saya masih tidak mengerti bagaimana penyusun menolak printf "%d" True. Ini sangat mistis bagi saya, karena tampaknya nilai runtime (?) "%d"Diuraikan pada waktu kompilasi untuk memerlukan file Int. Ini benar-benar membingungkan saya. . . terutama karena kode sumber tidak menggunakan hal-hal seperti DataKindsatau TemplateHaskell(saya memeriksa kode sumber, tetapi tidak memahaminya.)
Thomas Eding
2
@ThomasEding Alasan compiler menolak printf "%d" Trueadalah karena tidak ada Boolinstance dari PrintfArg. Jika Anda meneruskan argumen dengan tipe yang salah yang memang memiliki instance PrintfArg, itu mengkompilasi dan melontarkan pengecualian pada waktu proses. Contoh:printf "%d" "hi"
Travis Sunderland