🤖

paizaの問題集をHaskellで解く

2025/01/04に公開

これまで何度かHaskellの記事を書いていますが、筆者自身はHaskellを勉強したてのひよっこです

さて、今までIOモナドの理解に注力してきましたが、せっかくなので易しめな実践問題を一つ解いてみましょう
今回はpaizaラーニングの問題集より、Cランク問題に挑戦します
Haskellを用いた解説がまだ存在しない問題なので、題材としては最適でしょう

構造体の更新

https://paiza.jp/works/mondai/class_primer/class_primer__change/edit?language_uid=haskell&t=38dc9e65dd376a8268fa69e41f31e303

この問題の目的はインスタンスのメンバ値を書き換えることです
struct Userは次のメンバを持ちます

User{
    nickname : 名前
    old : 年齢
    birth : 誕生日
    state : 出身地
}

この内、nickname:名前が操作対象となるメンバです

前提

Haskellは全てのインスタンスが定数を持ちます
変数は戻り値のみの関数に置き換えられ、値を書き換えることはできません
ある変数の値を上書きすることは、副作用の一種に数えられます
このような制約下で構造体のメンバ値を書き換える場合は、インスタンスを丸ごと作り直し、それを関数の戻り値とします
値を操作できないHaskellで、それをどう表現するかの思索を体験できる点が、この問題の利点と言えるでしょう
まず、以下に回答を示します

回答
import Data.List.Split
import Data.List

data User=NewUser {
 nicName::String,
 old::String,
 birth::String,
 state::String
} deriving Show

--3行目以降指定行までのユーザーデータをUser配列に格納する再帰
addUser::Int->[User]->IO [User]
addUser count user=if count>0 then getLine>>=
 \ x->return ((\ x->user++[NewUser (x!!0) (x!!1) (x!!2) (x!!3)]) (splitOn " " x))>>=
  \ x->addUser (count-1) x
 else return user

--指定行のユーザー名を書き換える再帰
changeName::Int->[User]->IO [User]
changeName count names=if count>0 then getLine>>=
 \ x->(\ x->return (x!!0,x!!1)) (splitOn " " x)>>=
  \ (a,b)->return ((\ a->(makeList (a-1) ((length names)-1)
           (NewUser b
            (old (names!!(a-1)))
            (birth (names!!(a-1)))
            (state (names!!(a-1))))
           names [])) (read a::Int))>>=
     \ y->changeName (count-1) y
 else return names

--ユーザー名を変更したstruct Userを含む新しい配列を生成する再帰
makeList::Int->Int->User->[User]->[User]->[User]
makeList index count user users newList=if count>=0
 then makeList index (count-1) user users
      ((\ x->if x==index then [user]++newList else [(users!!x)]++newList) count)
 else newList

--struct Userからデータを取り出し、標準出力へ表示する再帰
printData::[User]->IO ()
printData users=if null users then return ()
 else return (head users)>>= 
       \ (NewUser a b c d)->return (a++" "++b++" "++c++" "++d)>>=
        putStrLn>>return (tail users)>>=printData

main = getLine>>= \ x->
 (\ x->return ((read (x!!0)::Int),(read (x!!1)::Int))) (splitOn " " x)>>=
 \ (a,b)->addUser a []>>=changeName b>>=printData

整数変換

このプログラムは全体を通して反復再帰で構成されます
入力から与えられる数値は、要素数と要素番号の指定に使われるので、まずは整数へ変換する手続きを構築します
IO StringからIntへのもっとも単純な変換は、クロージャを連結させることです

main=(\ x->read x::Int)<$>getLine>>=print
main = getLine>>= \ x->
 (\ x->return
  ((read (x!!0)::Int),(read (x!!1)::Int)))
  (splitOn " " x)

(<$>)(>>=)の抽象系です
本来\ x->return (read x::Int)と記述すべき場面でreturnを内部的にラップし、一般的な式構成に昇華させます
普遍的な戻り値を要する時は、直下に示す通り内部関数を定義します
仮引数を明示することで、一般的な関数と同様に振る舞います

再帰実装

配列の読み込みは常に反復式です
完全に同一のオブジェクトを一から再現し、その内容に指定の変更を加えます
構造体と同様、そのインスタンスは常に完全な複製である点を考慮します
命令型のスタンスで値を直接的に破壊することはありません
再帰と遅延評価の最適化は、配列生成のオーバーヘッドを軽減します
その実態は、同一オブジェクトへの参照あるいはポインタの切り替えによって柔軟に調整されるコレクションの階層構造です

--指定行のユーザー名を書き換える再帰
changeName::Int->[User]->IO [User]
changeName count names=if count>0 then getLine>>=
 \ x->(\ x->return (x!!0,x!!1)) (splitOn " " x)>>=
  \ (a,b)->return ((\ a->(makeList (a-1) ((length names)-1)
           (NewUser b
            (old (names!!(a-1)))
            (birth (names!!(a-1)))
            (state (names!!(a-1))))
           names [])) (read a::Int))>>=
     \ y->changeName (count-1) y
 else return names

タプルはとても強力なデータ構造です
オブジェクトの状態を破壊することなく、パターンマッチとの組み合わせで柔軟に値の出し入れが可能です
ここで、要素番号として扱う整数の振る舞いに注目します
(read a::Int))は実引数部で、getLine>>= \ x->(\ x->return (x!!0,x!!1)) (splitOn " " x)により標準入力から与えられる整数と要素の一方です
このように、Haskellの巧みな条件分岐機構は、関数に多彩な表現力をもたらします
例えば条件分岐をネストさせる場合も、関数として整理すれば簡素に分離を計れます

--ユーザー名を変更したstruct Userを含む新しい配列を生成する再帰
makeList::Int->Int->User->[User]->[User]->[User]
makeList index count user users newList=
 if count>=0
  then makeList index (count-1) user users
      ((\ x->if x==index
        then [user]++newList
        else [(users!!x)]++newList) count)
  else newList

内部関数を適宜構成することは、ロジックのフローを適切に可視化する意味で有用です
時にdo式で隠蔽されがちな副作用の変換フローが明確になることで、関数同士の相互作用が具体化し、プログラムを鮮明に把握する助けとなります

まとめ

Haskellといえばそのコードはdo式で示されることがほとんどですが、このように式や関数とその引数で、手続きは多様に記述できます
今回はCランクの少し本格的な問題を題材としましたが、また気が向けば他の問題も解説してみたいと思います

Discussion