2025年8月21日木曜日

Haskell入門 P347 ユーザー登録のためのINSERT

 仕組みがわかりにくかった。

そこで、次のような質問をChatGPTにしてみた。
Q1:piNewUserは、どういうふうに使い方をするのですか?
Ans: let ins = HRR.typedInsert User.tableOfUser piNewUser がポイント
typedInsert :: Table User -> Pi User NewUser -> Insert NewUser
第1引数 User.tableOfUser → 「Userテーブルそのもの」
第2引数 piNewUser → 「NewUser から User のカラムに対応付ける射影」

let ins = HRR.typedInsert User.tableOfUser piNewUser
    u'  = u { nuPassword = dec hashed }                ins は INSERT 文のひな型

DHR.runInsert conn' ins u'     と実行すると、
u' :: NewUser の値(ユーザー名とハッシュ済パスワード)を
piNewUser に従って User テーブルの name, password カラムに対応付け
実際に DB へ INSERT

Q2: 要するに、手順を最初にくみたてて、最後に、runInsertで実行するのですね。
Ans:  はい、その理解でバッチリです!
  まとめると:
   piNewUser で「どう射影するか」を定義(テーブルと型の橋渡し)。
   typedInsert で「INSERT文のひな型」を作成。
   runInsert で「実際の値」を流し込んでDBに送信。

※ Haskellは、手続き型のように「値を受け取りながら逐次SQL発行」というやり方はあまりせず、まず「設計図を型安全に作る値を流し込む」という 宣言的な流れ
 型安全性が高い:SQL文やカラム名の間違いをコンパイル時に検出できる
 SQLインジェクション対策が組み込みである:値を直接文字列連結しないので安全
 トランザクション管理が明確:withTransaction の中で設計図に値を流し込むので、途中でエラーがあればロールバック可能
 コードの再利用性が高い:射影(piNewUser のような定義)を作っておくと、INSERTや  SELECTで同じマッピングを使い回せる
 などの利点があるらしい
Q3:makeRecordPersistableDefault ''NewUser はどういう意味?
Ans:NewUser を Relational-Record の「DBに入出力できる型」として登録する宣言、Template Haskell によって必要な型クラスインスタンスを自動生成している
「この型をSQLに流せるようにしますよ!」と最初に宣言しておく儀式 みたいなもの
Q4:Template Haskellとは?(P340にも説明はありますが)
Ans:「Haskellプログラム自身を生成する Haskellプログラム」を書ける
Q5:自動生成ですね。DjangoやRuby on Railsなどでも、DB初期化で似たような処理あったような気がしますね.
Ans:まさに DjangoRuby on Rails と同じ発想です。
Rails/Django = 実行時に自動生成・マッピング
Haskell(TH) = コンパイル時に自動生成・型安全にマッピング
Q6:user <- DHR.runQuery conn q name >>= DHR.listToUniqueのDHR.listToUniqueの働きは?
Ans:[] の場合 → Nothing を返す、[x] の場合 → Just x を返す、[x, y, ...] の場合 → エラーか Nothing を返す。「DB側にユニーク制約があるとは限らない」設計を想定。
Q7:checkHash user | validated = Just user | otherwise = Nothingの意味は?
Ans: if ... else if ... else if ... elseのイメージで、最初にtrueになったところが checkHash userの値。ガードの機能(p62で既習)

2025年8月18日月曜日

Haskell入門 P326 処理のまとめ

stack install で実行ファイルを作り、動作確認してみた。これも、一筋縄ではいかず、Windowsだと、文字エンコードなど配慮しないとうまくいかなかった。エラーメッセージ見たり、ChatGPTに聞いたりしながら、なんとか解決。
 
echo [{"age":"aa","name":"名前","telnumber":"abcdef"},"age":"bb","name":"abcd","telnumber":"jkl"}] > test.json
としてファイルをつくってから
hjq-exe "{\"name\":.[1].name,\"telnumber\":.[1].telnumber}" test.json
としたらうまくいった。ハイフンがあるとうまくいかず、カットする必要あった。なかなか、大変だった。


Hjq.hs
・note:MaybeをEitherに変換    decode:ByteStringをValueに変換
 first:Rightはそのまま、Leftに関数を適用


Haskell入門 P323 テストようやく終了

 本のままでは、動かず新しいバージョンに合わせた修正が必要だった。

よくわからないところを確認してみた。
Spec.hs
・applyFilterTestについて
   ^?  指定データを1部取り出したいとき使用(lens)
 nth 2 リストの3番目要素取り出し
   fmap Right (testData^?key "string-field") だと  fmap::(a->b)->f a->f bでみると
 (a->b)がRight  f aがMaybe Value   f bがJustつまりMaybe(Either e Value)
 これにRightが適用なるようだ

Parser.hs
・queryArrayの jqQueryParser `sepBy` schar ',' について
  sepByは中値だが、2引数関数で schar ','を区切りとして jqQueryParserでパース

Query.hs
・applyFilter (JqField fieldName n) obj@(Object _)
  = join $ noteNotFoundError (T.pack fieldName) (fmap (applyFilter n) (obj ^? key (fromText(T.pack fieldName))))  について
   obj@(Object _)はObjectとか確認し、その値をobjとして利用可能
 joinはMaybeをはずすため

・executeQuery (JqQueryObject o) v
   = fmap (Object . KM.fromList . fmap (\(k,v) -> (Key.fromText k, v)))
    . sequence ②
    . fmap sequence ①
    $ fmap (fmap $ flip executeQuery v) o  について        
    左辺の(JqQueryObject o) vは{"name":.name,"age":.age}::[(Text,JqQuery)]
    右辺の fmap $ flip executeQuery v のflipは引数を逆順にして、部分適用を意図した 通りにするために使う。(flip executeQuery v) q == executeQuery q v 
  fmap (fmap $ flip executeQuery v) は、内側のfmapで( , )への処理を、外側のfmapで上の関数使って[  , , ]への処理を行う。
  sequenceは①で Eitherを外に出していく [(either Text (Text,Value))]
        ②で Ehter Text [(Text,Value)]にする

   パーシングは、なかなか複雑

2025年8月16日土曜日

Haskell入門 P318

 スペースがある場合のパース

Spec.hs

Parser.hs
本ではscharが標準である?のかもしれないが、新しいライブラリにはないようで、作る必要があった。ChatGPTから作ってもらう。ついでに、以下のような説明もしてもらうが、 Megaparsec自体なじみがないので、難しい感じが。理解できるようになるためには、もう少し時間がかかりそう。
1. sc :: Parser ()
sc = L.space space1 empty empty
目的:空白やコメントをまとめてスキップする
L.space は Megaparsec が提供する空白スキップ用パーサ。
引数:
space1 → 1個以上の空白文字(' ', '\t', '\n')をスキップ
empty → 行コメントの始まり(今回は使わないので空)
empty → ブロックコメントの始まり(今回は使わないので空)
つまり、この sc は「空白があるところを自由に無視する」パーサです。

2. lexeme :: Parser a -> Parser a
lexeme = L.lexeme sc
目的:トークンを読んだあと、自動で空白をスキップする
使い方:
lexeme L.decimal    -- 数字を読み取ったあと、空白を消費
lexeme (some letterChar)  -- 識別子を読み取ったあと、空白を消費
イメージ:
入力: "123   "
lexeme L.decimal → 123 を返す、空白も消費ない場合は、後ろの空白が残り、パースが失敗しやすくなります。


3. symbol :: String -> Parser String
symbol = L.symbol sc
目的:記号(キーワードや演算子)を読んだあと空白をスキップ
例:
symbol "."  -- ドットを読み取り、空白も消費     symbol "["  -- 左ブラケットを読み取り、空白も消費      内部的には lexeme (string s) と同じような働きです。


4. schar :: Char -> Parser Char
schar c = lexeme (char c)
目的:単一文字のトークンを読み取り、空白をスキップ
文字 . や [ ] の後ろに空白があっても問題なくパースできる
例:
schar '.'  -- "." と後ろの空白をまとめて消費      schar '['  -- "[" と後ろの空白をまとめて消費


Haskell入門 P316 修正コード

 P316のコードが動作しなかったので、ChatGPTから同等のものに書き直してもらったのがこちら。新しいライブラリMegaparsecを使うといいらしい。

Parser.hs

・type Parser =Parsec Void String     :Voidがエラーの型(ない) Stringが入力の型
   Parser   a     =   Parsec Void String  a
 限られた使い方 aが結果     Parsecのほうがカスタマイズしやすいといえる
・try paraseIndex <|>   try parseField  <|> pure JqNil    で<|>は左が失敗したら右ためす
   tryは 失敗しても入力を消費せず次を代わりにためす (入力を巻き戻す)
・some letterChar     :  some 1回以上繰り返し  letterChar a~zA~Zのパース
・option JqNil  (char '.' *> filterRest <|>  parseField)
      option::  a -> Parser a -> Parser a    
         ディフォルト値->試すパーサ->失敗したら ディフォルトaを返す
      char '.' *> filterRest    :  .を消費し、filterRest返す
       ここではtry不要。すでにfilterRestの中でtryは終えているので

Haskell入門 P316

 このへんのパーサ扱ったところはかなりややこしくなっている。
結局、コードが古くてうまく動作せず、別のライブラリを使うことになった。

でも、いちおう、古いほうも、コードをいろいろ確認してみた。
・showParserResult $ parse (jqFilterParser <* endOfInput) s `feed` ""は
showParserResult $ (parse (jqFilterParser <* endOfInput) s) `feed` ""と同じ意味
<* endOfInput 該当しないところはパースせず `feed` ""はパース停止の決まり文句のようなものらしい

・fmap pack $ many1 (letter <|> char '-' <|>..... digit())について
many1 は  letter <|> char '-' <|> ... <|> digit は 1文字分のパーサー
many1 (...) はこれを 1回以上繰り返して成功した結果をリスト [Char] にまとめる
fmap pack によって [Char]Text に変換  
        letter :: Parser Char なので   many1 letter :: Parser [Char] になります。
箱(Parser)はそのままで 中身 [Char] が Text に変わる

・JqIndex <$> パース1 <*> バース2 の意味について復習
<$>で最初に部分適用の関数ができて、その関数のまだ未適用な部分に<*>を順次適用していくしくみ
 パーサの場合、関数がつぎつぎと、無限に適用できる引数があるようなもの。その場合は、つねに、部分適用な関数のままでJqIndexが残ることになるといえそう。再帰状態になるのかも。

2025年8月13日水曜日

Haskell入門 P315 パーサの作成

 本のコードのままではだめで、けっこう厳密さが要求された。バージョンの違いか?

・型のスコープとインポートは明示的に
・Text と String は別物    (T.pack ".")で Textに
・コンストラクタもエクスポートリストで見えるようにする     (..)など利用
・stack.yaml / package.yaml の依存関係はビルド対象ごとに確認(ライブラリも指定)
といった注意も必要なことがわかる。

ちなみに修正したものは
Spec.hs

Parser.hs

Hjq.hs

package.yaml