digging with bing

Table of Contents

digging with ChatGPT

「AIだからすごいんでしょ」 うーん、アルゴリズムの表現の場合は、どうなんでしょ?

前回からやってるChatGPTに依頼した迷路作成だが、永久ループの泥沼から脱 出出来ずに、無理を指示しちゃった。AIと言えども、ヒントを与えないと頭を 働かせられない。debuggerを使って、堂々巡りしてる所を発見し、そこを改善 させたのさ。その結果が、これだ。

ghci> :main
##########
# # # # #
##########
# # # # #
##########
# # # # #
##########
# # # # #
##########
# # # # #

迷路失格である。原因と思う所は、配列の範囲外をアクセスしちゃう部分の防 止策。なんだかなあって方法で回避してたぞ。今となっては、それを追求する 気にもなれない。穴掘り方法だけを上げておく。

dig :: Maze -> (Int, Int) -> StdGen -> IO ()
dig maze (x, y) gen = do
  let directions = shuffle [minBound..maxBound] gen
  dig' maze (x, y) directions gen

dig' :: Maze -> (Int, Int) -> [Direction] -> StdGen -> IO ()
dig' maze _ [] _ = return ()
dig' maze (x, y) (dir:dirs) gen = do
  let nextCell = move dir (x, y)
      wallCell = move (opposite dir) nextCell
  isWallCellVisited <- readArray maze wallCell
  isNextCellVisited <- readArray maze nextCell  -- 新しく追加
  if isWallCellVisited == wall || isNextCellVisited == path  -- 新しく追加
    then dig' maze (x, y) dirs gen
    else do
      writeArray maze nextCell path
      writeArray maze wallCell path
      dig maze nextCell gen
      let remainingDirs = delete dir dirs
      randomIndex <- randomRIO (0, length remainingDirs - 1)
      let nextDir = remainingDirs !! randomIndex
      dig' maze nextCell (delete nextDir remainingDirs) gen

何だかなあの結末。

digging with bing

ならばbingではどうよ。セカンド・オピニオンしてみる。

穴掘り法を利用して、迷路を作成して下さい。出力言語はhaskellでお願いします。
盤の構造には、Array.IOを使用して下さい。

haskell maze digging method を検索しています

上の質問が、こんな風に要約されてるみたい。で、bingは何を参考書にしたん だやら。こんなコードを提示してきた。ああ、勿論オイラーが悪い所は修正し ましたよ。特にshuffleなんて出鱈目のいいところだったので、書き換えた。

それから、質問の回数が以前は5回だったんだけど、今回は10回に増量されて た。みんなオマケが大好きだからなあ。煎餅が1枚増量なんて言われると、い ちもにもなく飛び付きますからなあ。

import System.Random
import Data.Array.IO
import Control.Monad
import Debug.Trace

type Maze = IOUArray (Int, Int) Char

maze :: Int -> Int -> IO Maze
maze rows cols = newArray ((0, 0), (rows - 1, cols - 1)) '#'

dig :: Maze -> (Int, Int) -> IO ()
dig maze (row, col) = do
    writeArray maze (trace("(row,col)=" ++ show (row,col)) (row, col)) ' '
    directions <- shuffle [(0, -1), (-1, 0), (0, 1), (1, 0)]
    forM_ directions $ \(r, c) -> do
        let loc = (row + r * 2,  col + c * 2)
        okP <- liftM2 (&&)
          (inRange <$> getBounds maze <*> pure loc)
          (((==) '#') <$> readArray maze loc)
        when okP $ do
            writeArray maze (r + row, c + col) ' '
            dig maze loc

shuffle :: [a] -> IO [a]
shuffle xs = shuffle' xs []
  where
    shuffle' [] ys = return ys
    shuffle' zs ys  = do
      index <- randomRIO (0, length zs - 1)
      let elem = zs !! index
          zs' = take index zs ++ drop (index + 1) zs
      shuffle' zs' (elem : ys)
      
printMaze :: Maze -> IO ()
printMaze maze = do
    ((minRow, minCol), (maxRow, maxCol)) <- getBounds maze
    forM_ [minRow..maxRow] $ \row -> do
        forM_ [minCol..maxCol] $ \col -> do
            putChar =<< readArray maze (row, col)
        putChar '\n'

main :: IO ()
main = do
    let rows = 21 -- odd number to make the algorithm work properly.
        cols = 21 -- odd number to make the algorithm work properly.
    m <- maze rows cols
    dig m (1, 1)
    printMaze m

で、結果。

ghci> main
(row,col)=(1,1)
 *** Exception: Error in array index
ghci> main
(row,col)=(1,1)
(row,col)=(1,3)
(row,col)=(3,3)
(row,col)=(3,1)
 *** Exception: Error in array index

なんだかなあ、だ。

start debug

okP <- liftM2 (&&)
  (inRange <$> getBounds maze <*> pure loc)
  (((==) '#') <$> readArray maze loc)

指定されたlocが配列の範囲内になってるなら、そこからreadして、壁なら満 足って理解した。評価の都合で、先にreadしちゃったら、ドカンとなる可能性 が有るな。

okP <- liftM2 (&&)
  (inRange <$> getBounds maze <*> pure (trace("in=" ++ show loc) loc))
  (((==) '#') <$> readArray maze (trace("rd=" ++ show loc) loc))

こういうdebug文を挿入。

ghci> main
rd=(1,3)
in=(1,3)
rd=(1,1)
in=(1,1)
rd=(-1,3)
 *** Exception: Error in array index

こういう事で、足を掬われるんですなあ。ならば、inRangeを後ろに配置して みるか。が、、、駄目だった。コンパイラが勝手に実行順番を入れかえちゃっ たぞ。こういう微妙なコードは禁止って事なのか。

repair it

原因が判明すれば、対処は易しい事だ。下記の様に修正した。

dig :: Maze -> (Int, Int) -> IO ()
dig maze (row, col) = do
    writeArray maze (row, col) ' '
    directions <- shuffle [(0, -1), (-1, 0), (0, 1), (1, 0)]
    forM_ directions $ \(r, c) -> do
        let loc = (row + r * 2,  col + c * 2)
        okP <- liftM (&& True) (inRange <$> getBounds maze <*> pure loc)
        when okP $ do
          kw <- readArray maze loc
          when ((==) kw  '#') $ do
            writeArray maze (r + row, c + col) ' '
            dig maze loc

そして、実行例。入口と出口が明確に示されていないけど、暗黙の了解事項と して、左上と右下が相当する。digの最後に、これらの壁を撤去するのが正式 だろうけど、それはNHKの解体キングダムにでも、任せておけば良い。

λ> main
#####################
# #     #           #
# # ### # ####### # #
# #   #         # # #
# ############### ###
# #         #   #   #
# # ####### # # ### #
#   #   #   # #     #
##### # # ### ##### #
#   # #   #   # #   #
### # ##### ### # ###
#   # #       # #   #
# # # ####### # ### #
# # #         #     #
# # ########### #####
# #           #     #
# ################# #
#     #   #       # #
# ### # # # ### ### #
#   #   #     #     #
#####################

エラーが一杯

なにくわぬ顔で結果をさっと提示したけど、苦難の道があったのよ。あえて、 失敗事例をあげておく。

まずは、liftM2 -> liftM への変更だ。

λ>  :t liftM2
liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
λ>  :t liftM
liftM :: Monad m => (a1 -> r) -> m a1 -> m r

当初はliftM2が使われていた。そう (&&) で、評価したいから。それが、一つ になる場合はどうする家康。

すごいH本の344ページに答が出てた。通読は役にたたない。実例に遭遇して、 意味が分ってくるものなのね。

λ>  liftM (+5) (Just 3)
Just 8

ならば、第一引数は、(&& True) でいいな。壁かどうかの判定は、常にYesっ て事にしちゃえばいいんだ。これに思い至るまで、随分と余計な事をして、そ のエラーの撲滅で無駄をしてた。無知なんだから我流は、いかんぜよ。

次は、あられもない失敗。最初、こう記述しちゃったんだ。

when (==) kw  '#' $ do

その結果、とんでもないエラーに見舞われた。

app/Main.hs:19:11: error:
    • Couldn't match type ‘()’ with ‘IO () -> IO ()’
      Expected: Char -> IO () -> IO ()
        Actual: Char -> ()
    • In the result of a function call
      In the first argument of ‘($)’, namely ‘when (==) kw '#'’
      In a stmt of a 'do' block:
        when (==) kw '#'
          $ do writeArray maze (r + row, c + col) ' '
               dig maze loc
   |
19 |           when (==) kw  '#' $ do
   |           ^^^^^^^^^^^^^^^^^

app/Main.hs:19:16: error:
    • Couldn't match expected type ‘Bool’
                  with actual type ‘a0 -> a0 -> Bool’
    • Probable cause: ‘(==)’ is applied to too few arguments
      In the first argument of ‘when’, namely ‘(==)’
      In the first argument of ‘($)’, namely ‘when (==) kw '#'’
      In a stmt of a 'do' block:
        when (==) kw '#'
          $ do writeArray maze (r + row, c + col) ' '
               dig maze loc
   |
19 |           when (==) kw  '#' $ do
   |                ^^^^

app/Main.hs:19:21: error:
    • Couldn't match expected type ‘Char -> ()’ with actual type ‘Char’
    • In the second argument of ‘when’, namely ‘kw’
      In the first argument of ‘($)’, namely ‘when (==) kw '#'’
      In a stmt of a 'do' block:
        when (==) kw '#'
          $ do writeArray maze (r + row, c + col) ' '
               dig maze loc
   |
19 |           when (==) kw  '#' $ do
   |                     ^^

通常は、最初のエラーに注目して、それを潰す努力をするものだけど、例によっ て意味不明。ええ、色々とあがきましたよ。でエラーがエラーを呼ぶという悪 循環。

相当草臥れて、ボーとコーヒーを飲みながら、別のエラーを眺めていたんだ。 そしたら、

• Probable cause: ‘(==)’ is applied to too few arguments

これが目に止まった。これは関数なのに、引数が足りない。ピーンと気付いた ぞ。関数一式を括弧でくくれ。whenの侵略を許すな。

こうして、3件のエラーは、スーと消えたよ。エラーは、頼もしい助言者。邪 険にするな。少くとも、各エラーの冒頭から数行には、目を配ってあげよう。 ヒント満載だぞ。

thinking

今回の見せ場は、評価の順番に尽きる。記述した通りに実行される保証はないっ て事だ。

(and exp1 exp2)   ;; for scheme
(&&) exp1 exp2    -- for haskell

とやれば、schemeなら、exp1を評価した時点で結果がfalseなら、次のexp2は 評価されない。だって、S式全体でfalseって事が確定してるから。いわゆる ショートカットって奴だ。これで、多少の無駄(exp2を評価する事)が無くなる。 大事なテクニックとして、多用されてるぞ。

これに対してhaskellは、上で見たように、schemerとかの常識を逸脱してる。 内部的には、ショートカットが(多分)利用されてるだろうけど、評価の順番は haskellのコンパイラー次第って事だ。まあ、どちらの式の方が計算コストが 低いかとか吟味するんだろうけど。。

評価の順番で思い出したのは、他にもある。そう、letだ。 scheme: 変数束縛 に説明が有るように、letとlet*の違いね。letの場合は評価の順番は保証せず。 let*は、記述した順番通りに評価していく。

じゃhaskellは? 遅延評価だけだと出力の順番が定まらない例 上手い例だなあ。

Haskell の並列処理と遅延評価の罠 こういう楽しい記事も発見したぞ。 このサイト昔は良く見ていたなあ。

let、whereはなぜ必要か?【Haskellにおける局所的な変数】 関連でこういう も出てきた。

others

bingに指示した壁延し法により迷路作成。インチキっぽいな。適当に寄せ集め したんではなかろうか。落ちるしね。

import Data.Array.IO
import Control.Monad
import System.Random

type Maze = IOArray (Int, Int) Int

makeMaze :: Int -> Int -> IO Maze
makeMaze w h = do
    maze <- newArray ((0, 0), (w-1, h-1)) 1
    let start = (1, 1)
    dig maze start
    return maze

dig :: Maze -> (Int, Int) -> IO ()
dig maze pos@(x, y) = do
    writeArray maze pos 0
    let neighbors = [(x-2, y), (x+2, y), (x, y-2), (x, y+2)]
    validNeighbors <- filterM (\n -> do
        val <- readArray maze n
        return $ val == 1) neighbors
    case validNeighbors of
        [] -> return ()
        ns -> do
            next <- choose ns
            let mid = ((fst pos + fst next) `div` 2,
                       (snd pos + snd next) `div` 2)
            writeArray maze mid 0
            dig maze next
            dig maze pos

choose :: [a] -> IO a
choose xs = do
    i <- randomRIO (0, length xs - 1)
    return $ xs !! i

printMaze :: Maze -> IO ()
printMaze maze = do
    (_, (maxX, maxY)) <- getBounds maze
    forM_ [0..maxY] $ \y -> do
        forM_ [0..maxX] $ \x -> do
            cell <- readArray maze (x, y)
            putStr $ if cell == 1 then "#" else " "
        putStrLn ""

main :: IO ()
main = do
  out <- makeMaze 11 11   -- must be odd
  printMaze out

ならば、少し復習。

なでしこさんで迷路を自動生成したり、探索したり

迷路を作ろう[手法編]​

迷路作成アルゴリズムまとめ

こちらはhaskellでAIですってさ。

Learning to Navigate the Maze! in Haskell

etc

こういうのを博物館と言うんだろうな。色々な言語での迷路作成。 色々な言語でハロワじゃ子供騙しだからね。せめてこれぐらいは、やってよね、 です。

Maze generation by many lang

そして、こちらは、言語コンバータです。裏でChatGPTが動いているに違いな い。

Welcome to Code Language Converter!


This year's Index

Home