ภาษาที่ใช้งานได้จัดการกับตัวเลขสุ่มอย่างไร


68

สิ่งที่ผมหมายเกี่ยวกับที่อยู่ในเกือบทุกกวดวิชาที่ฉันได้อ่านเกี่ยวกับภาษาการทำงานเป็นที่หนึ่งในสิ่งที่ดีเกี่ยวกับฟังก์ชั่นคือว่าถ้าคุณเรียกใช้ฟังก์ชันที่มีพารามิเตอร์เดียวกันสองครั้งคุณจะเสมอจบลงด้วย ผลลัพธ์เดียวกัน

แล้วคุณสร้างฟังก์ชันที่รับเมล็ดเป็นพารามิเตอร์แล้วส่งกลับตัวเลขสุ่มตามเมล็ดนั้นได้อย่างไร

ฉันหมายความว่าสิ่งนี้ดูเหมือนจะขัดแย้งกับหนึ่งในสิ่งที่ดีเกี่ยวกับฟังก์ชั่นใช่มั้ย หรือว่าฉันขาดอะไรบางอย่างที่นี่อย่างสมบูรณ์?

คำตอบ:


89

คุณไม่สามารถสร้างฟังก์ชั่นบริสุทธิ์ที่เรียก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 ที่ไม่คาดคิดทุกครั้งที่เราสุ่มตัวอย่างตัวแปรแบบสุ่มและเราไม่ต้องระมัดระวังเกี่ยวกับสถานะเริ่มต้น


6
+1 สำหรับตัวอย่างที่ดีของ Applicative Functionors / Monads
jozefg

9
คำตอบที่ดี แต่มันเร็วไปหน่อยในบางขั้นตอน ตัวอย่างเช่นทำไมจะbad :: Random a -> aแนะนำความไม่สอดคล้อง? มีอะไรที่ไม่ดีเกี่ยวกับเรื่องนี้? โปรดอธิบายอย่างช้าๆโดยเฉพาะอย่างยิ่งสำหรับขั้นตอนแรก :) หากคุณสามารถอธิบายได้ว่าทำไมฟังก์ชั่น "มีประโยชน์" จึงมีประโยชน์นี่อาจเป็นคำตอบ 1000 จุด! :)
Andres F.

@AndresF ตกลงฉันจะแก้ไขอีกหน่อย
dan_waterworth

1
@AndresF ฉันได้แก้ไขคำตอบของฉันแล้ว แต่ฉันไม่คิดว่าฉันอธิบายได้พอเพียงว่าคุณจะใช้สิ่งนี้อย่างไรในการฝึกฝนดังนั้นฉันอาจกลับมาใช้มันในภายหลัง
dan_waterworth

3
คำตอบที่น่าทึ่ง ฉันไม่ใช่โปรแกรมเมอร์ที่ทำงานได้ แต่ฉันเข้าใจแนวคิดส่วนใหญ่และฉัน "เล่น" กับ Haskell นี่คือประเภทของคำตอบที่ทั้งแจ้งผู้ถามและเป็นแรงบันดาลใจให้ผู้อื่นขุดลึกลงไปและเรียนรู้เพิ่มเติมเกี่ยวกับหัวข้อ ฉันหวังว่าฉันจะให้คะแนนพิเศษคุณมากกว่า 10 คะแนนจากการโหวตของฉัน
RLH

10

คุณไม่ผิด หากคุณให้เมล็ดเดียวกันกับ RNG สองครั้งหมายเลขหลอกหลอกแรกที่ส่งคืนจะเหมือนกัน นี่คือสิ่งที่จะทำอย่างไรกับการเขียนโปรแกรมการทำงานกับผลข้างเคียง; นิยามของเมล็ดพันธุ์ก็คือการป้อนข้อมูลที่เฉพาะเจาะจงเป็นสาเหตุที่ทำให้การส่งออกที่เฉพาะเจาะจงของค่าดีกระจาย แต่เด็ดที่ไม่ใช่แบบสุ่ม นั่นคือเหตุผลที่เรียกว่าการสุ่มหลอกและมักจะเป็นสิ่งที่ดีเช่นการเขียนการทดสอบหน่วยที่คาดการณ์ได้เพื่อเปรียบเทียบวิธีการปรับให้เหมาะสมต่าง ๆ ที่เชื่อถือได้ในปัญหาเดียวกัน ฯลฯ

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

สิ่งนี้และเพียงแค่นี้จะลดทอนลักษณะการทำงานของระบบ เนื่องจากเครื่องกำเนิดไฟฟ้าแบบหลอกหลอกนั้นหายากจึงไม่เกิดขึ้นบ่อยนัก แต่ใช่ถ้าคุณมีวิธีการสร้างตัวเลขสุ่มจริงดังนั้นอย่างน้อยภาษาโปรแกรมของคุณจะไม่สามารถทำงานได้อย่างสมบูรณ์ 100% ว่าภาษาจะทำให้เกิดข้อยกเว้นหรือไม่เป็นเพียงคำถามว่าผู้ใช้ภาษานั้นใช้งานได้จริงอย่างไร


9
RNG ที่แท้จริงไม่สามารถเป็นโปรแกรมคอมพิวเตอร์ได้เลยไม่ว่ามันจะบริสุทธิ์หรือไม่ เราทุกคนรู้ว่าฟอนนอยมันน์พูดเกี่ยวกับวิธีการทางคณิตศาสตร์ในการผลิตตัวเลขสุ่ม (ผู้ที่ไม่ได้มองมัน - โดยเฉพาะอย่างยิ่งสิ่งทั้งหมดไม่ใช่แค่ประโยคแรก) คุณต้องมีปฏิสัมพันธ์กับฮาร์ดแวร์ที่ไม่ได้กำหนดค่าซึ่งแน่นอนว่าไม่บริสุทธิ์เช่นกัน แต่นั่นเป็นเพียง I / O ซึ่งได้รับการคืนดีกับความบริสุทธิ์หลายครั้งในช่วงเวลาที่แตกต่างกันมาก ไม่มีภาษาใดที่สามารถใช้งาน I / O ได้อย่างสมบูรณ์ - คุณไม่สามารถเห็นผลลัพธ์ของโปรแกรมได้

การลงคะแนนเสียงคืออะไร
l0b0

6
ทำไมแหล่งภายนอกและการสุ่มอย่างแท้จริงจะลดทอนลักษณะการทำงานของระบบ? มันยังคงเป็น "อินพุตเดียวกัน -> เอาต์พุตเดียวกัน" นอกเสียจากว่าคุณจะพิจารณาแหล่งภายนอกว่าเป็นส่วนหนึ่งของระบบ แต่จากนั้นมันจะไม่เป็น "ภายนอก" ใช่ไหม?
Andres F.

4
สิ่งนี้ไม่เกี่ยวข้องกับ PRNG กับ TRNG คุณไม่สามารถใช้ฟังก์ชันประเภทไม่คงที่() -> Integerได้ คุณสามารถมี PRNG ที่ทำงานได้อย่างหมดจดPRNG_State -> (PRNG_State, Integer)แต่คุณต้องเริ่มต้นด้วยวิธีการที่ไม่บริสุทธิ์)
Gilles

4
@Brian Agreed แต่ถ้อยคำ ("ขอให้มันขึ้นอยู่กับสิ่งที่สุ่มอย่างแท้จริง") แสดงให้เห็นว่าแหล่งที่สุ่มอยู่นอกระบบ ดังนั้นระบบยังคงทำงานได้อย่างหมดจด มันเป็นแหล่งอินพุตที่ไม่ใช่
Andres F.

6

วิธีหนึ่งคือคิดว่ามันเป็นลำดับสุ่มของอนันต์ตัวเลข:

IEnumerable<int> randomNumberGenerator = new RandomNumberGenerator(seed);

นั่นคือลองคิดว่ามันเป็นโครงสร้างข้อมูลที่ไม่มีที่สิ้นสุดเช่นเดียวกับStackที่คุณสามารถโทรออกPopได้ แต่คุณสามารถเรียกมันได้ตลอดไป เช่นเดียวกับสแต็กที่ไม่เปลี่ยนรูปแบบปกติการถอดด้านบนออกจะให้สแต็คอื่น (ต่างกัน) ให้คุณ

ดังนั้นตัวสร้างตัวเลขสุ่มที่ไม่เปลี่ยนรูป (ที่มีการประเมินผลที่ขี้เกียจ) อาจมีลักษณะดังนี้:

class RandomNumberGenerator
{
    private readonly int nextSeed;
    private RandomNumberGenerator next;

    public RandomNumberGenerator(int seed)
    {
        this.nextSeed = this.generateNewSeed(seed);
        this.RandomNumber = this.generateRandomNumberBasedOnSeed(seed);
    }

    public int RandomNumber { get; private set; }

    public RandomNumberGenerator Next
    {
        get
        {
            if(this.next == null) this.next = new RandomNumberGenerator(this.nextSeed);
            return this.next;
        }
    }

    private static int generateNewSeed(int seed)
    {
        //...
    }

    private static int generateRandomNumberBasedOnSeed(int seed)
    {
        //...
    }
}

นั่นเป็นหน้าที่


pseudoRandom :: Seed -> (Seed, Integer)ผมไม่เห็นว่าการสร้างรายการที่ไม่มีที่สิ้นสุดของตัวเลขสุ่มเป็นเรื่องง่ายที่จะทำงานร่วมกับกว่าฟังก์ชั่นที่ชอบ: คุณอาจจะจบลงด้วยการเขียนฟังก์ชั่นประเภทนี้[Integer] -> ([Integer], Integer)
dan_waterworth

2
@dan_waterworth จริง ๆ แล้วมันสมเหตุสมผลมาก ไม่สามารถระบุจำนวนเต็มแบบสุ่มได้ รายการของตัวเลขสามารถมีคุณสมบัตินี้ ดังนั้นความจริงก็คือเครื่องกำเนิดไฟฟ้าแบบสุ่มสามารถมีประเภท int -> [int] คือฟังก์ชั่นที่รับเมล็ดและส่งกลับรายการจำนวนเต็มแบบสุ่ม แน่นอนคุณสามารถมีรัฐ monad รอบนี้เพื่อรับการทำของ haskell แต่เป็นคำตอบทั่วไปของคำถามฉันคิดว่านี่เป็นประโยชน์จริงๆ
Simon Bergot

5

มันเหมือนกันสำหรับภาษาที่ไม่สามารถใช้งานได้ ไม่สนใจปัญหาที่แยกกันเล็กน้อยของตัวเลขสุ่มที่นี่

ตัวสร้างตัวเลขสุ่มจะรับค่าเมล็ดเสมอและสำหรับเมล็ดเดียวกันจะส่งคืนหมายเลขสุ่มตามลำดับ (มีประโยชน์มากถ้าคุณต้องการทดสอบโปรแกรมที่ใช้ตัวเลขสุ่ม) โดยพื้นฐานแล้วจะเริ่มต้นด้วยเมล็ดที่คุณเลือกจากนั้นใช้ผลลัพธ์สุดท้ายเป็นเมล็ดสำหรับการวนซ้ำครั้งถัดไป ดังนั้นการนำไปปฏิบัติส่วนใหญ่จะเป็นฟังก์ชั่น "บริสุทธิ์" ตามที่คุณอธิบายไว้: รับค่าและค่าเดียวกันจะส่งกลับผลลัพธ์เดียวกันเสมอ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.