order to Bing (2)

Table of Contents

受け入れ検査

前回の続きをやっていく。

開けたら閉めよ

CSVファイルがbusyだよってんで、例外が発生してた。 ファイルを開いたまま閉じない所業っぽい。

oldRecords <- readCSV fileName -- 既存のレコードを読み込む
let newRecords = mergeRecords oldRecords records -- マージする
writeCSV ("new-" ++ fileName) newRecords -- 書き込む

簡単に確認する方法として、writeCSVに新しいファイル名を与えてみる。

λ>  :main 2308
2308> 1004 120 60 50
2308> fin
CSVファイルに追記しました

今度は、ちゃんと書き込みできたっぽい。一応確認。

[sakae@deb hbld]$ diff test.csv new-test.csv
6a7
> 23081004,120,60,50

後は、すごいH本の181ページのように、readCSVを改造すればいいんだな。 オープンしたファイルはクローズするのが原則。それを忘れたら、某巨大国家 の田舎の厠みたいだぞ。ああ、あちらは、最初から扉なんて無かったっけ?

みんなメモリー・リソースばかり気にするけど、少しはファイル・リソース (ディスクリプター)にも気を配ってください。

Bingが提案したコードって、少々危いな。書き込みに失敗すると、データを失 なう重大事故になる。readCSVした後、renameして、バックアップとするのが 良いだろう。まあ、オイラーの指示がぬけていたんだけどね。

お前は既に死んでいる

今度は、入力時のリミット・チェックが機能してるか確認。判りやすいように、 お前は既に死んでいる というデータを与えてみる(血圧、脈共に0)。

λ> :main 2308
2308> 1121 0 0 0
入力が不正です。日時とhigh low plsをスペース区切りで入力してください

highの範囲は、どーたらです、ってのがでてこないぞ。

ちょっとプリチェックで、文字列を整数に変換する関数。

λ> :readMaybe "120"
Just 120
λ> readMaybe "12X"
Nothing

次は、第3引数が、範囲内にはいっているかの確認。

λ>  inRange 80 150 120
True
λ>  inRange 80 150 0
False

上記はreplからの単独実行だったので、次は、本チャンで呼出があるか、久し ぶりに、debuggerしてみる。

λ> :break inRange
Breakpoint 0 activated at /tmp/hbld/app/Main.hs:81:22-42
λ> :main 2308
2308> 1205 0 0 0
Stopped in Main.inRange, /tmp/hbld/app/Main.hs:81:22-29
_result :: Bool = _
low :: Int = 80
n :: Int = _
80  inRange :: Int -> Int -> Int -> Bool
81  inRange low high n = low <= n && n <= high
                         ^^^^^^^^
82
[/tmp/hbld/app/Main.hs:81:22-29] λ> :step
Stopped in Main.parseInput, /tmp/hbld/app/Main.hs:61:41-71
_result :: Maybe () = _
60      high' <- readMaybe high -- highを整数に変換する。失敗したらNothingを返す。
61      when (not $ inRange 80 180 high') $ fail "highは80から180の範囲で入力してください" -- highが範囲内かチェックする。
                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62      low' <- readMaybe low -- lowを整数に変換する。失敗したらNothingを返す。
[/tmp/hbld/app/Main.hs:61:41-71] λ> :step
Stopped in Main.inputLoop, /tmp/hbld/app/Main.hs:(46,20)-(48,37)
_result :: IO () = _
records :: [Record] = _
yearMonth :: String = "2308"
45            inputLoop yearMonth newRecords -- 入力待ちになる
                     vv
46          Nothing -> do -- パースに失敗したら
47            putStrLn "入力が不正です。日時とhigh low plsをスペース区切りで入力してください" -- 理由を説明する
48            inputLoop yearMonth records -- 入力待ちになる
                                         ^^
49
[/tmp/hbld/app/Main.hs:(46,20)-(48,37)] λ> :step
Stopped in Main.inputLoop, /tmp/hbld/app/Main.hs:47:11-61
_result :: IO () = _
46          Nothing -> do -- パースに失敗したら
47            putStrLn "入力が不正です。日時とhigh low plsをスペース区切りで入力してください" -- 理由を説明する
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48            inputLoop yearMonth records -- 入力待ちになる

failの文字列が表示されないまま、上位の階層に移っているな。

λ> :i fail
type MonadFail :: (* -> *) -> Constraint
class Monad m => MonadFail m where
  fail :: String -> m a
        -- Defined in ‘Control.Monad.Fail’

表示って勝手に解釈したけど、そうでもないみたい。

H本の300ページに若干の解説があったんで、ちょいと実験。

fail s = error s
を、追加しておくと、
2308> 0104 0 0 0
 *** Exception: highは80から180の範囲で入力してください
CallStack (from HasCallStack):
  error, called at /tmp/hbld/app/Main.hs:126:10 in main:Main

かと言って、failをprintに変更すると

/tmp/hbld/app/Main.hs:61:5-72: error:
    • Couldn't match type ‘IO’ with ‘Maybe’
      Expected: Maybe ()
        Actual: IO ()
    • In a stmt of a 'do' block:
        when (not $ inRange 80 180 high')
          $ print "highは80から180の範囲で入力してください"

こんな風に怒りを買ってしまう。

関数型プログラミング入門〜モナド変換子について

順を追って理解する MaybeT モナドの使い方

モナド変換子 MaybeT

こんな記事が、じんわりと効いてくるのかなあ。

ask again

ええい、相談しちゃえ。世の中のテクノロジーには、あがらえませんから。

haskellでCSVファイルに追記するプログラムを作成するで
提案されたコードで、範囲外データを入力しても、エラー
メッセージが出ない

:main 2308
2308> 1121 0 0 0
入力が不正です。日時とhigh low plsをスペース区切りで入力してください

ではなくて、
highは80から180の範囲で入力してください
が出てくるべき
こんにちは、Bingです。😊 HaskellでCSVファイルに追記するプログラムを作成するのは素晴らしいと思います。
提案されたコードで、範囲外データを入力しても、エラー メッセージが出ないという問題があるのですね。

エラーメッセージを変更するには、以下のようにコードを修正する必要があります。

こんな提案をしてきた。

-- 範囲外の値をチェックする関数
checkRange :: Int -> Int -> Int -> Bool
checkRange low high x = x >= low && x <= high

-- highの範囲を80から180に設定する
highRange :: (Int, Int)
highRange = (80, 180)

-- エラーメッセージを出力する関数
printError :: String -> IO ()
printError msg = putStrLn $ "入力が不正です。" ++ msg

-- 入力されたデータを検証する関数
validateInput :: [String] -> IO Bool
validateInput [date, high, low, pls] = do
  let high' = read high :: Int
  let low' = read low :: Int
  let pls' = read pls :: Int
  if not (checkRange (fst highRange) (snd highRange) high')
    then do
      printError $ "highは" ++ show (fst highRange) ++ "から" ++ show (snd highRange) ++ "の範囲で入力してください"
      return False
    else if not (checkRange 0 100 low')
      then do
        printError "lowは0から100の範囲で入力してください"
        return False
      else if not (checkRange 0 100 pls')
        then do
          printError "plsは0から100の範囲で入力してください"
          return False
        else return True
validateInput _ = do
  printError "日時とhigh low plsをスペース区切りで入力してください"
  return False

初回のものより、大分後退したように思えるけど、どうよ?

別の日に再チャレンジしてみた。 漫然と要求するんじゃなくて、関数を指定して、改造要求してみたんだ。Eitherを 使うように、提案してきたぞ。

-- 入力をパースしてレコードに変換する関数。パースに失敗したらLeftでエラーメッセージを返す。
parseInput :: String -> String -> Either String Record
parseInput yearMonth input = case words input of -- スペースで分割する
  [dayHour, high, low, pls] -> do -- 要素が4つの場合
    let ymdh = yearMonth ++ dayHour -- 年月と日時を合成する
    unless (isValidDayHour dayHour) $ Left "日時はDDHHの形式で入力してください" -- 日時が正しいかチェックする
    high' <- maybeToEither "highは整数で入力してください" $ readMaybe high -- highを整数に変換する。失敗したらLeftでエラーメッセージを返す。
    unless (inRange 80 180 high') $ Left "highは80から180の範囲で入力してください" -- highが範囲内かチェックする。
    low' <- maybeToEither "lowは整数で入力してください" $ readMaybe low -- lowを整数に変換する。失敗したらLeftでエラーメッセージを返す。
    unless (inRange 50 110 low') $ Left "lowは50から110の範囲で入力してください" -- lowが範囲内かチェックする。
    pls' <- maybeToEither "plsは整数で入力してください" $ readMaybe pls -- plsを整数に変換する。失敗したらLeftでエラーメッセージを返す。
    unless (inRange 40 120 pls') $ Left "plsは40から120の範囲で入力してください" -- plsが範囲内かチェックする。
    return (ymdh, high', low', pls') -- レコードを返す
  _ -> Left "入力はスペース区切りの4つの要素である必要があります" -- 要素が4つでない場合はLeftでエラーメッセージを返す

-- Maybe値をEither値に変換する補助関数。Nothingの場合は指定されたエラーメッセージをLeftに入れる。
maybeToEither :: String -> Maybe a -> Either String a
maybeToEither msg Nothing = Left msg
maybeToEither _ (Just x) = Right x

それでいいんだけど、この提案が上位の関数に波及するな。

-- 入力待ちになる関数
inputLoop :: String -> [Record] -> IO ()
inputLoop yearMonth records = do
  putStr $ yearMonth ++ "> " -- プロンプトに年月が表示される
  input <- getLine -- ユーザーは日時に続いて high low pls を入力する
  if input == endInput -- finが入力されたら
    then do -- CSVファイルとマージして同名なファイル名で保存する
      oldRecords <- readCSV fileName -- 既存のレコードを読み込む
      let newRecords = mergeRecords oldRecords records -- マージする
      writeCSV fileName newRecords -- 書き込む
      putStrLn "CSVファイルに追記しました"
    else do -- fin以外が入力されたら
      either handleError handleRecord $ parseInput yearMonth input -- 入力をパースしてエラーかレコードか分岐する

-- エラーメッセージを表示して入力待ちになる関数
handleError :: String -> IO ()
handleError msg = do
  putStrLn $ "入力が不正です。" ++ msg -- エラーメッセージを表示する
  inputLoop yearMonth records -- 入力待ちになる

-- レコードを挿入して入力待ちになる関数
handleRecord :: Record -> IO ()
handleRecord record = do
  let newRecords = insertRecord records record -- レコードを挿入する
  inputLoop yearMonth newRecords -- 入力待ちになる

注目は、下記。綺麗に分離してくれた。

else do -- fin以外が入力されたら
  either handleError handleRecord $ parseInput yearMonth input -- 入力をパースしてエラーかレコードか分岐する

そのおかげで、yearMonthとrecordsがnot in scopeになってしまった。人間的なエラーを仕出かすなあ。オイラーもやりそ うな失敗だ。

-- 入力待ちになる関数
inputLoop :: String -> [Record] -> IO ()
inputLoop yearMonth records = do
  putStr $ yearMonth ++ "> " -- プロンプトに年月が表示される
  input <- getLine -- ユーザーは日時に続いて high low pls を入力する
  if input == endInput -- finが入力されたら
    then do -- CSVファイルとマージして同名なファイル名で保存する
      oldRecords <- readCSV fileName -- 既存のレコードを読み込む
      let newRecords = mergeRecords oldRecords records -- マージする
      writeCSV fileName newRecords -- 書き込む
      putStrLn "CSVファイルに追記しました"
    else do -- fin以外が入力されたら
      either (handleError yearMonth records) (handleRecord yearMonth records) $ parseInput yearMonth input -- 入力をパースしてエラーかレコードか分岐する

-- エラーメッセージを表示して入力待ちになる関数。yearMonthとrecordsを引数として受け取る。
handleError :: String -> [Record] -> String -> IO ()
handleError yearMonth records msg = do
  putStrLn $ "入力が不正です。" ++ msg -- エラーメッセージを表示する
  inputLoop yearMonth records -- 入力待ちになる

-- レコードを挿入して入力待ちになる関数。yearMonthとrecordsを引数として受け取る。
handleRecord :: String -> [Record] -> Record -> IO ()
handleRecord yearMonth records record = do
  let newRecords = insertRecord records record -- レコードを挿入する
  inputLoop yearMonth newRecords -- 入力待ちになる

ここまでで、今日の分を使い果した。 ってか、ユーザーとBingの不毛な喧嘩を避ける為、やりとりは5回までに制限 されているようだ。Bingは常に強気で回答してくる。それに対してカチンとき たユーザーが反論する。それにBingが反論。フレーム(喧嘩)が何時までたっても終了 しない困った状況を回避する為の苦肉の策らしい。もっと大人になれよ、人間 どもよ。

ghci> :main 2308
2308> 0104 120 0 50
入力が不正です。lowは50から110の範囲で入力してください
2308> 0104 120 60 50
2308>

望み通りの動作になったな。

肝は、eitherの活用なんだな。昔お目にかかった事があるけど、ピンとこなかっ た。今回は自分事なんで、しっかりと血肉になった(と思うぞ)。

haskellの常識は非常識

冒頭の、開けたら閉めよ を実践する為に、改造してみた。

-- CSVファイルからレコードのリストを読み込む関数
readCSV :: String -> IO [Record]
readCSV file = do
  handle <- openFile file ReadMode
  contents <- hGetContents handle -- ファイルの内容を読み込む
  let lines = filter (not . null) $ splitOn "\n" contents -- 改行で分割して空行を除く
  hClose handle
  return $ map parseLine lines -- 各行をパースしてレコードに変換する

いざ、実行。

ghci> :main 2308
2308> 0104 120 60 60
2308> fin
 *** Exception: test.csv: hGetContents: illegal operation (delayed read on closed handle)

遅延読み込みを、クローズされたハンドルに対して行なったと、例外をあげて きた。調べてみると、haskellは必要になるまで、実行を遅延するのが常識ら しい。普通の人からは非常識な動作だ。

このような実行戦略により、無限リストを簡単に扱えたりする。

ghci> take 5 [1..]
[1,2,3,4,5]

1から始まる無限リストの最初の5つを取り出せという指令。 普通なら、最初に無限リストを生成して、ってなって、普通の言語ではここで 破綻する事になる。

が、haskellの場合は、必要になった分だけ、実行がおこなわれる。5つだけ 無限リストから、取ってこい。取りおわったら、そこで終了。無駄な事はしな い戦略。

評価戦略 こちらに実行戦略の説明が出てたぞ。色々な事に気を使わないとい けないってのが大変だ。まあ、その分面白いんだけどね。

対策方法 その壱

import System.Posix.Files(rename)

inputLoop
    :
    then do -- CSVファイルとマージして同名なファイル名で保存する
      oldRecords <- readCSV fileName -- 既存のレコードを読み込む
      let newRecords = mergeRecords oldRecords records -- マージする
      writeCSV ("new-" ++ fileName) newRecords -- 書き込む
      rename fileName ("bu-" ++ fileName)
      rename ("new-" ++ fileName) fileName
      putStrLn "CSVファイルに追記しました"

unixパッケージにある、renameを使って、元のファイルを改名(こればバック アップ・ファイルになる)する。新規に作成したファイルも改名。ちっとも haskellチックじゃなくて、unixのオペレーションそのものだ。あれ、Windows 屋さんは、どうするんだろう?

対策方法 その弐

本来の要求が、CSVファイルに追記しろなのに、余計な事をしている。だから、 自爆しちゃうんだな。少しは、haskellの余計な事はしない精神を見習えよ。 と、言う事で、素直に実装。

inputLoop
    :
    then do -- CSVファイルとマージして同名なファイル名で保存する
      appendCSV fileName records -- 書き込む
      putStrLn "CSVファイルに追記しました"

-- レコードのリストをCSVファイルにappend
appendCSV :: String -> [Record] -> IO ()
appendCSV file records = do
  handle <- openFile file AppendMode -- ファイルをappendModeで開く
  mapM_ (writeLine handle) records -- 各レコードを一行ずつ書き込む
  hClose handle -- ファイルを閉じる

追記は、appendModeを使えばいいんで、writeCSVのファイルを開くモードを変 更しただけの関数を定義。追記するデータはコンソールから入力されたデータ に変更しただけだ。

追記で失敗するのは、Diskに余裕が無い場合ぐらいしか考えられないので、バッ クアップは特に考えなくてもいいかな(本当は、いかんなのだろうけど)。

more bug

ghci> :main 2308
2308> 0521 135 70 55
2308> 0521 140 77 66
2308> 0605 145 80 50
2308> fin
 *** Exception: 日時が重複しています
CallStack (from HasCallStack):
  error, called at app/Main.hs:99:11 in main:Main

書き込みの段階になって、文句を垂れるのは、正直、よしこ ちゃんです。っ ていう、昭和のギャグを残して、とりあえずBing編は、これにて打ち止めにし とくかな。

まとめ

Bingは、なかなかいいんだけど、致命的なのは、ずっとセッション出来無い事。 過去の記憶が、まだら模様になるんで、プロンプトに余計な考慮が必要なのが 欠点かな。

スポットで、さっと質問するんなら、ChatGPTみたいに、login出来無い事もな く、便利だと思うよ。


This year's Index

Home