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ฟังก์ชั่นใช้การขัดแย้ง variadic เช่นนี้หรือไม่

เทคนิคทั่วไปที่ใช้ในการอนุญาตให้มีอาร์กิวเมนต์ตัวแปรใน Haskell คืออะไรและทำงานอย่างไร

(หมายเหตุด้านข้าง: ความปลอดภัยบางประเภทหายไปเมื่อใช้เทคนิคนี้)

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

15
คุณสามารถรับ type safe printf โดยใช้ชนิดที่ขึ้นต่อกันเท่านั้น
สิงหาคม

9
Lennart ค่อนข้างถูกต้อง ประเภทความปลอดภัยของ Haskell เป็นภาษาที่สองรองจากภาษาที่มีประเภทที่ขึ้นกับมากกว่า Haskell แน่นอนว่าคุณสามารถสร้างสิ่งที่เหมือนพิมพ์ได้อย่างปลอดภัยหากคุณเลือกประเภทที่ให้ข้อมูลมากกว่าสตริงสำหรับรูปแบบ
pigworker

3
ดู oleg สำหรับรูปแบบต่างๆของ printf: okmij.org/ftp/typed-formatting/FPrintScan.html#DSL-In
sclv

1
@augustss คุณสามารถรับ printf ที่ปลอดภัยได้โดยใช้ชนิดที่ขึ้นกับหรือเทมเพลต HASKELL เท่านั้น! ;-)
MathematicalOrchid

3
@MathematicalOrchid Template Haskell ไม่นับ :)
augustss

คำตอบ:


131

เคล็ดลับคือการใช้คลาสประเภท ในกรณีของprintfคีย์คือPrintfTypeคลาสประเภท ไม่เปิดเผยวิธีการใด ๆ แต่ส่วนสำคัญอยู่ที่ประเภทอยู่ดี

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

ดังนั้นจึงprintfมีประเภทผลตอบแทนที่มากเกินไป เล็กน้อยในกรณีเราไม่มีข้อโต้แย้งพิเศษดังนั้นเราจึงจำเป็นที่จะสามารถยกตัวอย่างไปr IO ()สำหรับสิ่งนี้เรามีอินสแตนซ์

instance PrintfType (IO ())

ถัดไปเพื่อรองรับอาร์กิวเมนต์ที่มีจำนวนตัวแปรเราจำเป็นต้องใช้การเรียกซ้ำที่ระดับอินสแตนซ์ โดยเฉพาะอย่างยิ่งที่เราต้องการเช่นเพื่อที่ว่าถ้าrเป็นPrintfTypeประเภทฟังก์ชั่นนี้ยังมีx -> rPrintfType

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

แน่นอนว่าเราต้องการสนับสนุนข้อโต้แย้งที่สามารถจัดรูปแบบได้จริงเท่านั้น นั่นคือสิ่งที่คลาสประเภทที่สองเข้า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 :: (แสดง x, แสดง y) => x -> y -> IO ()) 3 "hello"
redfish64

1
แม้ว่าฉันจะเข้าใจวิธีการใช้งานส่วนอาร์กิวเมนต์ความยาวตัวแปร แต่ฉันก็ยังไม่เข้าใจว่าคอมไพเลอร์ปฏิเสธprintf "%d" Trueอย่างไร นี่เป็นเรื่องลึกลับสำหรับฉันเนื่องจากดูเหมือนว่าค่ารันไทม์ (?) "%d"จะถูกถอดรหัสในเวลาคอมไพล์เพื่อต้องการIntไฟล์. นี่เป็นเรื่องที่ทำให้ฉันงงงวยอย่างยิ่ง . . โดยเฉพาะอย่างยิ่งเนื่องจากซอร์สโค้ดไม่ได้ใช้สิ่งต่างๆเช่นDataKindsหรือTemplateHaskell(ฉันตรวจสอบซอร์สโค้ด แต่ไม่เข้าใจ)
Thomas Eding

2
@ThomasEding เหตุผลที่คอมไพลเลอร์ปฏิเสธprintf "%d" Trueเนื่องจากไม่มีBoolอินสแตนซ์ของPrintfArg. ถ้าคุณผ่านข้อโต้แย้งของผิดประเภทที่ไม่ได้มีตัวอย่างของPrintfArgมันจะรวบรวมและพ่นยกเว้นที่รันไทม์ เช่นprintf "%d" "hi"
Travis Sunderland
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.