รูปแบบ“ Free Monad + Interpreter” คืออะไร


95

ฉันเคยเห็นคนที่พูดถึงFree Monad กับ Interpreterโดยเฉพาะอย่างยิ่งในบริบทของการเข้าถึงข้อมูล รูปแบบนี้คืออะไร? ฉันควรใช้เมื่อใด มันทำงานอย่างไรและฉันจะใช้มันอย่างไร

ฉันเข้าใจ (จากโพสต์เช่นนี้ ) ว่ามันเกี่ยวกับการแยกแบบจำลองจากการเข้าถึงข้อมูล มันแตกต่างจากรูปแบบพื้นที่เก็บข้อมูลที่รู้จักกันดีอย่างไร ดูเหมือนว่าพวกเขาจะมีแรงจูงใจที่เหมือนกัน

คำตอบ:


138

รูปแบบที่เกิดขึ้นจริงนั้นกว้างกว่าการเข้าถึงข้อมูล เป็นวิธีที่มีน้ำหนักเบาในการสร้างภาษาเฉพาะโดเมนที่ให้ 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


6
ทำไมถึงเรียกว่า monad 'ฟรี'
Benjamin Hodgson

14
ชื่อ "free" มาจากทฤษฎีหมวดหมู่: ncatlab.org/nlab/show/free+objectแต่มันหมายถึงว่ามันเป็น "น้อยที่สุด" monad - การดำเนินการที่ถูกต้องเท่านั้นคือการดำเนินการ monad ตามที่มี " ลืม "มันคือโครงสร้างอื่นทั้งหมด
Boyd Stephen Smith Jr.

3
@BenjaminHodgson: Boyd พูดถูก ฉันจะไม่กังวลเกี่ยวกับมันมากนักถ้าคุณแค่อยากรู้ Dan Piponi พูดคุยอย่างยอดเยี่ยมเกี่ยวกับความหมายของ "ฟรี" ที่ BayHac ลองติดตามพร้อมกับสไลด์ของเขาเพราะภาพในวิดีโอนั้นไร้ประโยชน์อย่างสมบูรณ์
Tikhon Jelvis

3
Nitpick: "ส่วนของ monad ฟรีเป็นเพียง [การเน้นของฉัน] เป็นวิธีที่สะดวกในการรับ AST ที่คุณสามารถรวบรวมโดยใช้สิ่งอำนวยความสะดวก monad มาตรฐานของ Haskell (เช่น do-notation) โดยไม่ต้องเขียนโค้ดที่กำหนดเองมากมาย" มันเป็นมากกว่า "เพียงแค่" (อย่างที่ฉันแน่ใจว่าคุณรู้) นักบวชฟรีก็เป็นตัวแทนของโปรแกรมที่ทำให้ผู้แปลไม่สามารถแยกแยะระหว่างโปรแกรมที่คำdoอธิบายประกอบต่างกัน แต่ที่จริงแล้ว "หมายถึงสิ่งเดียวกัน"
sacundim

5
@sacundim: คุณช่วยอธิบายความคิดเห็นของคุณได้ไหม? โดยเฉพาะอย่างยิ่งประโยค 'monads ฟรียังเป็นการนำเสนอโปรแกรมแบบปกติที่ทำให้เป็นไปไม่ได้ที่ล่ามจะแยกความแตกต่างระหว่างโปรแกรมที่มีเครื่องหมายที่แตกต่างกัน แต่จริงๆแล้ว "หมายถึงเหมือนกัน"
Giorgio

15

ฟรี monad นั้นเป็น monad ที่สร้างโครงสร้างข้อมูลใน "รูปร่าง" เดียวกันกับการคำนวณแทนที่จะทำอะไรที่ซับซ้อนกว่า ( มีตัวอย่างที่จะพบคนออนไลน์. ) โครงสร้างข้อมูลนี้จะถูกส่งไปแล้วชิ้นส่วนของรหัสที่สิ้นเปลืองและดำเนินการการดำเนินงานที่. * ผมไม่ได้อย่างสิ้นเชิงคุ้นเคยกับรูปแบบการเก็บข้อมูล แต่จากสิ่งที่ฉันได้อ่านก็จะปรากฏขึ้น ที่จะเป็นสถาปัตยกรรมระดับที่สูงขึ้นและล่าม monad + ฟรีสามารถใช้ในการดำเนินการได้ ในทางกลับกันตัวแปล monad + ฟรีสามารถใช้ในการทำสิ่งต่าง ๆ เช่นตัวแยกวิเคราะห์

* เป็นที่น่าสังเกตว่าการทำแบบนี้ไม่ได้ จำกัด เฉพาะ monads และในความเป็นจริงสามารถผลิตรหัสที่มีประสิทธิภาพมากขึ้นด้วย applicatives ฟรีหรือลูกศรฟรี ( Parsers เป็นอีกตัวอย่างหนึ่งของสิ่งนี้ )


ขอโทษฉันควรชัดเจนเกี่ยวกับพื้นที่เก็บข้อมูล (ฉันลืมว่าไม่ใช่ทุกคนที่มีระบบธุรกิจ / พื้นหลัง OO / DDD!) พื้นที่เก็บข้อมูลโดยทั่วไปจะห่อหุ้มการเข้าถึงข้อมูลและทำให้วัตถุในโดเมนของคุณกลับมามีชีวิตชีวาอีกครั้ง มักใช้ควบคู่ไปกับ Dependency Inversion - คุณสามารถ 'เสียบ' การใช้งาน Repo ที่แตกต่างกัน (มีประโยชน์สำหรับการทดสอบหรือถ้าคุณต้องการเปลี่ยนฐานข้อมูลหรือ ORM) รหัสโดเมนเพียงแค่โทรrepository.Get()โดยที่ไม่รู้ว่ามันได้รับวัตถุโดเมนจากที่ใด
Benjamin Hodgson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.