monad ผู้อ่านมีความซับซ้อนและดูเหมือนจะไม่มีประโยชน์ ในภาษาที่จำเป็นเช่น Java หรือ C ++ ไม่มีแนวคิดที่เทียบเท่าสำหรับผู้อ่าน monad ถ้าฉันไม่เข้าใจผิด
คุณช่วยยกตัวอย่างง่ายๆและทำให้ชัดเจนขึ้นหน่อยได้ไหม
monad ผู้อ่านมีความซับซ้อนและดูเหมือนจะไม่มีประโยชน์ ในภาษาที่จำเป็นเช่น Java หรือ C ++ ไม่มีแนวคิดที่เทียบเท่าสำหรับผู้อ่าน monad ถ้าฉันไม่เข้าใจผิด
คุณช่วยยกตัวอย่างง่ายๆและทำให้ชัดเจนขึ้นหน่อยได้ไหม
คำตอบ:
ไม่ต้องกลัว! ตัวอ่าน monad นั้นไม่ซับซ้อนและมียูทิลิตี้ที่ใช้งานง่ายอย่างแท้จริง
มีสองวิธีในการเข้าหาโมนาด: เราถามได้
จากแนวทางแรกผู้อ่าน monad เป็นนามธรรมบางประเภท
data Reader env a
ดังนั้น
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
แล้วเราจะใช้สิ่งนี้อย่างไร? ผู้อ่าน monad นั้นดีสำหรับการส่งผ่านข้อมูลการกำหนดค่า (โดยนัย) ผ่านการคำนวณ
เมื่อใดก็ตามที่คุณมี "ค่าคงที่" ในการคำนวณที่คุณต้องการในหลาย ๆ จุด แต่จริงๆแล้วคุณต้องการที่จะสามารถทำการคำนวณเดียวกันกับค่าที่แตกต่างกันคุณควรใช้ตัวอ่าน monad
monads อ่านยังจะใช้ในการทำในสิ่งที่คน OO เรียกฉีดพึ่งพา ตัวอย่างเช่นอัลกอริทึมNegamaxถูกใช้บ่อย (ในรูปแบบที่ได้รับการปรับให้เหมาะสมที่สุด) เพื่อคำนวณมูลค่าของตำแหน่งในเกมที่มีผู้เล่นสองคน แม้ว่าอัลกอริทึมจะไม่สนใจว่าคุณกำลังเล่นเกมอะไรอยู่ยกเว้นว่าคุณต้องสามารถกำหนดตำแหน่ง "ถัดไป" ในเกมได้และคุณต้องสามารถบอกได้ว่าตำแหน่งปัจจุบันเป็นตำแหน่งแห่งชัยชนะหรือไม่
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
จากนั้นสิ่งนี้จะใช้ได้กับเกมที่มีผู้เล่นสองคน
รูปแบบนี้มีประโยชน์แม้ในสิ่งที่ไม่ใช่การฉีดแบบพึ่งพาจริงๆ สมมติว่าคุณทำงานด้านการเงินคุณอาจออกแบบตรรกะที่ซับซ้อนบางอย่างสำหรับการกำหนดราคาสินทรัพย์ (คำพูดที่เป็นอนุพันธ์) ซึ่งเป็นสิ่งที่ดีและดีและคุณสามารถทำได้โดยไม่ต้องมีกลิ่นเหม็นใด ๆ แต่คุณได้ปรับเปลี่ยนโปรแกรมของคุณเพื่อจัดการกับสกุลเงินหลายสกุล คุณต้องสามารถแปลงระหว่างสกุลเงินได้ทันที ความพยายามครั้งแรกของคุณคือกำหนดฟังก์ชันระดับบนสุด
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
เพื่อรับราคาสปอต จากนั้นคุณสามารถเรียกพจนานุกรมนี้ในรหัสของคุณได้ .... แต่เดี๋ยวก่อน! ไม่ได้ผล! พจนานุกรมสกุลเงินนั้นไม่เปลี่ยนรูปและต้องเหมือนกันไม่เพียง แต่ตลอดอายุการใช้งานของโปรแกรมของคุณเท่านั้น แต่ยังรวมถึงเวลาที่รวบรวมด้วย ! แล้วคุณจะทำอย่างไร? ทางเลือกหนึ่งคือใช้ Reader monad:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
บางทีการใช้งานที่คลาสสิกที่สุดคือการใช้ล่าม แต่ก่อนที่เราจะดูนั้นเราต้องแนะนำฟังก์ชันอื่น
local :: (env -> env) -> Reader env a -> Reader env a
เอาล่ะเพื่อ Haskell และภาษาอื่น ๆ ที่ทำงานอยู่บนพื้นฐานของแคลคูลัสแลมบ์ดา แลมบ์ดาแคลคูลัสมีไวยากรณ์ที่ดูเหมือน
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
และเราต้องการเขียนผู้ประเมินสำหรับภาษานี้ ในการทำเช่นนั้นเราจะต้องติดตามสภาพแวดล้อมซึ่งเป็นรายการของการเชื่อมโยงที่เกี่ยวข้องกับคำศัพท์ (จริงๆแล้วมันจะปิดเพราะเราต้องการทำขอบเขตแบบคงที่)
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
เมื่อเสร็จแล้วเราควรหาค่า (หรือข้อผิดพลาด):
data Value = Lam String Closure | Failure String
ลองเขียนล่าม:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
ในที่สุดเราสามารถใช้มันได้โดยผ่านสภาพแวดล้อมที่ไม่สำคัญ:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
และนั่นก็คือ ล่ามที่ทำงานได้อย่างสมบูรณ์สำหรับแคลคูลัสแลมบ์ดา
อีกวิธีหนึ่งในการคิดเกี่ยวกับเรื่องนี้คือการถามว่ามีการดำเนินการอย่างไร คำตอบก็คือผู้อ่าน monad เป็นหนึ่งในตัวละครที่เรียบง่ายและสง่างามที่สุดในบรรดา monad ทั้งหมด
newtype Reader env a = Reader {runReader :: env -> a}
Reader เป็นเพียงชื่อแฟนซีสำหรับฟังก์ชัน! เราได้กำหนดไว้แล้วแล้วrunReader
ส่วนอื่น ๆ ของ API ล่ะ? ทุกคนMonad
ยังเป็นFunctor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
ตอนนี้เพื่อรับ monad:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
ซึ่งไม่น่ากลัวเท่าไหร่ ask
ง่ายมาก:
ask = Reader $ \x -> x
ในขณะที่local
ไม่เลวร้ายนัก:
local f (Reader g) = Reader $ \x -> runReader g (f x)
โอเคดังนั้นผู้อ่าน monad เป็นเพียงหน้าที่ ทำไมมี Reader เลย? คำถามที่ดี. ที่จริงคุณไม่ต้องการมัน!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
สิ่งเหล่านี้ง่ายกว่าด้วยซ้ำ มีอะไรมากกว่าที่ask
เป็นเพียงid
และlocal
เป็นเพียงองค์ประกอบของฟังก์ชั่นคำสั่งของฟังก์ชั่นเปลี่ยน!
Reader
ฟังก์ชันที่มีการใช้งานคลาสประเภท monad โดยเฉพาะหรือไม่? การพูดก่อนหน้านี้จะช่วยให้ฉันงงงวยน้อยลง ก่อนอื่นฉันไม่เข้าใจ ผ่านไปครึ่งทางฉันคิดว่า "โอ้มันช่วยให้คุณสามารถคืนของที่จะให้ผลลัพธ์ที่ต้องการเมื่อคุณระบุมูลค่าที่ขาดหายไป" ฉันคิดว่ามีประโยชน์ แต่ทันใดนั้นก็รู้ว่าฟังก์ชันทำสิ่งนี้ได้
local
ฟังก์ชั่นที่ไม่ต้องการคำอธิบายเพิ่มเติมบางส่วน แต่ ..
(Reader f) >>= g = (g (f x))
ไม่ได้หรือไม่?
x
?
ผมจำได้ว่าเป็นงงว่าคุณจนผมค้นพบตัวเองว่าสายพันธุ์ของผู้อ่าน monad มีทุกที่ ฉันค้นพบมันได้อย่างไร เพราะฉันเอาแต่เขียนโค้ดที่กลายเป็นรูปแบบเล็ก ๆ
ยกตัวอย่างเช่นที่จุดหนึ่งที่ผมเขียนโค้ดบางส่วนที่จะจัดการกับประวัติศาสตร์ค่า; ค่าที่เปลี่ยนแปลงตลอดเวลา แบบจำลองที่ง่ายมากของสิ่งนี้คือฟังก์ชันจากจุดเวลาไปจนถึงค่า ณ เวลานั้น:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
Applicative
เช่นหมายความว่าถ้าคุณมีemployees :: History Day [Person]
และcustomers :: History Day [Person]
คุณสามารถทำสิ่งนี้:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
กล่าวคือFunctor
และApplicative
อนุญาตให้เราปรับฟังก์ชั่นปกติที่ไม่ใช่ในอดีตเพื่อทำงานร่วมกับประวัติศาสตร์
อินสแตนซ์ monad (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
เป็นที่เข้าใจมากที่สุดสังหรณ์ใจโดยพิจารณาฟังก์ชัน ฟังก์ชันประเภทa -> History t b
คือฟังก์ชันที่จับคู่a
กับประวัติของb
ค่า ตัวอย่างเช่นคุณอาจมีและgetSupervisor :: Person -> History Day Supervisor
getVP :: Supervisor -> History Day VP
ดังนั้นตัวอย่าง Monad History
จึงเกี่ยวกับการเขียนฟังก์ชันเช่นนี้ ตัวอย่างเช่นgetSupervisor >=> getVP :: Person -> History Day VP
เป็นฟังก์ชันที่ได้รับPerson
ประวัติของVP
s ที่เคยมี
ดีนี้History
monad เป็นจริงว่าReader
เป็นเช่นเดียวกับ History t a
เหมือนกับReader t a
(ซึ่งเหมือนกับt -> a
) จริงๆ
อีกตัวอย่างหนึ่ง: ฉันได้สร้างต้นแบบการออกแบบOLAPใน Haskell เมื่อเร็ว ๆ นี้ แนวคิดหนึ่งที่นี่คือ "ไฮเปอร์คิวบ์" ซึ่งเป็นการจับคู่จากจุดตัดของชุดมิติข้อมูลไปยังค่า ที่นี่เราไปอีกครั้ง:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
การดำเนินการทั่วไปอย่างหนึ่งบนไฮเปอร์คิวบ์คือการใช้ฟังก์ชันสเกลาร์แบบหลายตำแหน่งกับจุดที่สอดคล้องกันของไฮเปอร์คิวบ์ สิ่งนี้เราจะได้รับโดยการกำหนดApplicative
อินสแตนซ์สำหรับHypercube
:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
ฉันแค่คัดลอกHistory
โค้ดด้านบนและเปลี่ยนชื่อ ในขณะที่คุณสามารถบอกได้นอกจากนี้ยังเป็นเพียงแค่Hypercube
Reader
มันดำเนินไปเรื่อย ๆ ตัวอย่างเช่นล่ามภาษาก็มีปัญหาReader
เช่นกันเมื่อคุณใช้โมเดลนี้:
Reader
ask
Reader
สภาพแวดล้อมการดำเนินการlocal
การเปรียบเทียบที่ดีก็คือเครื่องหมายReader r a
แสดงถึงa
"รู" ในนั้นซึ่งทำให้คุณไม่รู้ว่าa
เรากำลังพูดถึงอะไร คุณจะได้รับจริงก็ต่อa
เมื่อคุณจัดหาr
เพื่อเติมเต็มในหลุม มีหลายอย่างเช่นนั้น ในตัวอย่างด้านบน "ประวัติ" คือค่าที่คำนวณไม่ได้จนกว่าคุณจะระบุเวลาไฮเปอร์คิวบ์คือค่าที่คำนวณไม่ได้จนกว่าคุณจะระบุจุดตัดและนิพจน์ภาษาเป็นค่าที่สามารถ ไม่สามารถคำนวณได้จนกว่าคุณจะระบุค่าของตัวแปร นอกจากนี้ยังช่วยให้คุณมีสัญชาตญาณว่าทำไมReader r a
เป็นเช่นเดียวกับr -> a
เพราะฟังก์ชั่นดังกล่าวยังเป็นสังหรณ์ใจหายไปa
r
ดังนั้นFunctor
, Applicative
และMonad
กรณีของการReader
เป็นลักษณะทั่วไปมีประโยชน์มากสำหรับกรณีที่คุณกำลังสร้างแบบจำลองอะไรของการจัดเรียงที่ " a
ที่หายไปr
" และช่วยให้คุณในการรักษาเหล่านี้ "ไม่สมบูรณ์" วัตถุราวกับว่าพวกเขาสมบูรณ์
แต่วิธีการพูดในสิ่งเดียวกันอีกกReader r a
เป็นสิ่งที่กินr
และผลิตa
และFunctor
, Applicative
และMonad
กรณีที่มีรูปแบบพื้นฐานสำหรับการทำงานกับReader
s Functor
= สร้างReader
ที่ปรับเปลี่ยนผลลัพธ์ของอีกReader
; Applicative
= เชื่อมต่อสองReader
วินาทีกับอินพุตเดียวกันและรวมเอาท์พุท Monad
= ตรวจสอบผลมาจากการที่และใช้มันเพื่อสร้างอีกReader
และฟังก์ชั่น = ทำให้ที่ปรับเปลี่ยนการป้อนข้อมูลไปยังอีกReader
local
withReader
Reader
Reader
GeneralizedNewtypeDeriving
ส่วนขยายให้ได้มาซึ่งFunctor
, Applicative
, Monad
ฯลฯ สำหรับ Newtypes ขึ้นอยู่กับประเภทพื้นฐานของพวกเขา
ใน Java หรือ C ++ คุณสามารถเข้าถึงตัวแปรได้จากทุกที่โดยไม่มีปัญหา ปัญหาจะปรากฏขึ้นเมื่อรหัสของคุณกลายเป็นแบบมัลติเธรด
ใน Haskell คุณมีเพียงสองวิธีในการส่งผ่านค่าจากฟังก์ชันหนึ่งไปยังอีกฟังก์ชันหนึ่ง:
fn1 -> fn2 -> fn3
การทำงานfn2
อาจจะไม่ต้องใช้พารามิเตอร์ที่คุณผ่านจากไปfn1
fn3
Reader monad เพียงแค่ส่งข้อมูลที่คุณต้องการแบ่งปันระหว่างฟังก์ชันต่างๆ ฟังก์ชันอาจอ่านข้อมูลนั้น แต่ไม่สามารถเปลี่ยนแปลงได้ นั่นคือทั้งหมดที่ทำ Reader monad ดีเกือบทั้งหมด นอกจากนี้ยังมีฟังก์ชั่นต่างๆเช่นlocal
แต่เป็นครั้งแรกที่คุณสามารถใช้งานได้asks
เท่านั้น
do
-notation ซึ่งจะดีกว่าหากถูก refactored เป็นฟังก์ชันที่บริสุทธิ์
where
อนุประโยคจะได้รับการยอมรับเป็นวิธีที่ 3 ในการส่งผ่านตัวแปรหรือไม่?