maze.c to …
Table of Contents
see maze.c
前回は、C言語アルゴリズム辞典に出てた迷路作成プログラムをhaskell様に作 成させて実行してみた。勿論一発で動くはずはなく、永久ループの迷路に突入。 自ら体現させてくれるChatGPTは偉いなあ。そんな事を言ってる場合じゃない でしょう。
アルゴリズム辞典には、壁延長法による例が掲載されてたんだけど、他にどん な方法が有る? 野次馬根性で調べてみたら、 迷路生成(壁伸ばし法) こんな楽しいのに出あった。動く物大好きって、ガラ ガラ大好きな赤ちゃ んだな。
dump map
maze.c を久し振りにgdbにかけてみた。迷路のサイズは小さ目で 8x8にした。
下記は、迷路の初期化が終了した時の配列データ。1が壁(wall)、0が通路(path)に相当。
(gdb) x/64cx map 0x3b5fa340 <map>: 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x3b5fa348 <map+8>: 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x3b5fa350 <map+16>: 0x01 0x01 0x01 0x01 0x01 0x00 0x01 0x01 0x3b5fa358 <map+24>: 0x01 0x01 0x01 0x01 0x01 0x01 0x00 0x00 0x3b5fa360 <map+32>: 0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00 0x3b5fa368 <map+40>: 0x00 0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x3b5fa370 <map+48>: 0x00 0x00 0x00 0x01 0x01 0x01 0x01 0x01 0x3b5fa378 <map+56>: 0x01 0x01 0x01 0x00 0x01 0x01 0x01 0x01
こちらが普段使いのコマンドの結果。上記と微妙に結果が違うな。コードと照 らし合わせると、p/x の方が正しい。なんで?
(gdb) p/x map $1 = { {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x0, 0x1, 0x1, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}, {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}}
そしてこちらが壁を成長させていくべき始点となる座標。コメント上ではサイ トって呼称してる。
(gdb) p xx $1 = {4, 4, 2, 6, 0 <repeats 12 times>} (gdb) p yy $2 = {2, 6, 4, 4, 0 <repeats 12 times>}
これが登録完了すると、後は乱数でサイトを選択して壁を成長。もう壁を延長 できなくなったら、次のサイトを選択して同様に繰り返し。全部のサイトを実 施したら、迷路完成ってこと。大事な構造を確認しましたからね。
Main.hs
何度かChatGPTとチャットして、コンパイルエラーを潰した。奴はコンパイラー を持っていないので、オイラーが代行するのさ。奴もコンパイラーを内包して くれればいいのに。pythonなら人口が多いから内包するかも。いや、そりゃ無 理だろうよ。昔々のCGI問題にぶちあたるからね。その点、コンパイラー系な ら、コンパイルと実行とは全く別物だから、容易だろう。
外観検査
[sakae@fb /tmp/c2hmaze]$ grep :: app/Main.hs | cut -b-75 xMax, yMax, maxSite :: Int dx, dy :: [Int] dirTable :: [[Direction]] add :: [(Int, Int)] -> (Int, Int) -> [(Int, Int)] selsite :: [(Int, Int)] -> IO ([(Int, Int)], Maybe (Int, Int)) isValid :: Maze -> (Int, Int) -> Bool generateMaze :: IO Maze generateMaze' :: [(Int, Int)] -> Maze -> IO ([(Int, Int)], Maze) generatePath :: (Int, Int) -> [(Int, Int)] -> Maze -> IO ([(Int, Int)], Maz generatePathHelper :: [(Int, Int)] -> Maze -> IO ([(Int, Int)], Maze) let neighbors = [(x + dx !! fromEnum (dir :: Direction), y + dy generatePath' :: (Int, Int) -> [(Int, Int)] -> Maze -> IO ([(Int, Int)], Ma generatePath'' :: (Int, Int) -> [Direction] -> [(Int, Int)] -> Maze -> IO ( updateMaze :: Maze -> Int -> Int -> Int -> Int -> Int -> Int -> Maze replaceElem :: [a] -> Int -> a -> [a] main :: IO ()
maze.cでは、主要ロジックがmainの中に閉じ込められていたけど、それが適当 な単位に分割されて関数になってる。補助関数もwhereで内包するのを嫌って 分離してる。
main = do maze <- generateMaze mapM_ putStrLn maze
haskellではmainを数行内に収めるってのが鉄則。やりたい主張が良くわかる。 なお、maze.cでmainに集中してるのは、辞書と言う事で、圧縮した記述になっ ているのだろう。
小手調べで、独立した関数の比較
maze.cで独立した関数になってるサイトを乱数で選ぶ部分を、haskellと対比 させてみる。尚オリジナルでは、selectとなってたけど、これだとCのライブ ラリィーと名前がバッティングするので、変更してる。
int xselect(int *i, int *j) { int r; if (nsite == 0) return 0; /* サイトが尽きた */ nsite--; r = (int)(nsite * (rand() / (RAND_MAX + 1.0))); *i = xx[r]; xx[r] = xx[nsite]; *j = yy[r]; yy[r] = yy[nsite]; return 1; /* 成功 */ }
この関数は、whileの中からcallされるので、その制御用に return 0/1 が用 意されてる。肝心の選んだサイトのX,Y座標は、i/jを更新する事で、親側に伝 えてる。これが、haskellだと、ちょっと関数名が異なるけど
selsite :: [(Int, Int)] -> IO ([(Int, Int)], Maybe (Int, Int)) selsite [] = return ([], Nothing) selsite sites = do r <- randomRIO (0, length sites - 1) let selected = sites !! r let remaining = take r sites ++ drop (r + 1) sites return (remaining, Just selected)
サイトを表現するsitesが入力。よく見ると複数を表わすsが変数名になってる。 見落しがちだけど、大事な情報だ。座標はタプル表現。出力は、タプル。 Maybeを付けてある方は、選択できたらって意味だ。
乱数で、入力のうちの一つを選ぶ。選んだ物を取り除けば、残りになる。 takeとdropを上手に使って、残り物を計算してるね。結局、Just selectedが 肝。これがNotiingなら、maze.cのreturn 0になるし、Justならば、その中身 のタプルが、i/j 相当となる。
今度は冒頭から構造の比較
char map[XMAX + 1][YMAX + 1]; /* 地図 */ int nsite = 0; /* 登録サイト数 */ int xx[MAXSITE], yy[MAXSITE]; /* 登録サイト座標 */ int dx[4] = { 2, 0, -2, 0 }; /* 変位ベクトル */ int dy[4] = { 0, 2, 0, -2 }; /* 変位ベクトル */ int dirtable[24][4] = { /* 方向表 */ 0,1,2,3, 0,1,3,2, 0,2,1,3, 0,2,3,1, 0,3,1,2, 0,3,2,1, :
map,xx,yyがしっかり配列として鎮座してる。その他、方向を決定する為のテー ブルも配列として、グローバルな場所に用意してる。haskell側はどうよ?
type Maze = [[Char]] data Direction = North | East | South | West deriving (Enum, Bounded) dx, dy :: [Int] dx = [0, 1, 0, -1] dy = [-1, 0, 1, 0] dirTable :: [[Direction]] dirTable = [ [North, East, South, West] , [North, East, West, South] :
mapに相当する配列は、Mazeって名前で呼んでくれって宣言が有るのみ。それ から、サイト座標を表わす xx/yy配列に至っては、名前すら出てこない。
方向テーブルはhaskellの拘りの表現になってる。日本人なら、東西南北より、 上下左右を選ぶだろうけど、あちらの人は、この表現の方を好むのか。 そうか、上下って、ちと曖昧な表現だな。平面じゃなくて、立体での上下って 解釈もできるからね。
昔のパソコンの中身の説明でCPUを中心に置いて、ノースブリッジやらサウス ブリッジなんてのが出てきて、しばし何の事やらと悩んだものだ。それから、 javaでお絵書きした時も、東西南北が出てたので、普通の事なんだな。
迷路中心 generateMaze
いよいよ核心部分に迫って行く。こんな定義だ。
generateMaze :: IO Maze generateMaze = do let initialMaze = replicate yMax (replicate xMax '#') let sites = foldl add [] [(i, j) | i <- [0, 2 .. xMax - 2], j <- [0, 2 .. yMax - 2]] (_, maze) <- generateMaze' sites initialMaze return maze
本当の核心は、ダッシュの付いた generateMaze' なんだけどね。だから、こ の関数は、generateMaze' を呼び出す準備をしてるんだな。引数は、ここで作 成した、初期状態のMazeとサイトだ。だから、グローバルに配列を用意するっ て事は無いんだ。そして、変更されて作成されたmazeが、最終結果になるとな。
ちょいと実験
debuggerを使うのは難儀なので、ghci上で実験。
λ> let wall = replicate yMax (replicate xMax '#') λ> mapM_ putStrLn wall ######## ######## ######## ######## ######## ######## ######## ########
立っている者は、親でも使えって言われているので、mainの中の出力方法を利 用してみた。望み通りの結果。続いて、サイトの作成。
λ> let sites = foldl add [] [(i, j) | i <- [0, 2 .. xMax - 2], j <- [0, 2 .. yMax - 2]] λ> sites [(6,6),(6,4),(6,2),(6,0),(4,6),(4,4),(4,2),(4,0),(2,6),(2,4),(2,2),(2,0),(0,6),(0,4),(0,2),(0,0)]
配列の変更
立っている者は親でも使えの第二弾。ChatGPTが提示してきた、replaceElemを 利用して、ピンポイントで、2次元配列を変更する関数を作成。それなりに英 語チックに読めるのは、提示された関数だ。オイラーのは意味不。
pinW :: Int -> Int -> Maze -> Maze pinW x y maze = replaceElem maze y (replaceElem (maze !! y) x '.') replaceElem :: [a] -> Int -> a -> [a] replaceElem lst idx val = take idx lst ++ [val] ++ drop (idx + 1) lst
本当は、Maze -> Int -> Int -> Maze にしたいんだけど、後の都合を考えて こうしてる。とりあえずテスト。
λ> mapM_ putStrLn $ pinW 5 2 wall ######## ######## #####.## ######## ######## ######## ######## ########
良さそうなので、発展させる。複数の場所の変更バージョン。
λ> let z = foldr (\s -> pinW (fst s) (snd s)) wall [(2,3), (7,5)] λ> mapM_ putStrLn z ######## ######## ######## ##.##### ######## #######. ######## ########
何は無くとも、要素技術を追加したぞ。GPTがアイデアを提示してくれるんで 楽だ。丁度スタートアップ企業で 0 -> 1 にするアイデア出しがGPT担当で、1 -> 2 にするのがオイラーの役目って所かな。
ちゃんと構築してるか?
C言語は普通に何でもできる奴。それに対してhaskellは小回りが効かない風。 適当にやっちゃっていないか?
maze.cでの初期化部分だ。
for (i = 0; i <= XMAX; i++) /* 地図を初期化 */ for (j = 0; j <= YMAX; j++) map[i][j] = 1; for (i = 3; i <= XMAX - 3; i++) for (j = 3; j <= YMAX - 3; j++) map[i][j] = 0; map[2][3] = 0; map[XMAX - 2][YMAX - 3] = 0; for (i = 4; i <= XMAX - 4; i += 2) { /* サイトを加える */ add(i, 2); add(i, YMAX - 2); } for (j = 4; j <= YMAX - 4; j += 2) { add(2, j); add(XMAX - 2, j); } B => while (selran(&i, &j)) { /* サイトを選ぶ */
上で出てきた、generateMazeと比べてみると、明らかに手抜きした感がある。 例えば、sitesの登録部分。適当に内包表記してる。3,4番目のforを実現する のは、下記だろう。こうして取り出した物を使って、add関数を呼び出す必要 がある。
λ> let q = [(i, j) | i <- [4,6 .. xMax - 4], j <- [4,6 .. yMax - 4]] λ> q [(4,4)]
裏のmain
本当のmainはgenerateMaze' になる。
enerateMaze' :: [(Int, Int)] -> Maze -> IO ([(Int, Int)], Maze) generateMaze' [] maze = return ([], maze) generateMaze' sites maze = do (sites', maybeSite) <- selsite sites case maybeSite of Nothing -> return (sites', maze) Just (x, y) -> do (_, maze') <- generatePath (x, y) sites' maze generateMaze' sites' maze'
注目は、この関数の終了条件。ちゃんとsitesが尽きた時って指定されている。 それ以外は、selsiteを呼出、壁を成長させる作業を実施。何か玉葱の皮を永 遠と剥いて行く風だな。きりが無いので、generatePathに注目してみる。
generatePathの中
更に奥の所に、こんなのが出てきてた。提示されたものは、一行で表現されて たけど、内包表記の構造が分かり易いように、変更してみた。
let neighbors = [(x + dx !! fromEnum (dir :: Direction), y + dy !! fromEnum (dir :: Direction)) | dir <- [minBound .. maxBound]]
見慣れない関数が出てきた。調べてみると、Enumの値を数値に変換するみたい。 minBoundとかは、その範囲ね。EnumなんてC語にも普通に出てくるけど、 haskellでは、面倒な事を強いる訳ね。まあ、それが安心材料になるんだけど ね。
λ> fromEnum North 0 λ> fromEnum West 3
続いて実行
λ> dx [0,1,0,-1] λ> dy [-1,0,1,0] λ> let x = 0 λ> let y = 0 λ> let near = [(x + dx !! fromEnum (dir :: Direction), y + dy !! fromEnum (dir :: Direction)) | dir <- [minBound .. maxBound]] λ> near [(0,-1),(1,0),(0,1),(-1,0)]
何か無駄な空振りしてないか。単に自慢したいだけ?
足りないぞ
maze.cでは、方向テーブルから方向を乱数で選択してる。
tt = dirtable[(int)(24 * (rand() / (RAND_MAX + 1.0)))];
対してhaskell側では、こんなのだ。ここ一箇所しかない。別な角度って事で randを探しても、selsiteの中だけだ。よって、インチキ捏造が確定した。
let directions = dirTable !! fromEnum (maze !! snd current !! fst current == ' ')
これ以上、探索しても無駄だな。
せいぜい、部品取りとか、普段ではお目にかかる事が無い関数に遭遇して、そ れ調べてみろぐらいにしか、使い道がない。
今回の試行で得る所が有ったとするなら、リストを1次元もしくは2次元と見做 した時の、更新方法を手に入れた事だろう。それとて、演算量は、O(n) なの で、使う場所を選ぶだろう。
今回は、不本意ながら、これで終了する。