Parsec with ChatGPT

Table of Contents

parsec

Parsec 入門 good

Parsec の実例: HTMangl

parsec で極める文章編集 good

haskell parsec個人メモ

意味がやっと分った。

ParsecT s u m a
s : stream type (String, ByteString, Text のどれか)
u : user state type (ユーザが使用できる状態の型、状態を使わないときは () を指定する)
m : underlying monad (StateT 複合モナドの内部のモナド、型指定に Parsec s u a を使うときは Identity モナドに指定されている)
a : return type (return 関数で戻されるデータの型) 

gen by ChatGPT

haskell parsecで、16進数8文字の連続を検出するプログラムって指示。 こんなのを説明付きで提示してきた。8文字の検出関数を作成します。それを 使用する関数を用意します。最後に、mainに組み込みます。

module Main where

import Text.Parsec
import Text.Parsec.String (Parser)
import Text.Parsec (parse)

hexParser :: Parser String
hexParser = count 8 hexDigit

detectHex64 :: String -> Either ParseError String
detectHex64 input = parse hexParser "" input

main :: IO ()
main = do
  let input = "12345678Hello"
  case detectHex64 input of
    Left err -> putStrLn $ "Error: " ++ show err
    Right res -> putStrLn $ "Detected hex string: " ++ res

実行例。

λ>  :main
Detected hex string: 12345678
λ>  :main
Error: (line 1, column 5):
unexpected "X"
expecting hexadecimal digit

5文字目にXを追加したら、それを検出してくれた。

sed ?

マッチした文字列を、別な文字列に置き換える方法をコミュニティーの方に教 えて頂いた。自分では、調べきれなかっただろう。

import Control.Applicative
import Text.Parsec.Char
import Control.Monad

hexParser :: Parser String
hexParser = "SHA256" <$ (replicateM_ 8 hexDigit)

これだけで了解してしまっては、飢えている人に魚を恵んでもらうの構図なの で、漁法を探索してみる。

λ> :i replicateM_
replicateM_ :: Applicative m => Int -> m a -> m ()
        -- Defined in ‘Control.Monad’
λ> :i ((<$)
type Functor :: (* -> *) -> Constraint
class Functor f where
  ...
  (<$) :: a -> f b -> f a
        -- Defined in ‘GHC.Base’       ^
infixl 4 <$

この、<$ の用例は、RWHの409ページにさりげなく出てた。右側の解析器が成 功したら、左側の値を使います、ですって。見逃してしまうよな。もう忘れる 事は無いぞ。

また、全体にマッチさせるには、上記だけでは難しいので、下記の物を勧めら れた。

https://hackage.haskell.org/package/replace-attoparsec

https://hackage.haskell.org/package/replace-megaparsec

16進数の連続検出に失敗したら、普通に1文字を検出すればいいんでないかい。 そんな考えで色々やったけど、型の壁に阻まれてしまった。基本に還って型確 認。刑事がおこなう行確です。

型宣言を外して、自由に うたってもらう。自供させよって訳。

λ>  :t hexParser
hexParser :: Parser String
λ>  :t hexParser
hexParser :: ParsecT String u Data.Functor.Identity.Identity [Char]

辻褄合わせで、こんなのにした。本当は anyChar にしたかったんだけどね。

hexParser :: Parser String
hexParser = try (count 4 hexDigit) <|> string "BAD"

で、実験すると、

λ> parse hexParser "" "12x3456"
Left (line 1, column 3):
unexpected "x"
expecting hexadecimal digit

tryのブロックの更に奥で違反してると言われた。根本的に駄目な方法だな。

ask ChatGPT

ニュートン誌の特集に取り上げられていた。コード作成の正解率は50%ぐらい と低い。ただ、それでもアイデアを貰ってくるには十分だろう。検証は簡単だ から、余り気にする事は無いだろう。

そんな訳なんで、手詰りなってるのを聞いてみる事にする。 人間が使う言語は、自然言語、それに対してコンピュータが扱う言語の事を、 形式言語と言うそうだ。

形式言語だと、あいまいさが無い。だから、形式言語を与えて、それを haskell語に変換させるのが良いだろう。勿論、お願いは自然言語を使うけど ね。形式言語は、sedにしとく。perl,ruby,pythonとかの祖だからね。

ちょっと、sedで実験。

[sakae@deb ~]$ echo 'python is easy, so I like python.' | sed -e 's/python/haskell/'
haskell is easy, so I like python.
[sakae@deb ~]$ echo 'python is easy, so I like python.' | sed -e 's/python/haskell/g'
haskell is easy, so I like haskell.

gを付けないと、最初に出会った単語しか置換しない。gを付けると、手当たり 次第に痴漢する。おっと、ポルノみたいだな。

んな事で、下記が依頼文。regexを使わないように、モジュールを指示するの が味噌かな。

sed -e 's/old/new/g' と同等な機能をhaskellのparsecモジュールで実現して
import Text.Parsec

patternParser :: Parsec String () (String, String)
patternParser = do
  _ <- char 's'
  _ <- char '/'
  old <- many1 (noneOf "/")
  _ <- char '/'
  new <- many1 (noneOf "/")
  _ <- char '/'
  _ <- many (oneOf "g")
  return (old, new)

parsePattern :: String -> Either ParseError (String, String)
parsePattern = parse patternParser ""

replacePattern :: String -> String -> String -> String
replacePattern old new = concatMap replace
  where
    replace [] = []
    replace str@(c:cs)
      | isPrefixOf old str = new ++ replace (drop (length old) str)
      | otherwise = c : replace cs

main :: IO ()
main = do
  let input = "This is some old text."
      patternStr = "s/old/new/g"

  case parsePattern patternStr of
    Left err -> putStrLn $ "Parse error: " ++ show err
    Right (old, new) ->
      putStrLn $ replacePattern old new input

おまけの説明も出てきた。megaparsecって識者の方も推していたな。

なお、parsecモジュールの他にも、attoparsecやmegaparsecなどのパーサーコンビネータ
ライブラリも利用できます。具体的な実装方法は異なるかもしれませんが、基本的なアイデアは
同じです。

コンパイルしてみると、こんな2つのエラーが報告された。奴はGHCを持ってい ないからしょうがない。エィヤーとでっちあげて提示するからね。

app/Main.hs:20:36: error:
    • Couldn't match type ‘[Char]’ with ‘Char’
      Expected: Char -> [Char]
        Actual: [Char] -> [Char]
    :
app/Main.hs:24:9: error:
    Variable not in scope: isPrefixOf :: String -> [Char] -> Bool
    :

ざっと見、無理してParsecを使ってみました感がするぞ。sedのコマンドをパー スするのに無理して使っている。肝心な部分は、replacePatternだ。

isPrefixOfは、import Data.List(isPrefixOf) で、導入。それから、 replacePattern内のconcatMapは不要なんで削除。main内の入力文字列と置換 対象を変更してから実行。

λ>  :main
Haskell is easy, so I like Haskell.

労力の削減になるな。折角なんで、ありがちなエラーを発生させてみる。

  let input = "python is easy, so I like python."
      patternStr = "s/python/Haskell"               -- need /

λ>  :main
Parse error: (line 1, column 17):
unexpected end of input
expecting "/"

パターンの最後のgは飾りの扱い。手当たり次第の置換器になってる。

念の為、hlintの意見も聞いてみた。

app/Main.hs:24:9-26: Suggestion: Use infix
Found:
  isPrefixOf old str
Perhaps:
  old `isPrefixOf` str

ふーん、kaskell業界は、これが主流なのか。

コンパイルエラーとか、こういう習慣を奴にフィードバックすべきだろうか? 日本人なら、一宿一飯の義理があるから、返礼すべきだろね。 それが巡り巡って皆の幸福につながる? より賢くなるけど、ユーザーの機密 情報が漏れる可能性がある。セキュリティー に注意って言われる訳だ。前任では、gmailが無料な事を思いだせ、だな。

多大なフィードバックが欲しい。そうじゃ なければ、無料で公開するはずはない。販促目的だけなら、機能の制限とか、 利用期間の限定だけでいいからね。 奴がどんな返答するか、聞いてみればいいのか。

はい、フィードバックをお願いします。コンパイルエラーの内容と、Haskellの良き習慣について
教えてください。修正や改善の方法についてアドバイスを提供します。

どうしてもマウントを取りたいのだな。人間みたいな奴だ。こういう会話は楽 しい。一人でプログラミングするより、ペアプロみたいで知見が広がる。 ニュートン誌の第二特集は、70才以上の取扱説明書だった。それによると、孤 独は心身共に非常にダメージが有るそうだ。

ちょっと生意気な孫と、ペアプロを楽しむ、おじいちゃんの構図だな。AIの一 人者である松尾先生も認めているみたいだしね。

find <count 64 hexDigit>

いよいよ本命を問い合わせ。

文字列の中から、連続する16進数文字64個を検出して、それを"sha"に置き換える(それ以外は、そのまま)コードを作れ。
haskellのparsecモジュールを使用する事。

最初にこんなコードを提示してきた。

import Text.Parsec
import Text.Parsec.String (Parser)

-- パーサー: 16進数文字の64個の連続を検出し、それを"sha"に置き換える
parser :: Parser String
parser = do
  hexChars <- count 64 hexDigit
  return $ if all isHexDigit hexChars
           then "sha"
           else hexChars

-- 文字列をパースして結果を取得
parseString :: String -> Either ParseError String
parseString = parse parser ""

-- テスト用例
testString :: String
testString = "This is a sample string with 64 hex characters: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"

main :: IO ()
main = case parseString testString of
  Left err -> print err
  Right result -> putStrLn result

Data.Charを追加するのは、お約束? それにしても、エラーってるぞ。

λ>  :main
(line 1, column 1):
unexpected "T"
expecting hexadecimal digit

このエラーを貼り付けて再度問い合わせ。おっしゃる通りって言ってきて、修 正コードが提示された。(この間、何度かのやりとり有り。オイラーはひたす ら、コピペ実行委員でしたよ。)

import Text.Parsec
import Text.Parsec.String (Parser)

parser :: Parser String
parser = do
  content <- many (try hexString <|> nonHexString)
  return (concat content)
  where
    hexString = do
      hexChars <- count 64 hexDigit
      return "sha"
    nonHexString = do
      nonHexChars <- many1 (noneOf ("0123456789ABCDEFabcdef" :: String))
      return nonHexChars

parseString :: String -> Either ParseError String
parseString = parse (parser <* eof) ""

testString :: String
testString = "This is a sample string with 64 hex characters: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"

main :: IO ()
main = case parseString testString of
  Left err -> print err
  Right result -> putStrLn result

こんなエラーになった。

λ> :main
(line 1, column 10):
unexpected " "
expecting hexadecimal digit

そして最後通告っぽい、泣き言が、、、

もしもまだエラーが発生する場合は、使用しているParsecライブラリのバージョンによって
動作が異なる可能性があります。Parsecのバージョンによっては、上記のコードが正常に動作
しないことがあります。その場合は、別のパーサーコンビネータライブラリ
(例:Megaparsec)を検討することをお勧めします。

上記のエラーを分析すると、"This is a " の a を発見して、その後に16進が 続くと期待したんだな。が、それを裏切られたので、文句を言ったんだな。

ちょっと全体のコードを眺めると、

parseString = parse (parser <* eof) ""

が気になった。これって、eof が成功するまで、左項(すなわち、parser)を実 行しろって事だな。C言語にある、 do { … } while の事だな。

gold data

最初から難しいデータの解析では、萎えるので、ゴールドデータを与えてみる。 パーサーも規模を縮小しておく。 's/hexDigit x 6/__/' 名前もptって具合に unixerっぽく短縮名です。

gold = "QRZ123456QTHabcdef"
tc  = parseTest (pt <* eof) gold

λ>  tc
"QRZ__QTH__"

面倒なのでコードとghciとのセッション(頭がλで始まる)を併記します。 parseTestはparseの簡易版。ちょいと実験に使うには、うってつけ。

なお、 Qで始まる3文字は、無線業界の隠語です。気にしないで下さい。

s/QRZ/QRA/ に変更すると、(往年のパソコン通信みただな)

λ>  tc
parse error at (line 1, column 10):
unexpected "Q"
expecting hexadecimal digit

A12345 を検出、続いて6は16進数だな。期待して次の文字にとりかかると、Q が出てきた。話が番うじゃん。

gold = "543210QRZ123456QTHabcdefQRO"

λ>  tc
"__QRZ__QTH__QRO"

QRO(出力増大)しても、クリーンなデータである限り、問題を撒き散らす事は 無さそうです。

tryの正体

問題は、count 6 hexDigit だな。これを tryで囲って、臭い物に蓋をしよう にも、効き目がない。tryは使えない子?

まずは、countだ。Conbinamtor.hsに存在した。

count :: (Stream s m t) => Int -> ParsecT s u m a -> ParsecT s u m [a]
{-# INLINABLE count #-}
count n p           | n <= 0    = return []
                    | otherwise = sequence (replicate n p)

定義は簡単だな。sequenceは良く知らない。何だかモナドを返すでいいのかな。

λ>  :t sequence
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)

これじゃ、ブルトーザーみたいに、16進数をパースしちゃう。これが問題の根 源。これを何とか解消しない限りだめだ。

Parsec コンビネータ count 詳しい説明を発見。

他に注目できる事がないか、tryを参照しとく。

λ>  :i try
try :: ParsecT s u m a -> ParsecT s u m a
        -- Defined in ‘Text.Parsec.Prim’
try :: ParsecT s u m a -> ParsecT s u m a
try p =
    ParsecT $ \s cok _ eok eerr ->
    unParser p s cok eerr eok eerr
newtype ParsecT s u m a
    = ParsecT {unParser :: forall b .
                 State s u
              -> (a -> State s u -> ParseError -> m b) -- consumed ok
              -> (ParseError -> m b)                   -- consumed err
              -> (a -> State s u -> ParseError -> m b) -- empty ok
              -> (ParseError -> m b)                   -- empty err
              -> m b
             }

チンプンカンプンであります。

Text.Parsec.Prim: cok, cerr, eok, eerr

etc