คุณไม่สามารถสร้างฟังก์ชั่นบริสุทธิ์ที่เรียกrandom
ว่าจะให้ผลลัพธ์ที่แตกต่างทุกครั้งที่มีการเรียก ในความเป็นจริงคุณไม่สามารถแม้แต่เรียก "บริสุทธิ์" ฟังก์ชั่น คุณใช้พวกเขา ดังนั้นคุณจะไม่พลาดอะไรเลย แต่นี่ไม่ได้หมายความว่าตัวเลขสุ่มนั้นมีขีด จำกัด ในการเขียนโปรแกรมเชิงฟังก์ชัน อนุญาตให้ฉันสาธิตฉันจะใช้ไวยากรณ์ Haskell ตลอด
มาจากพื้นหลังที่จำเป็นคุณอาจคาดหวังว่าสุ่มมีประเภทดังนี้:
random :: () -> Integer
แต่สิ่งนี้ได้ถูกตัดออกไปแล้วเพราะการสุ่มไม่สามารถทำหน้าที่บริสุทธิ์ได้
พิจารณาความคิดของค่า คุณค่าคือสิ่งที่ไม่เปลี่ยนรูป มันไม่เคยเปลี่ยนแปลงและการสังเกตทุกครั้งที่คุณสามารถทำได้เกี่ยวกับมันสอดคล้องกันตลอดเวลา
เห็นได้ชัดว่าการสุ่มไม่สามารถสร้างค่าจำนวนเต็มได้ แต่จะสร้างตัวแปรสุ่มจำนวนเต็ม ประเภทของมันอาจเป็นแบบนี้:
random :: () -> Random Integer
ยกเว้นว่าผ่านการโต้แย้งไม่จำเป็นครบถ้วน, ฟังก์ชั่นที่บริสุทธิ์ดังนั้นหนึ่งเป็นดีอีกrandom ()
random ()
ฉันจะให้สุ่มจากนี้ไปประเภทนี้:
random :: Random Integer
ซึ่งเป็นสิ่งที่ดีและดี แต่ไม่มีประโยชน์มาก คุณอาจคาดหวังว่าจะสามารถเขียนนิพจน์เช่นrandom + 42
แต่คุณทำไม่ได้เพราะจะไม่พิมพ์ดีด คุณไม่สามารถทำอะไรกับตัวแปรสุ่มได้
นี่ทำให้เกิดคำถามที่น่าสนใจ มีฟังก์ชั่นใดที่ควรจัดการกับตัวแปรสุ่ม
ฟังก์ชั่นนี้ไม่สามารถอยู่ได้:
bad :: Random a -> a
ในวิธีที่มีประโยชน์เพราะคุณสามารถเขียน:
badRandom :: Integer
badRandom = bad random
ซึ่งแนะนำความไม่ลงรอยกัน badRandom เป็นค่าที่คาดคะเน แต่ก็เป็นตัวเลขสุ่มด้วยเช่นกัน ความขัดแย้ง
บางทีเราควรเพิ่มฟังก์ชั่นนี้:
randomAdd :: Integer -> Random Integer -> Random Integer
แต่นี่เป็นกรณีพิเศษของรูปแบบทั่วไปที่มากกว่า คุณควรจะสามารถใช้ฟังก์ชั่นใด ๆ กับสิ่งที่สุ่มเพื่อรับสิ่งอื่น ๆ ที่เป็นเช่น
randomMap :: (a -> b) -> Random a -> Random b
แทนที่จะเขียนrandom + 42
เราสามารถเขียนrandomMap (+42) random
ได้
หากทั้งหมดที่คุณมีคือ RandomMap คุณจะไม่สามารถรวมตัวแปรสุ่มเข้าด้วยกันได้ คุณไม่สามารถเขียนฟังก์ชันนี้ได้เช่น:
randomCombine :: Random a -> Random b -> Random (a, b)
คุณอาจลองเขียนแบบนี้:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
แต่มันก็มีประเภทที่ไม่ถูกต้อง แทนที่จะลงท้ายด้วย a Random (a, b)
เราจะจบด้วย aRandom (Random (a, b))
สิ่งนี้สามารถแก้ไขได้โดยการเพิ่มฟังก์ชั่นอื่น:
randomJoin :: Random (Random a) -> Random a
แต่ด้วยเหตุผลที่ชัดเจนในที่สุดฉันจะไม่ทำเช่นนั้น ฉันจะเพิ่มสิ่งนี้แทน:
randomBind :: Random a -> (a -> Random b) -> Random b
ไม่ชัดเจนในทันทีว่านี่จะแก้ปัญหาได้จริง แต่ก็เป็นเช่นนั้น:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
ในความเป็นจริงมันเป็นไปได้ที่จะเขียน randomBind ในแง่ของ randomJoin และ randomMap นอกจากนี้ยังเป็นไปได้ที่จะเขียน randomJoin ในแง่ของ randomBind แต่ฉันจะออกไปทำแบบนี้เป็นการออกกำลังกาย
เราสามารถทำให้มันง่ายขึ้นได้เล็กน้อย อนุญาตให้ฉันกำหนดฟังก์ชันนี้:
randomUnit :: a -> Random a
randomUnit เปลี่ยนค่าเป็นตัวแปรสุ่ม ซึ่งหมายความว่าเราสามารถมีตัวแปรสุ่มที่ไม่ได้สุ่มจริง แม้ว่าจะเป็นเช่นนี้เสมอ เราสามารถทำrandomMap (const 4) random
มาก่อน เหตุผลที่กำหนด randomUnit เป็นความคิดที่ดีคือตอนนี้เราสามารถกำหนด randomMap ในแง่ของ randomUnit และ randomBind:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
ตกลงตอนนี้เรากำลังเดินทางไปที่อื่น เรามีตัวแปรสุ่มที่เราสามารถจัดการได้ อย่างไรก็ตาม:
- ไม่ชัดเจนว่าเราจะใช้ฟังก์ชันเหล่านี้อย่างไร
- มันค่อนข้างยุ่งยาก
การดำเนินงาน
ฉันจะเล่นเลขสุ่มหลอก เป็นไปได้ที่จะใช้ฟังก์ชั่นเหล่านี้สำหรับตัวเลขสุ่มจริง แต่คำตอบนี้เริ่มค่อนข้างยาวแล้ว
โดยพื้นฐานแล้ววิธีนี้จะใช้งานได้ก็คือเราจะส่งค่าเมล็ดพันธุ์ไปทั่วทุกแห่ง เมื่อใดก็ตามที่เราสร้างค่าสุ่มใหม่เราจะผลิตเมล็ดพันธุ์ใหม่ ในตอนท้ายเมื่อเราสร้างตัวแปรสุ่มเสร็จแล้วเราจะต้องการตัวอย่างจากมันโดยใช้ฟังก์ชั่นนี้:
runRandom :: Seed -> Random a -> a
ฉันจะกำหนดประเภทแบบสุ่มเช่นนี้:
data Random a = Random (Seed -> (Seed, a))
จากนั้นเราเพียงแค่ต้องทำการใช้งานของ randomUnit, randomBind, runRandom และ random ซึ่งค่อนข้างตรงไปตรงมา:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
สำหรับการสุ่มฉันจะสมมติว่ามีฟังก์ชั่นของประเภท:
psuedoRandom :: Seed -> (Seed, Integer)
Random psuedoRandom
ซึ่งในกรณีนี้เป็นเพียงการสุ่ม
ทำสิ่งที่ยุ่งยากน้อยลง
Haskell มีน้ำตาลประโยคเพื่อทำสิ่งต่าง ๆ เช่นนี้ดีกว่าในสายตา มันเรียกว่าการทำเครื่องหมายและใช้ทุกอย่างที่เราต้องทำเพื่อสร้างตัวอย่างของ Monad สำหรับการสุ่ม
instance Monad Random where
return = randomUnit
(>>=) = randomBind
เสร็จสิ้น randomCombine
ก่อนหน้านี้สามารถเขียนได้:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
ถ้าฉันทำสิ่งนี้เพื่อตัวฉันเองฉันจะก้าวไปอีกขั้นหนึ่งไกลกว่านี้และสร้างตัวอย่างของการใช้งาน (ไม่ต้องกังวลหากไม่มีเหตุผล)
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
จากนั้น RandomCombine สามารถเขียนได้:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
ตอนนี้เรามีอินสแตนซ์เหล่านี้เราสามารถใช้>>=
แทน randomBind เข้าร่วมแทน randomJoin, fmap แทน randomMap กลับมาแทน randomUnit นอกจากนี้เรายังรับฟังก์ชั่นโหลดฟรีมากมาย
มันคุ้มหรือไม่ คุณอาจโต้เถียงว่าการเข้าสู่ขั้นตอนนี้ซึ่งการทำงานกับตัวเลขสุ่มไม่น่ากลัวอย่างสมบูรณ์นั้นค่อนข้างยากและยืดยาว เราได้แลกเปลี่ยนอะไรกับความพยายามนี้
รางวัลที่ฉับพลันที่สุดคือตอนนี้เราสามารถเห็นได้ว่าส่วนใดของโปรแกรมของเราขึ้นอยู่กับการสุ่มและส่วนใดที่กำหนดไว้ทั้งหมด จากประสบการณ์ของฉันการบังคับให้แยกจากกันอย่างเข้มงวดเช่นนี้ทำให้สิ่งต่าง ๆ ง่ายขึ้นอย่างมาก
เราสันนิษฐานว่าเราต้องการเพียงตัวอย่างเดียวจากตัวแปรสุ่มแต่ละตัวที่เราสร้าง แต่หากปรากฎว่าในอนาคตเราต้องการเห็นการกระจายตัวมากขึ้นนี่เป็นเรื่องเล็กน้อย คุณสามารถใช้ runRandom ได้หลายครั้งในตัวแปรสุ่มเดียวกันกับเมล็ดที่แตกต่างกัน แน่นอนว่าเป็นไปได้ในภาษาที่จำเป็น แต่ในกรณีนี้เราสามารถมั่นใจได้ว่าเราจะไม่ดำเนินการ IO ที่ไม่คาดคิดทุกครั้งที่เราสุ่มตัวอย่างตัวแปรแบบสุ่มและเราไม่ต้องระมัดระวังเกี่ยวกับสถานะเริ่มต้น