하스켈에서 의사 난수 만들기가 어렵다는 말이 있어 찾아봤다. 역시 내 기대를 저버리지 않았고, 간단하게 만드는 방법을 찾았다. 찾은 방법을 소개하기 전에 일반적으로 프로그래밍할 때 의사 난수 만드는 법을 살펴보자.
C같은 프로그램 언어에서 의사 난수는 다음 단계를 거쳐서 얻는다.
- 씨앗(seed) 정하기
- 의사 난수 얻기
- 2를 반복
rand() 류 함수를 부르면 정해진 씨앗에 따라 순차적으로 의사 난수를 반환한다. 명시적으로 씨앗 설정을 안해도 되는 언어는 rand() 류 함수를 처음 불렀을 때 내부에서 자동으로 씨앗을 정하고 시작한다.
하스켈에는 이와 동일한 방식의 rand() 함수가 있을 리는 없는데, 그 이유는 위 함수는 부작용(side effect)이 있기 때문이다. 씨앗을 하나의 상태(status)로 가지고 있고 rand() 함수를 호출함에 따라 씨앗 값에 따라 의사 난수를 생성하고 씨앗 값을 다시 갱신한다. 씨앗 값을 갱신하는 부분이 부작용이다. 대신 하스켈에서는 의사 난수와 씨앗 역할을 하는 생성자(generator)를 함께 넘겨받는 함수 next가 있다.
(r, g1) = next g0
g0는 생성자고, r은 의사 난수, g1은 새 성성자다. 새 생성자로 다음 의사 난수를 만드는 과정을 반복한다. 즉 하스켈에서는 다음 단계를 거쳐 의사 난수를 얻는다.
- 생성자(generator)를 만들기
- 의사 난수와 새 성성자 얻기
- 2를 반복
씨앗 대신 생성자를 만들어 사용한다는 것과 의사 난수를 생성할 때마다 새 생성자를 같이 받는다는 점 외에는 비슷하다.
그러나 게으름뱅이들은 좋은 툴을 가지고 여기서 그만두지 않았다. 의사 난수를 연속해서 얻는 간단한 방법은 다음과 같다.
import System.Random
main = do
gen <- newStdGen
let ns = randoms gen :: [Int]
print $ take 10 ns
셋째 줄에서 생성자를 만들고 넷째 줄에서 randoms 함수를 이용해서 의사 난수 리스트 ns를 만든다. 이 리스트에서 의사 난수를 하나씩 곶감 빼먹듯이 빼먹으면 된다. 좀 더 다양하게 의사 난수를 얻는 방법은 Haskell/Hierarchical libraries/Randoms를 참고하기 바란다.
p.s: 이 글을 쓰게 된 원인이 된 글에서 의사 난수를 필요로 하는 이유는 임의의 파일 이름을 만들기 위해서인 것 같다. 하지만 이런 용도로 의사 난수를 사용하는 것은 좋지 않다. 첫째로 의사 난수가 겹칠 수 있다. 둘째로 같은 프로그램이 두 개 이상 동시에 동작할 경우 파일 이름이 겹칠 수 있다. 셋째로 이미 그런 이름을 가진 파일이 존재할 수 있다. 여러 가지인 척 얘기를 했지만 결국 의사 난수로는 겹치지 않는 임의의 파일 이름을 만든다는 보장이 없다. 이런 목적으로는 C에서의 mkstemp 함수와 같은 것을 사용해야 한다. 하스켈에서 이에 해당하는 함수가 뭔지는 모른다. 불행히도.
January 5, 2009 at 6:53 am |
찾아보니까 반드시 결과를 변수에 assign하고 그 변수를 조작하면 괜찮은 것 같네요. 제 문제는 리턴타입이 IO Int라서 이게 자꾸 타입에러를 일으킨 것이었거든요. 쓰신 방법을 써도 newStdGen이 IO StdGen타입이라 main이 아닌 다른 함수에 넣으면 결과는 IO [Int]를 리턴합니다. 마찬가지로 mkstemp같은 하스켈 함수를 만들어도 아마 리턴타입은 IO FilePath가 되어야 할 것 같습니다.
January 5, 2009 at 4:36 pm |
newStdGen이 부작용이 있기 때문에 이 함수를 사용하는 다른 함수도 부작용이 있는 함수가 됩니다.
아주 간단하게 생각해서 “<-”가 IO 꼬리표를 떼준다고 생각하셔도 될 겁니다.
rn <- randomIO :: IO Int
이런 식으로 의사 난수 하나씩 받아 사용하셔도 될 것 같네요. rn은 Int 형이 됩니다. (아마 비슷하게 하신듯. ㅎㅎ)