Common Lisp vs Haskell, Chapter 9
Meta note
対象読者
前章を読了済みの者。
Introduction
本稿は「すごいH本」の内容をCommon Lispに翻訳しながらCLerがHaskellを学ぶその第9章である。 本章ではHaskellに於けるファイル操作、乱数生成、コマンドライン引数の取り扱いを、Common Lispに翻訳しながら学習していく。
初心者CLerにとっては幾つか導入される簡単なマクロの実装が参考になるかと思われる。 また、途中比較的大きな関数が出てくるのだが、そこは行番号を振って丁寧に解説してあるので、そこも参考になるかもしれない。
中級以上のCLerにとっては、特別興味深い点はなかろうと思われるので、読み飛ばして、どうぞ。
比較的長文なので、お暇なときにでも、たっぷりのコーヒーとチョコレートを用意してお読みくだされば幸い。
9
9.1
input redirect
import Control.Monad
import Data.Char
main = forever $ do
l <- getLine
putStrLn $ map toUpper l
再現にこだわらずに作るなら以下の通り。
(defun main ()
(loop (write-line(string-upcase(read-line)))))
こだわるなら以下の通り。
(defaction main
(forever(action l <- (get-line)
(put-string-line(string-upcase l)))))
input stream
import Data.Char
main = do
contents <- getContents
putStr $ map toUpper contents
こだわらないなら以下の通り。
(defun main()
(loop :for contents = (read-line nil nil)
:while contents
:do (write-line(string-upcase contents))))
さて、こだわった場合の実装だが、これはどう作ったら良いのかさっぱり分からない。
getContetns
は標準入力からEOF
まで入力を読み込むアクションだという。
それだけなら簡単だ。
(defun get-contents()
(lambda()
(loop :for char = (read-char nil nil)
:while char
:collect char :into result
:finally (return (coerce result 'string)))))
ところが、それは遅延IOだという。
getContentsの結果がcontentsに束縛されとき、それは本当の文字列ではなく、最終的には文字列に評価されるプロミス(promise)としてメモリ上に置かれます。contentsにtoUpperをマップするとき、それもまた入力の結果に関数をマップするというプロミスになります。最終的にputStrが呼ばれると、これがさっきのプロミスに対して「やあ、大文字化された行が必要なんだ!」と言います。そのプロミスはまだ入力の行を何も持っていないので、contentsに対し「端末からの入力の状況はどうなってる?」と問い合わせます。それでようやくgetContentsは実際に端末から入力して、何か入力をくれといってきたコードに生成したものを渡すのです。受け取ったコードは渡されたものにtoUpperをマップし、その結果をputStrに渡して、画面に行が出力されます。さらに続けてputStrは「ヘイ、次の行をくれ!カモン!」と言います。これが入力がなくなるまで、つまりEOF文字が入力されるまで繰り返されます。
上記引用の中で、特に筆者が理解出来ないのが、行単位でデータのやり取りが行われる点である。 また暗黙裏の繰り返しが誰の責任で行われているのかという点も分からない。 遅延評価まわりやHaskellの実装についての論文をちゃんと読めば分かるようにもなるだろうが、そこまでやる気はないので、以降本節に関しては再現にこだわらず読み飛ばして行くものとする。
さて、 HaskellのgetContents
関数を実装する代わりに、ここではDO-CONTENTS
マクロを作ろう。
(defmacro do-contents((var &optional(reader '#'read)(stream '*standard-input*))&body body)
`(LOOP :FOR ,var = (FUNCALL ,reader ,stream NIL)
:WHILE ,var
:DO ,@(mapcar(lambda(form)
`(MAY-CALL ,form))
body)))
これで上記Haskellコードは以下のように翻訳できる。
(defaction main
(do-contents(contents #'read-line)
(put-string(string-upcase contents))))
main = do
contents <- getContents
putStr (shortLinesOnly contents)
shortLinesOnly :: String -> String
shortLinesOnly = unlines . filter (\line -> length line < 10) . lines
Haskellに於けるlines
、unlines
はCommon Lispには存在しない。
必要なら自作せねばならない。
(defun lines(string)
(uiop:split-string string :separator #.(string #\newline)))
(defun unlines(string*)
(format nil "~{~A~^~%~}"string*))
これで以下のように書ける。
(defun main ()
(loop :for contents = (read-line nil nil)
:while contents
:do (write-string (short-lines-only contents))
(force-output)))
(declaim(ftype(function(string)string)short-lines-only))
(defun short-lines-only (string)
(unlines (remove-if-not (lambda(line)
(< (length line)10))
(lines string))))
先のDO-CONTENTS
マクロを使うなら以下の通り。
(defun main()
(do-contents(contents #'read-line)
(put-string (short-lines-only contents))))
convert
main = interact shortLinesOnly
shortLinesOnly :: String -> String
shortLinesOnly = unlines . filter (\line -> length line < 10) . lines
Haskellに於けるinteract
はCommon Lispに存在しない。
必要なら自作するしかない。
(defun interact (&optional(function #'identity))
(lambda()
(loop :for content = (read-line nil nil)
:while content
:do (write-line(funcall function content)))))
これで以下のように書ける。
(defaction main (interact #'short-lines-only))
respondPalindromes :: String -> String
respondPalindromes =
unlines .
map (\xs -> if isPal xs then "palindrome" else "not a palindrome") .
lines
isPal :: String -> Bool
isPal xs = xs == reverse xs
(declaim(ftype(function(string)string)respond-palindromes))
(defun respond-palindromes(string)
(unlines (mapcar (lambda(xs)
(if(palindromep xs)
"palindrome"
"not a palindrome"))
(lines string))))
(declaim(ftype(function(string)boolean)palindromep))
(defun palindromep(string)
(string= string (reverse string)))
(defaction main (interact #'respond-palindromes))
file
import System.IO
main = do
handle <- openFile "baabaa.txt" ReadMode
contents <- hGetContents handle
putStr contents
hClose handle
Haskellに於けるopenFile
、hClose
は各々Common Lispに於けるOPEN
、CLOSE
におよそ等しい。
違いはこれまでと同様に、関数を返すか処理を行うかである。
必要なら自作するしか無い。
(defun open-file(path direction)
(lambda()
(open path :direction direction)))
(defun h-close(handle)
(lambda()(close handle)))
上記Haskellコードを直訳するなら以下の通り。
(defaction main
handle <- (open-file "baabaa.txt" :input)
(let((*standard-input* handle))
(do-contents(contents #'read-line)
(put-string-line contents)))
(h-close handle))
再現にこだわらないなら以下の通り。
(defun main ()
(with-open-file(*standard-input* "baabaa.txt")
(loop :for line = (read-line nil nil)
:while line
:do (write-line line))))
Common Lispに於いてOPEN
やCLOSE
は上級者向けのコマンドである。
というのも、上記のように通常はWITH-OPEN-FILE
マクロを使うものだからである。
ちなみに、折衷案として以下のようにも書ける。
(defun main()
(with-open-file(*standard-input* "baabaa.txt")
(funcall(interact))))
INTERACT
が返すIOアクションをWITH-OPEN-FILE
のスコープ内でFUNCALL
を用いて強引に呼び出している。
明示的なFUNCALL
が不細工に見えるかもしれないが、上記のどのコードよりも短い。
type FilePath = String
Haskellに於いてFilePath
はString
へのシノニムのようだが、Common Lispにはそれそのものを表す型PATHNAME
がある。
型PATHNAME
は型STRING
とは明確に区別される。
しかしながらファイルパスを受け付ける関数の多くは文字列でも機能するように仕様で決まっている。
そこで、仕様書ではPATHNAME
とSTRING
とを統合した用語PATHNAME-DESIGNATOR
というものがしばしば仮引数の名前などに使われる。
残念ながらPATHNAME-DESIGNATOR
は仕様書で使われる用語でしかなく、そのような型は言語仕様には存在しない。
あれば便利なのでこれらを提供するライブラリがある。
名をTrivial-typesという。
よって、HaskellのFilePath
はCommon Lispに於いてはTRIVIAL-TYPES:PATHNAME-DESIGNATOR
が最も望ましいと言える。
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
Haskellに於ける型IOMode
はCommon LispではOPEN
への引数、ひいてはWITH-OPEN-FILE
への引数として組み合わせて使うこととなる。
HaskellのReadMode
は以下のようにしてCommon Lispで再現できる。
(open "path")
HaskellのWriteMode
は以下のようにしてCommon Lispで再現できる。
(open path" :direction :output :if-does-not-exist :create :if-exists :supersede)
HaskellのAppendMode
は以下のようにしてCommon Lispで再現できる。
(open "path" :direction :output :if-exists :append :if-does-not-exist :create)
HaskellのReadWriteMode
は以下のようにしてCommon Lispで再現できる。
(open "path" :direction :io :if-does-not-exist :create)
Haskell風に再現するなら自作するしかない。
(defdata io-mode()
:read-mode :write-mode :append-mode :read-write-mode)
IO-MODE
を作った以上は、上記OPEN-FILE
も作りなおさねばなるまい。
(defun open-file(path mode)
(lambda()
(apply #'open path (ecase mode
(:read-mode)
(:write-mode `(:direction :output :if-does-not-exist :create :if-exists :supersede))
(:append-mode `(:direction :output :if-exists :append :if-does-not-exist :create))
(:read-write-mode `(:direction :io :if-does-not-exist :create :if-exists :supersede))))))
withFile
main = do
withFile "baabaa.txt" ReadMode $ \handle -> do
contents <- hGetContents handle
putStr contents
HaskellのwithFile
はCommon LispのWITH-OPEN-FILE
におよそ等しい。
違いは関数を返すが処理を行うかである。
再現にこだわるなら自作するしかない。
(declaim(ftype(function(trivial-types:pathname-designator
io-mode
function)
function)
with-file))
(defun with-file(file-path io-mode function)
(coerce `(LAMBDA()
(WITH-OPEN-FILE(*TERMINAL-IO* ,file-path
,@(ecase io-mode
(:read-mode)
(:write-mode `(:DIRECTION :OUTPUT :IF-DOES-NOT-EXIST :CREATE :IF-EXISTS :SUPERSEDE))
(:append-mode `(:DIRECTION :OUTPUT :IF-EXISTS :APPEND :IF-DOES-NOT-EXIST :CREATE))
(:read-write-mode `(:DIRECTION :IO :IF-DOES-NOT-EXIST :CREATE :IF-EXISTS :SUPERSEDE))))
(FUNCALL(FUNCALL ,function *TERMINAL-IO*))))
'function))
これで以下のように書ける。
(defaction main()
(with-file "baabaa.txt" :read-mode
(lambda(handle)
(action (do-contents(contents #'read-line handle)
(put-string contents))))))
bracket
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile name mode f = bracket (openFile name mode)
(\handle -> hClose handle)
(\handle -> f handle)
Haskellに於けるbracket
はCommon Lispに於けるUNWIND-PROTECT
におよそ等しい。
大きな違いはbracket
が関数であるのに対し、UNWIND-PROTECT
は特殊形式である点だろう。
WITH-OPEN-FILE
はもちろんUNWIND-PROTECT
上に実装されている。
(macroexpand-1 '(with-open-file(s file-path)(read-line s)))
#+expanded
(LET ((S (OPEN FILE-PATH))
(#:G1 T))
(UNWIND-PROTECT (MULTIPLE-VALUE-PROG1 (PROGN (READ-LINE S))
(SETQ #:G1 NIL))
(WHEN S (CLOSE S :ABORT #:G1))))
関数版が欲しいなら実装するしかない。
(declaim(ftype(function(function function function)function)bracket))
(defun bracket(prologue epilogue body)
(lambda()
(let(handle)
(unwind-protect(progn (setf handle(funcall prologue))
(funcall(funcall body handle)))
(funcall(funcall epilogue handle))))))
これでWITH-FILE
の実装を以下のようにできる。
(defun with-file(name mode f)
(bracket (open-file name mode)
(lambda(handle)(h-close handle))
(lambda(handle)(funcall f handle))))
handle
Haskellでhandle
と呼ばれているものは、Common LispではSTREAM
と呼ばれている。
Haskellで、例えばgetContents
が標準入出力に振る舞うのに対し、hGetContents
は受け取ったhandle
に対し振る舞う。
翻ってCommon Lispでは、例えばREAD-LINE
はオプショナルにSTREAM
を引数に取る。
引数の規定値は*STANDARD-INPUT*
であり、*STANDARD-INPUT*
の規定値は標準入力である。
例えば(read-line)
とすれば、標準入力から一行読み込むが、(read-line stream)
とすれば渡されたstream
から一行読み込むこととなる。
また、Common Lispではスペシャル変数の動的束縛をよく用いる。
(let((*standard-input* stream))
(read-line))
上記コードではスペシャル変数*STANDARD-INPUT*
の値を動的にstream
の値に束縛し、そのコンテクスト下でREAD-LINE
が呼ばれる。
READ-LINE
は引数が指定されていないので、規定値である*STANDARD-INPUT*
から一行読み込むのだが、その*STANDARD-INPUT*
はstream
に束縛されているので、結果的にstream
から一行読み込むこととなる。
このため、Common LispではHaskellの様に標準入出力に振る舞うものとhandle
を受け取るものと2つの関数を用意する必要がない。
それが良いことなのか悪いことなのかについてはここでは議論しない。
import System.IO
main = do
contents <- readFile "baabaa.txt"
putStr contents
HaskellのreadFile
はCommon Lispには存在しない。
こだわらずに作るなら、自作する代わりにuiopのREAD-FILE-STRING
が利用できよう。
(defun main ()
(write-string (uiop:read-file-string "baabaa.txt")))
import System.IO
import Data.Char
main = do
contents <- readFile "baabaa.txt"
writeFile "baabaacaps.txt" (map toUpper contents)
HaskellのwriteFile
に相当するものはCommon Lispには存在しない。
素のCommon Lispで書くなら上記Haskellコードは以下のようになるだろう。
(defun main()
(witn-open-file(*standard-input* "baabaa.txt")
(with-open-file(*standard-output* "baabaacaps.txt"
:direction :output
:if-does-not-exist :create
:if-exists :supersede)
(loop :for line = (read-line)
:while line
:do (write-line(string-upcase line))))))
Haskellのコードは非常にシンプルで美しいものとなっている。 繰り返しが暗黙裏に行われている点が愁眉であろう。 これを遅延評価の無いCommon Lispで再現するのは難しい。 よってマクロにして隠蔽してしまおう。
(defmacro with-write-file(path &body body)
`(WITH-OPEN-FILE(*STANDARD-OUTPUT* ,path :DIRECTION :OUTPUT
:IF-EXISTS :SUPERSEDE :IF-DOES-NOT-EXIST :CREATE)
,@body))
(defmacro with-append-file(path &body body)
`(WITH-OPEN-FILE(*STANDARD-OUTPUT* ,path :DIRECTION :OUTPUT
:IF-EXISTS :APPEND :IF-DOES-NOT-EXIST :CREATE)
,@body))
これで、これまで作ってきたものと組み合わせて以下のように書ける。
(defun main ()
(with-write-file "baabaacaps.txt"
(write-string(string-upcase(uiop:read-file-string "baabaa.txt")))))
todo list
import System.IO
main = do
todoItem <- getLine
appendFile "todo.txt" (todoItem ++ "\n")
素のCommon Lispで書くなら以下の通り。
(defun main()
(with-open-file(*standard-output* "todo.txt"
:direction :output
:if-does-not-exist :create
:if-exists :append)
(loop :for line = (read-line nil nil)
:while line
(write-line line))))
これまで作ってきたものを駆使するなら以下の通り。
(defun main()
(with-append-file "todo.txt"
(funcall(interact))))
delete
import System.IO
import System.Directory
import Data.List
main = do
contents <- readFile "todo.txt"
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStrLn "There are your TO-DO items:"
mapM_ putStrLn numberedTasks
putStrLn "Which one do you want to delete?"
numberString <- getLine
let number = read numberString
newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
(tempName, tempHandle) <- openTempFile "." "temp"
hPutStr tempHandle newTodoItems
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt"
0(defun main()
1 (let*((lines(delete ""(uiop:read-file-lines "baabaa.txt"):test #'string=))
2 (alist(loop :for i :upfrom 0
3 :for line :in lines
4 :collect (list i line)))
5 (number(prompt-for:prompt-for `(mod ,(length lines))
6 "There are your TO-DO items:~%~:{~3D - ~A~%~}~%~
7 Which one do you want to delete?~%>> " alist))
8 (temp-name(loop :for name = (symbol-name(gensym))
9 :unless (probe-file name)
10 :return name)))
11 (with-write-file temp-name
12 (loop :for (i line) :in alist
13 :unless (= number i)
14 :do (write-line line)))
15 (delete-file "todo.txt")
16 (rename-file temp-name "todo.txt")))
少々規模が大きいので解説する。
まず、UIOP:READ-FILE-LINES
で指定されたファイルの行のリストを得る。(1)
念の為、空行を消しておく。(1、DELETE
)
REMOVE
を使わずDELETE
を使っているのはUIOP:READ-FILE-LINES
がフレッシュなリストを返すからだ。
破壊的に編集しても他に影響を及ぼさない。
変数ALIST
に束縛されるリストの要素は、後のことを考えてCONS
ではなくLIST
で括られている。(4)
PROMPT-FOR
はCLtL2に出てきた架空の関数を実際に作ってみた拙作ライブラリである。(5)
第一引数にリストを受け取った場合、それはCompound-type-specifierと解釈され、ユーザの入力がそれを満たさない場合、クレームを出力し再入力を促す。
ここでは`(mod ,(length lines))
が渡されている。(5)
仮に行数が3だった場合、ユーザの入力が0、1、2のいずれかでない限り再入力を促すこととなる。
PROMPT-FOR
の第二引数はFormat-controlである。
ここでは(4)でLIST
を使って要素をくくったおかげで~:{ ... ~}
というFormat-directiveを使うことが出来る。
HaskellのopenTempFile
に相当する機能はCommon Lispには無いので、GENSYM
を利用する。(8)
GENSYM
により生成されたシンボルの名前をSYMBOL-NAME
で取り出し(8)、PROBE-FILE
で同名のファイルが存在するか確認し(9)、存在しないようならその名前を返す(10)。
後はWITH-WRITE-FILE
にTEMP-NAME
を渡し(11)、ALIST
をLOOP
し(12)、ユーザが指定した行番号と異なるなら(13)出力する(14)。
後はHaskellと変わらないので説明はいらないだろう。
ところで、tempファイルに書き出してから安全にリネームすることはよくあることだろう。 マクロで隠蔽してしまおう。
(defmacro with-safe-write-file(file &body body)
(let((temp(gensym "TEMP")))
`(LET((,temp(LOOP :FOR NAME = (SYMBOL-NAME(GENSYM))
:UNLESS(PROBE-FILE NAME)
:RETURN NAME)))
(HANDLER-BIND((ERROR(LAMBDA(C)(DECLARE(IGNORE C))
(WHEN(PROBE-FILE ,tmep)
(DELETE-FILE ,temp)))))
(WITH-WRITE-FILE ,temp ,@body)
(WHEN(PROBE-FILE ,file)
(DELETE-FILE ,file))
(RENAME-FILE ,temp ,file)))))
cleanup
HaskellのbracketOnError
はCommon Lispに於けるHANDLER-BIND
に近しい。
実装するなら以下の通り。
(defun bracket-on-error(arg handler body)
(handler-bind((error(lambda(c)
(declare(ignore c))
(funcall handler arg))))
(funcall body arg)))
9.4
command line arguments
import System.Environment
import Data.List
main = do
args <- getArgs
progName <- getProgName
putStrLn "The arguments are:"
mapM putStrLn args
putStrLn "The program name is:"
putStrLn progName
HaskellのgetArgs
に相当する機能はCommon Lispにはない。
しかしながらRoswellスクリプトとして書いているなら、Roswellが面倒を見てくれる。
Rosスクリプトのmain関数は&REST
でコマンドライン引数を受け取るようになっている。
HaskellのgetProgName
に相当する機能はCommon Lispにはない。
対応するライブラリ等についても筆者は知らない。
(defun main (&rest argv)
(format t "The arguments are:~%~{~A~%~}"argv))
9.5
Multi task task list
import System.Environment
import System.Directory
import System.IO
import Data.List
dispatch :: String -> [String] -> IO()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
main = do
(command:argList) <- getArgs
dispatch command argList
(defun dispatch(command)
(trivia:match command
("add" #'add)
("view" #'view)
("remove" #'.remove)))
(defun main (&rest argv)
(apply (dispatch (car argv))(cdr argv)))
add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")
(declaim(ftype(function(string string)t)add))
(defun add (file-name todo-item)
(with-append-file file-name
(write-line todo-item)))
view :: [String] -> IO ()
view [fileName] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStr $ unlines numberedTasks
素のCommon Lispで書くなら以下の通り。
(declaim(ftype(function(string)null)view))
(defun view(file)
(with-open-file(*standard-input* file)
(loop :for i :upfrom 0
:for line = (read-line nil nil)
:while line
:do (format t "~&~3D - ~A"i line))))
再現にこだわるなら、DEFACTION
マクロに手を入れて以下のようにしよう。
なお、後方互換性がなくなるので要注意。
(defmacro defaction(name lambda-list &body body)
(check-type name symbol)
`(PROGN (SETF (SYMBOL-FUNCTION ',name)
(LAMBDA ,lambda-list
(FUNCALL(ACTION ,@body))))
',name))
これで以下のように書ける。
(defaction view(file)
let ((todo-tasks(uiop:read-file-lines file))
(numbered-tasks (loop :for i :upfrom 0
:for line :in todo-tasks
:collect (format nil "~3D - ~A"i line))))
(put-string(unlines numbered-tasks)))
なお、SERIESを使って以下のように書いてもいい。
(defun view(file)
(series:collect-ignore
(series:map-fn 'null (lambda(i l)(format t "~&~D - ~A"i l))
(series:scan-range :from 0)
(series:scan-file file #'read-line))))
remove :: [String] -> IO ()
remove [fileName, numberString] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
[0..] todoTasks
putStrLn "There are your TO-DO items:"
mapM_ putStrLn numberedTasks
let number = read numberString
newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
bracketOnError (openTempFile "." "temp")
(\(tempName, tempHandle) -> do
hClose tempHandle
removeFile tempName)
(\(tempName, tempHandle) -> do
hPutStr tempHandle newTodoItems
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt")
(declaim(ftype(function(string string)t).remove))
(defun .remove (file-name number-string)
(let*((todo-tasks(uiop:read-file-lines file-name))
(numbered-tasks (loop :for line :in todo-tasks
:for i :upfrom 0
:collect (list i line)))
(number(parse-integer number-string))
(new-todo-items(loop :for task :in todo-tasks
:for i :upfrom 0
:unless (= i number)
:collect task)))
(format t "There are your TO-DO items:~%~{~D - ~A~%~}" numbered-tasks)
(with-safe-write-file "todo.txt"
(mapc #'write-line new-todo-items))))
9.6
random
ghci> random (mkStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
ghci> random (mdStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
ghci> random (mkStdGen 949494) :: (Int, StdGen)
(539963926, 466647808 1655838864)
ghci> random (mkStdGen 949488) :: (Float, StdGen)
(0.8938442, 1597344447 1655838864)
ghci> random (mkStdGen 949488) :: (Bool, StdGen)
(False, 1485632275 40692)
ghci> random (mkStdGen 949488) :: (Integer, StdGen)
(1691547873, 1597344447 1655838864)
Common LispのRANDOM
関数は0から第一引数に指定された数までのランダムな値を返す。
cl-user> (random 1000)
504
Common LispのRANDOM
関数は第二引数にランダムシードを受け付ける。
その規定値は*random-state*
で、本引数は破壊的に変更される。
ランダムシードを作るにはMAKE-RANDOM-STATE
を使う。
MAKE-RANDOM-STATE
はオプショナルに引数を取り、それがT
ならフレッシュなRANDOM-STATE
オブジェクトを返す。
それがNIL
なら*random-state*
のコピーを返す。
それがRANDOM-STATE
ならそのコピーを返す。
cl-user> (random 100(make-random-state))
44
cl-user> (random 100(make-random-state))
44
cl-user> (random 100(make-random-state t))
29
なお、RANDOM
の引数は整数かfloatが受け付け可能である。
cl-user> (random 100.0(make-random-state))
81.15838
cl-user> (random 100.0d0(make-random-state))
67.00164098613853d0
threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
let (firstCoin, newGen) = random gen
(secondCoin, newGen') = (random newGen)
(thirdCoin, newGen'') = (random newGen')
in (firstCoin, secondCoin, thirdCoin)
(declaim(ftype(function(random-state)list)three-coins))
(defun three-coins(state)
(let((*random-state* (make-random-state state)))
`(,(oddp(random 100))
,(oddp(random 100))
,(oddp(random 100)))))
More randoms
randoms' :: (RandomGen g, Random a) => g -> [a]
randoms' gen = let (value, newGen) = random gen in value:randoms' newGen
Haskellのrandoms
に相当する機能をCommon Lispで作るのは難しい。
というのもCommon Lispは遅延評価をする言語ではないからだ。
SERIESを使えば、無限に乱数を生成するSERIESオブジェクトを返す関数を作ることも可能だろう。
(defun randoms (&optional(state *random-state*))
(series:scan-fn 'fixnum
(lambda()(random most-positive-fixnum state))
(lambda(x)
(declare(ignore x))
(random most-positive-fixnum state))))
どこかで区切ってとり出さねばならないなら、それごと統合して一関数にしてしまうのが簡単だ。
(defun randoms (num &optional (*random-state* *random-state*))
(loop :repeat num :collect (random most-positive-fixnum)))
ghci> randomR (1,6) (mkStdGen 359353)
(6, 1494289578 40692)
ghci> randomR (1,6) (mkStdGen 35935335)
(3, 1250031057 40692)
Haskellに於けるrandomR
はCommon Lispには存在しない。
必要なら自作するしか無い。
(defun random-range(min max &optional(*random-state* *random-state*))
(+ min (random (1+(- max min)))))
ghci> take 10 $ randomRs ('a','z') (mkStdGen 3) :: [Char]
"ndkxbvmomg"
HaskellのrandomRs
もまたCommon Lispには無い。
作るなら、取り出しと統合して以下のようにするのがよろしかろう。
(defun random-ranges(length min max
&optional(*random-state* *random-state*))
(loop :repeat length :collect (random-range min max)))
文字列にしたいなら以下のようにするしかない。
(map 'string #'code-char(random-ranges 10 (char-code #\a)(char-code #\z)))
9.7
bytestring
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString as S
ghci> B.pack [99,97,110]
Chunk "can" Empty
ghci> B.pack [98..120]
Chunk "bcdefghijklmnopqrstuvwx" Empty
ghci> let by = B.pack [98,111,114,116]
ghci> by
Chunk "bort" Empty
ghci> B.unpack by
[98,111,114,116]
Common Lispは遅延評価をする言語ではないので、Haskellのbytestring
に相当するものがそもそもない。
なお、バイトベクタと文字列との変換にはbabelを使う。
cl-user> (defvar by (make-array 4 :element-type '(unsigned-byte 8)
:initial-contents '(98 111 114 116)))
BY
cl-user> (babel:octets-to-string *)
"bort"
cl-user> by
#(98 111 114 116)
cl-user> (babel:string-to-octets "hoge")
#(104 111 103 101)