Làm thế nào để ngôn ngữ chức năng xử lý số ngẫu nhiên?


68

Ý tôi là về điều đó là trong gần như mọi hướng dẫn tôi đã đọc về các ngôn ngữ chức năng, đó là một trong những điều tuyệt vời về chức năng, là nếu bạn gọi một hàm có cùng tham số hai lần, bạn sẽ luôn kết thúc với cùng một kết quả.

Làm thế nào trên trái đất bạn sau đó thực hiện một chức năng lấy một hạt giống làm tham số, và sau đó trả về một số ngẫu nhiên dựa trên hạt giống đó?

Tôi có nghĩa là điều này dường như đi ngược lại với một trong những điều rất tốt về chức năng, phải không? Hay tôi hoàn toàn thiếu một cái gì đó ở đây?

Câu trả lời:


89

Bạn không thể tạo một hàm thuần túy được gọi randomsẽ cho kết quả khác nhau mỗi lần nó được gọi. Trong thực tế, bạn thậm chí không thể "gọi" các hàm thuần túy. Bạn áp dụng chúng. Vì vậy, bạn không thiếu thứ gì, nhưng điều này không có nghĩa là các số ngẫu nhiên nằm ngoài giới hạn trong lập trình chức năng. Cho phép tôi chứng minh, tôi sẽ sử dụng cú pháp Haskell trong suốt.

Đến từ một nền tảng bắt buộc, ban đầu bạn có thể mong đợi ngẫu nhiên có một loại như thế này:

random :: () -> Integer

Nhưng điều này đã được loại trừ vì ngẫu nhiên không thể là một hàm thuần túy.

Hãy xem xét ý tưởng của một giá trị. Một giá trị là một điều bất di bất dịch. Nó không bao giờ thay đổi và mọi quan sát mà bạn có thể thực hiện về nó đều phù hợp với mọi thời đại.

Rõ ràng, ngẫu nhiên không thể tạo ra một giá trị Integer. Thay vào đó, nó tạo ra một biến ngẫu nhiên Integer. Kiểu của nó có thể trông như thế này:

random :: () -> Random Integer

Ngoại trừ việc thông qua một đối số là hoàn toàn không cần thiết, các hàm là thuần túy, vì vậy một hàm random ()cũng tốt như một đối số khác random (). Tôi sẽ đưa ra ngẫu nhiên, từ đây về sau, loại này:

random :: Random Integer

Đó là tất cả tốt và tốt, nhưng không hữu ích. Bạn có thể mong đợi có thể viết các biểu thức như thế nào random + 42, nhưng bạn không thể, bởi vì nó sẽ không đánh máy. Bạn không thể làm bất cứ điều gì với các biến ngẫu nhiên.

Điều này đặt ra một câu hỏi thú vị. Những chức năng nào nên tồn tại để thao tác các biến ngẫu nhiên?

Chức năng này không thể tồn tại:

bad :: Random a -> a

theo bất kỳ cách hữu ích nào, bởi vì sau đó bạn có thể viết:

badRandom :: Integer
badRandom = bad random

Mà giới thiệu một sự không nhất quán. badRandom được cho là một giá trị, nhưng nó cũng là một con số ngẫu nhiên; một mâu thuẫn.

Có lẽ chúng ta nên thêm chức năng này:

randomAdd :: Integer -> Random Integer -> Random Integer

Nhưng đây chỉ là một trường hợp đặc biệt của một mô hình tổng quát hơn. Bạn sẽ có thể áp dụng bất kỳ chức năng nào cho điều ngẫu nhiên để có được những thứ ngẫu nhiên khác như vậy:

randomMap :: (a -> b) -> Random a -> Random b

Thay vì viết random + 42, bây giờ chúng ta có thể viết randomMap (+42) random.

Nếu tất cả những gì bạn có là RandomMap, bạn sẽ không thể kết hợp các biến ngẫu nhiên lại với nhau. Bạn không thể viết hàm này chẳng hạn:

randomCombine :: Random a -> Random b -> Random (a, b)

Bạn có thể thử viết nó như thế này:

randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a

Nhưng nó có loại sai. Thay vì kết thúc bằng một Random (a, b), chúng tôi kết thúc với mộtRandom (Random (a, b))

Điều này có thể được khắc phục bằng cách thêm một chức năng khác:

randomJoin :: Random (Random a) -> Random a

Nhưng, vì những lý do cuối cùng có thể trở nên rõ ràng, tôi sẽ không làm điều đó. Thay vào đó tôi sẽ thêm điều này:

randomBind :: Random a -> (a -> Random b) -> Random b

Không rõ ràng ngay lập tức rằng điều này thực sự giải quyết được vấn đề, nhưng nó thực hiện:

randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)

Trên thực tế, có thể viết RandomBind theo phương thức RandomJoin và RandomMap. Cũng có thể viết RandomJoin theo phương thức RandomBind. Nhưng, tôi sẽ làm việc này như một bài tập.

Chúng ta có thể đơn giản hóa điều này một chút. Cho phép tôi xác định chức năng này:

randomUnit :: a -> Random a

RandomUnit biến một giá trị thành một biến ngẫu nhiên. Điều này có nghĩa là chúng ta có thể có các biến ngẫu nhiên không thực sự ngẫu nhiên. Đây luôn luôn là trường hợp; chúng ta có thể làm randomMap (const 4) randomtrước đây Lý do xác định RandomUnit là một ý tưởng hay là bây giờ chúng ta có thể định nghĩa RandomMap theo thuật ngữ RandomUnit và RandomBind:

randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)

Ok, bây giờ chúng tôi đang nhận được ở đâu đó. Chúng ta có các biến ngẫu nhiên mà chúng ta có thể thao tác. Tuy nhiên:

  • Không rõ ràng làm thế nào chúng ta thực sự có thể thực hiện các chức năng này,
  • Nó khá cồng kềnh.

Thực hiện

Tôi sẽ giải quyết các số giả ngẫu nhiên. Có thể thực hiện các hàm này cho các số ngẫu nhiên thực, nhưng câu trả lời này đã trở nên khá dài.

Về cơ bản, cách thức này sẽ hoạt động là chúng ta sẽ vượt qua một giá trị hạt giống ở khắp mọi nơi. Bất cứ khi nào chúng tôi tạo ra một giá trị ngẫu nhiên mới, chúng tôi sẽ tạo ra một hạt giống mới. Cuối cùng, khi chúng ta hoàn thành việc xây dựng một biến ngẫu nhiên, chúng ta sẽ muốn lấy mẫu từ nó bằng hàm này:

runRandom :: Seed -> Random a -> a

Tôi sẽ định nghĩa loại Ngẫu nhiên như thế này:

data Random a = Random (Seed -> (Seed, a))

Sau đó, chúng tôi chỉ cần cung cấp các triển khai của RandomUnit, RandomBind, runRandom và ngẫu nhiên khá đơn giản:

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

Ngẫu nhiên, tôi sẽ giả sử đã có một loại chức năng:

psuedoRandom :: Seed -> (Seed, Integer)

Trong trường hợp ngẫu nhiên là chỉ Random psuedoRandom.

Làm cho mọi thứ bớt cồng kềnh

Haskell có cú pháp đường để làm cho những thứ như thế này đẹp hơn trên mắt. Nó được gọi là ký hiệu và để sử dụng tất cả chúng ta phải làm nó tạo ra một thể hiện của Monad for Random.

instance Monad Random where
  return = randomUnit
  (>>=) = randomBind

Làm xong. randomCombinetừ trước có thể được viết:

randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
  a' <- a
  b' <- b
  return (a', b')

Nếu tôi đang làm điều này cho chính mình, tôi thậm chí sẽ tiến xa hơn một bước so với điều này và tạo ra một ví dụ của Ứng dụng. (Đừng lo lắng nếu điều này vô nghĩa).

instance Functor Random where
  fmap = liftM

instance Applicative Random where
  pure = return
  (<*>) = ap

Sau đó, RandomCombine có thể được viết:

randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b

Bây giờ chúng ta có các trường hợp này, chúng ta có thể sử dụng >>=thay vì RandomBind, tham gia thay vì RandomJoin, fmap thay vì RandomMap, trả về thay vì RandomUnit. Chúng tôi cũng nhận được toàn bộ tải các chức năng miễn phí.

Nó có đáng không? Bạn có thể tranh luận rằng, đến giai đoạn này, nơi làm việc với các số ngẫu nhiên không hoàn toàn khủng khiếp là khá khó khăn và dài dòng. Chúng ta đã nhận được gì để đổi lấy nỗ lực này?

Phần thưởng ngay lập tức nhất là bây giờ chúng ta có thể thấy chính xác phần nào trong chương trình của chúng ta phụ thuộc vào tính ngẫu nhiên và phần nào hoàn toàn mang tính quyết định. Theo kinh nghiệm của tôi, việc buộc một sự tách biệt nghiêm ngặt như thế này đơn giản hóa mọi thứ vô cùng.

Chúng tôi đã giả định rằng chúng tôi chỉ muốn một mẫu từ mỗi biến ngẫu nhiên mà chúng tôi tạo ra, nhưng nếu nó chỉ ra rằng trong tương lai chúng tôi thực sự muốn thấy nhiều hơn về phân phối, thì điều này là tầm thường. Bạn chỉ có thể sử dụng runRandom rất nhiều lần trên cùng một biến ngẫu nhiên với các hạt giống khác nhau. Tất nhiên, điều này là có thể trong các ngôn ngữ bắt buộc, nhưng trong trường hợp này, chúng ta có thể chắc chắn rằng chúng ta sẽ không thực hiện IO không dự đoán được mỗi khi chúng ta lấy mẫu một biến ngẫu nhiên và chúng ta không phải cẩn thận về việc khởi tạo trạng thái.


6
+1 cho một ví dụ hay về sử dụng thực tế của ứng dụng / Monads.
jozefg

9
Câu trả lời hay, nhưng nó đi hơi nhanh với một số bước. Ví dụ, tại sao sẽ bad :: Random a -> agiới thiệu sự không nhất quán? Điều gì là xấu về nó? Vui lòng đi chậm trong phần giải thích, đặc biệt là cho các bước đầu tiên :) Nếu bạn có thể giải thích tại sao các chức năng "hữu ích" lại hữu ích, đây có thể là câu trả lời 1000 điểm! :)
Andres F.

@AresresF. Ok, tôi sẽ sửa lại một chút.
dan_waterworth

1
@AresresF. Tôi đã sửa đổi câu trả lời của mình, nhưng tôi không nghĩ rằng tôi đã giải thích đầy đủ về cách bạn có thể sử dụng điều này là thực hành, vì vậy tôi có thể quay lại sau.
dan_waterworth

3
Câu trả lời đáng chú ý. Tôi không phải là một lập trình viên chức năng nhưng tôi hiểu hầu hết các khái niệm và tôi đã "chơi" với Haskell. Đây là loại câu trả lời vừa thông báo cho người hỏi và truyền cảm hứng cho những người khác đào sâu hơn và tìm hiểu thêm về chủ đề này. Tôi ước tôi có thể cho bạn thêm một vài điểm trên 10 từ phiếu bầu của tôi.
RLH

10

Bạn không sai. Nếu bạn cung cấp cùng một hạt giống cho RNG hai lần, thì số giả ngẫu nhiên đầu tiên mà nó trả về sẽ giống nhau. Điều này không liên quan gì đến lập trình chức năng so với hiệu ứng phụ; các định nghĩa của một hạt giống là một đầu vào cụ thể gây ra một sản lượng cụ thể của các giá trị cũng phân phối nhưng dứt khoát không ngẫu nhiên. Đó là lý do tại sao nó được gọi là giả ngẫu nhiên và thường là một điều tốt để có, ví dụ như viết các bài kiểm tra đơn vị dự đoán, để so sánh đáng tin cậy các phương pháp tối ưu hóa khác nhau trên cùng một vấn đề, v.v.

Nếu bạn thực sự muốn các số không giả ngẫu nhiên từ máy tính, bạn phải nối nó với một số ngẫu nhiên thực sự, chẳng hạn như nguồn phân rã hạt, các sự kiện không thể đoán trước xảy ra trong mạng mà máy tính đang bật, v.v. đúng và thường đắt ngay cả khi nó hoạt động, nhưng đó là cách duy nhất để không nhận các giá trị giả ngẫu nhiên (thường là các giá trị bạn nhận được từ ngôn ngữ lập trình của bạn dựa trên một số hạt giống, ngay cả khi bạn không cung cấp rõ ràng.

Điều này, và chỉ điều này, sẽ làm tổn hại đến bản chất chức năng của một hệ thống. Vì các trình tạo không giả ngẫu nhiên rất hiếm, nên điều này không xuất hiện thường xuyên, nhưng đúng vậy, nếu bạn thực sự có một phương pháp tạo ra các số ngẫu nhiên thực sự, thì ít nhất một chút ngôn ngữ lập trình của bạn có thể là chức năng thuần túy 100%. Liệu một ngôn ngữ có tạo ra ngoại lệ cho nó hay không chỉ là một câu hỏi về việc người thực thi ngôn ngữ đó thực dụng đến mức nào.


9
Một RNG thực sự hoàn toàn không thể là một chương trình máy tính, bất kể nó có thuần túy (chức năng ly) hay không. Tất cả chúng ta đều biết trích dẫn von Neumann về các phương pháp tạo ra các chữ số ngẫu nhiên (những người không tìm kiếm - tốt nhất là toàn bộ, không chỉ là câu đầu tiên). Bạn cần phải tương tác với một số phần cứng không xác định, tất nhiên là không tinh khiết. Nhưng đó chỉ là I / O, đã được điều hòa với độ tinh khiết nhiều lần trong các wans rất khác nhau. Không có ngôn ngữ nào có thể sử dụng hoàn toàn không cho phép I / O hoàn toàn - bạn thậm chí không thể thấy kết quả của chương trình.

Những gì với phiếu bầu xuống?
l0b0

6
Tại sao một nguồn bên ngoài & thực sự ngẫu nhiên làm tổn hại đến bản chất chức năng của hệ thống? Nó vẫn "cùng một đầu vào -> cùng một đầu ra". Trừ khi bạn coi nguồn bên ngoài là một phần của hệ thống, nhưng sau đó nó sẽ không phải là "bên ngoài", phải không?
Andres F.

4
Điều này không liên quan gì đến PRNG vs TRNG. Bạn không thể có một hàm không cố định của loại () -> Integer. Bạn có thể có một loại PRNG hoàn toàn chức năng PRNG_State -> (PRNG_State, Integer), nhưng bạn sẽ phải khởi tạo nó bằng phương tiện không tinh khiết).
Gilles

4
@Brian Đồng ý, nhưng từ ngữ ("nối nó với một cái gì đó thực sự ngẫu nhiên") cho thấy nguồn ngẫu nhiên là bên ngoài hệ thống. Do đó, bản thân hệ thống vẫn hoàn toàn hoạt động; đó là nguồn đầu vào không.
Andres F.

6

Một cách là nghĩ về nó như một chuỗi vô số các số ngẫu nhiên:

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

Đó là, chỉ cần nghĩ về nó như một cấu trúc dữ liệu không đáy, giống như một Stacknơi bạn chỉ có thể gọi Pop, nhưng bạn có thể gọi nó mãi mãi. Giống như một ngăn xếp bất biến bình thường, lấy một cái khỏi đầu cung cấp cho bạn một ngăn xếp khác (khác).

Vì vậy, một trình tạo số ngẫu nhiên bất biến (với đánh giá lười biếng) có thể trông giống như:

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)
    {
        //...
    }
}

Đó là chức năng.


Tôi không thấy cách tạo một danh sách vô hạn các số ngẫu nhiên dễ làm việc hơn chức năng như : pseudoRandom :: Seed -> (Seed, Integer). Thậm chí bạn có thể sẽ viết một chức năng thuộc loại này[Integer] -> ([Integer], Integer)
dan_waterworth

2
@dan_waterworth thực sự nó rất có ý nghĩa. Một số nguyên không thể nói là ngẫu nhiên. Một danh sách các số có thể có protperty này. Vì vậy, sự thật là, một trình tạo ngẫu nhiên có thể có kiểu int -> [int] tức là một hàm lấy một hạt giống và trả về một danh sách các số nguyên ngẫu nhiên. Chắc chắn, bạn có thể có một đơn vị nhà nước xung quanh điều này để có được ký hiệu của haskell. Nhưng như một câu trả lời chung cho câu hỏi, tôi nghĩ rằng điều này thực sự hữu ích.
Simon Bergot

5

Các ngôn ngữ phi chức năng cũng vậy. Bỏ qua vấn đề hơi riêng biệt của các số thực sự ngẫu nhiên ở đây.

Một trình tạo số ngẫu nhiên luôn lấy một giá trị hạt giống và cho cùng một hạt giống trả về cùng một chuỗi các số ngẫu nhiên (rất hữu ích nếu bạn cần kiểm tra một chương trình sử dụng các số ngẫu nhiên). Về cơ bản, nó bắt đầu với hạt giống bạn chọn và sau đó sử dụng kết quả cuối cùng làm hạt giống cho lần lặp tiếp theo. Vì vậy, hầu hết các triển khai là các hàm "thuần túy" như bạn mô tả chúng: Lấy một giá trị và cho cùng một giá trị luôn trả về cùng một kết quả.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.