รูปแบบที่เกิดขึ้นจริงนั้นกว้างกว่าการเข้าถึงข้อมูล เป็นวิธีที่มีน้ำหนักเบาในการสร้างภาษาเฉพาะโดเมนที่ให้ AST แก่คุณแล้วมีล่ามอย่างน้อยหนึ่งตัวเพื่อ "เรียกใช้" AST ตามที่คุณต้องการ
ส่วน monad ฟรีเป็นเพียงวิธีที่สะดวกในการรับ AST ที่คุณสามารถประกอบโดยใช้สิ่งอำนวยความสะดวก monad มาตรฐานของ Haskell (เช่นการทำเครื่องหมาย) โดยไม่ต้องเขียนโค้ดที่กำหนดเองมากมาย นอกจากนี้ยังช่วยให้มั่นใจว่า DSL ของคุณcomposable : คุณสามารถกำหนดมันในส่วนแล้วใส่ชิ้นส่วนร่วมกันในทางโครงสร้างให้คุณใช้ประโยชน์จากแนวคิดปกติ Haskell เช่นฟังก์ชั่น
การใช้ monad ฟรีให้โครงสร้างของ DSL ที่สามารถคอมโพสิตได้ สิ่งที่คุณต้องทำคือระบุชิ้น คุณเพิ่งเขียนประเภทข้อมูลที่ครอบคลุมการดำเนินการทั้งหมดใน DSL ของคุณ การกระทำเหล่านี้สามารถทำอะไรก็ได้ไม่ใช่แค่การเข้าถึงข้อมูล อย่างไรก็ตามหากคุณระบุการเข้าถึงข้อมูลทั้งหมดของคุณเป็นการกระทำคุณจะได้รับ AST ที่ระบุการสืบค้นและคำสั่งทั้งหมดไปยังแหล่งข้อมูล จากนั้นคุณสามารถตีความสิ่งนี้ตามที่คุณต้องการ: เรียกใช้กับฐานข้อมูลสดเรียกใช้กับการจำลองเพียงบันทึกคำสั่งสำหรับการดีบักหรือลองเพิ่มประสิทธิภาพการสืบค้น
ให้ดูตัวอย่างง่าย ๆ สำหรับพูดเก็บค่าคีย์ สำหรับตอนนี้เราจะปฏิบัติต่อทั้งคีย์และค่าเป็นสตริง แต่คุณสามารถเพิ่มประเภทได้อย่างง่ายดาย
data DSL next = Get String (String -> next)
| Set String String next
| End
next
พารามิเตอร์ช่วยให้เรารวมการดำเนิน เราสามารถใช้สิ่งนี้เพื่อเขียนโปรแกรมที่ได้รับ "foo" และตั้งค่า "bar" ด้วยค่าดังกล่าว:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
ขออภัยนี่ไม่เพียงพอสำหรับ DSL ที่มีความหมาย เนื่องจากเราใช้next
ในการเขียนเรียงความชนิดของp1
จึงมีความยาวเท่ากับโปรแกรมของเรา (เช่น 3 คำสั่ง):
p1 :: DSL (DSL (DSL next))
ในตัวอย่างนี้การใช้next
สิ่งนี้ดูแปลก ๆ เล็กน้อย แต่สำคัญถ้าเราต้องการให้การกระทำของเรามีตัวแปรประเภทต่าง ๆ เราอาจต้องการพิมพ์get
และset
ตัวอย่างเช่น
โปรดทราบว่าแต่ละnext
ฟิลด์มีความแตกต่างกันอย่างไร คำแนะนำนี้เราสามารถใช้เพื่อสร้างDSL
นักแสดง:
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
อันที่จริงนี่เป็นวิธีเดียวที่ถูกต้องในการทำให้เป็น Functor ดังนั้นเราสามารถใช้deriving
เพื่อสร้างอินสแตนซ์โดยอัตโนมัติโดยเปิดใช้งานDeriveFunctor
ส่วนขยาย
ขั้นตอนต่อไปคือFree
พิมพ์เอง นั่นคือสิ่งที่เราใช้เพื่อแสดงโครงสร้าง AST ของเราต่อยอดจากDSL
ประเภท คุณสามารถคิดได้ว่ามันเหมือนกับรายการในระดับประเภทที่ "ข้อเสีย" เป็นเพียงการทำรังนักแสดงเช่นDSL
:
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
ดังนั้นเราสามารถใช้Free DSL next
เพื่อให้โปรแกรมที่มีขนาดต่างกันในประเภทเดียวกัน:
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
ซึ่งมีประเภทที่ดีกว่ามาก:
p2 :: Free DSL a
อย่างไรก็ตามการแสดงออกที่เกิดขึ้นจริงกับตัวสร้างทั้งหมดของมันยังคงอึดอัดใจที่จะใช้! นี่คือที่มาของส่วน monad ในขณะที่ชื่อ "free monad" หมายถึงFree
เป็น monad - ตราบใดที่f
(ในกรณีนี้DSL
) เป็น functor:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
ตอนนี้เราอยู่ที่ไหนสักแห่ง: เราสามารถใช้do
สัญลักษณ์เพื่อทำให้นิพจน์ DSL ของเราดีกว่า คำถามเดียวคือสิ่งที่จะใส่ในnext
? แนวคิดก็คือการใช้Free
โครงสร้างสำหรับการจัดวางดังนั้นเราจะใส่Return
สำหรับแต่ละฟิลด์ถัดไปและปล่อยให้การทำโน้ตทำหน้าที่ประปาทั้งหมด:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
นี้จะดีกว่า แต่ก็ยังค่อนข้างอึดอัดใจ เรามีFree
และReturn
ทั่วทุกสถานที่ มีความสุขมีรูปแบบที่เราสามารถใช้ประโยชน์ได้: วิธีที่เรา "ยก" การดำเนินการ DSL ให้Free
เหมือนเดิมเสมอ - เราใส่มันเข้าไปFree
และนำไปใช้Return
สำหรับnext
:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
ตอนนี้เมื่อใช้สิ่งนี้เราสามารถเขียนคำสั่งแต่ละเวอร์ชันที่ดีและมี DSL แบบเต็ม:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
ใช้นี่คือวิธีที่เราสามารถเขียนโปรแกรมของเรา:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
เคล็ดลับเรียบร้อยคือในขณะที่p4
ดูเหมือนโปรแกรมที่จำเป็นเล็กน้อย แต่จริงๆแล้วมันคือสำนวนที่มีคุณค่า
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
ดังนั้นส่วน monad ที่เป็นอิสระของรูปแบบทำให้เราได้รับ DSL ที่สร้างแผนภูมิต้นไม้ด้วยไวยากรณ์ที่ดี นอกจากนี้เรายังสามารถเขียน composable ย่อยต้นไม้โดยไม่ได้ใช้End
; ตัวอย่างเช่นเราสามารถมีfollow
คีย์ที่รับค่าของมันจากนั้นใช้คีย์นั้นเป็นคีย์:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
ตอนนี้follow
สามารถใช้งานได้ในโปรแกรมของเราเหมือนget
หรือset
:
p5 = do foo <- follow "foo"
set "bar" foo
end
ดังนั้นเราจึงมีองค์ประกอบที่ดีและสิ่งที่เป็นนามธรรมสำหรับ DSL ของเราเช่นกัน
ตอนนี้เรามีต้นไม้เราไปถึงครึ่งหลังของรูปแบบ: ล่าม เราสามารถตีความต้นไม้ แต่เราชอบเพียงแค่จับคู่รูปแบบบนต้นไม้ นี่จะให้เราเขียนโค้ดกับแหล่งข้อมูลจริงในIO
รวมถึงสิ่งอื่น ๆ นี่คือตัวอย่างของแหล่งเก็บข้อมูลสมมุติ:
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
สิ่งนี้จะประเมินDSL
ชิ้นส่วนอย่างมีความสุขแม้จะไม่ได้จบลงด้วยend
ก็ตาม อย่างมีความสุขที่เราสามารถสร้างความ "ปลอดภัย" รุ่นของฟังก์ชั่นที่ยอมรับเฉพาะโปรแกรมปิดด้วยโดยการตั้งค่าลายเซ็นประเภทการป้อนข้อมูลเพื่อend
(forall a. Free DSL a) -> IO ()
ในขณะที่ลายเซ็นเก่ายอมรับFree DSL a
สำหรับการใด ๆ a
(เช่นFree DSL String
, Free DSL Int
และอื่น ๆ ) รุ่นนี้ยอมรับเฉพาะFree DSL a
ที่เหมาะกับทุกคนที่เป็นไปได้a
-which end
เราเท่านั้นที่สามารถสร้างขึ้นด้วย สิ่งนี้รับประกันว่าเราจะไม่ลืมปิดการเชื่อมต่อเมื่อเราทำเสร็จแล้ว
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(เราไม่สามารถเริ่มด้วยการให้runIO
ประเภทนี้เพราะมันใช้งานไม่ได้สำหรับการโทรซ้ำของเราอย่างไรก็ตามเราสามารถย้ายคำจำกัดความของบล็อกrunIO
ไปเป็นwhere
บล็อกอินsafeRunIO
และรับเอฟเฟกต์เดียวกันได้
การรันโค้ดของเราIO
ไม่ใช่สิ่งเดียวที่เราทำได้ สำหรับการทดสอบเราอาจต้องการเรียกใช้กับ pure State Map
แทน การเขียนรหัสนั้นเป็นการออกกำลังกายที่ดี
นี่คือรูปแบบการแปล monad + interpreter เราสร้าง DSL โดยใช้ประโยชน์จากโครงสร้าง monad ฟรีเพื่อใช้งานระบบประปาทั้งหมด เราสามารถใช้การทำเครื่องหมายและฟังก์ชั่นมาตรฐาน monad กับ DSL ของเรา จากนั้นเพื่อใช้งานจริงเราต้องตีความมันอย่างใด; เนื่องจากต้นไม้เป็นเพียงโครงสร้างข้อมูลในท้ายที่สุดเราสามารถตีความได้อย่างไรก็ตามเราต้องการเพื่อวัตถุประสงค์ที่แตกต่างกัน
เมื่อเราใช้สิ่งนี้เพื่อจัดการการเข้าถึงที่เก็บข้อมูลภายนอกมันก็คล้ายกับรูปแบบ Repository มันเป็นตัวกลางระหว่างแหล่งข้อมูลของเราและรหัสของเราโดยแยกออกเป็นสองส่วน อย่างไรก็ตามในบางวิธีมันมีความเฉพาะเจาะจงมากขึ้น: "พื้นที่เก็บข้อมูล" เป็น DSL ที่มี AST อย่างชัดเจนซึ่งเราสามารถใช้งานได้ตามที่เราต้องการ
อย่างไรก็ตามรูปแบบของตัวเองนั้นกว้างกว่านั้นมาก สามารถใช้กับสิ่งต่าง ๆ มากมายซึ่งไม่จำเป็นต้องเกี่ยวข้องกับฐานข้อมูลหรือที่เก็บข้อมูลภายนอก เหมาะสมทุกที่ที่คุณต้องการควบคุมเอฟเฟกต์หรือเป้าหมายหลายอย่างสำหรับ DSL