すごいHaskell 読書メモ #2

2015年10月7日水曜日

前回: すごいHaskell 読書メモ #1
前回は関数の記述と適用の話だった。if式にはelse節が必須など、いくつかのルールを学んだ。
今回からはデータ構造の話、今回はリスト。

リストの基本操作と性質

まずはリスト。定義の仕方や扱い方はざっくりと他の言語と変わらないように見える。GHCi内での定義にはletを使うとひっそりtipsが書かれてる。
リストの連結は++でおこなう。文字列は文字のリストとして表現されるので、リスト関数は文字列に対して適用できる。
リストの末尾に連結をおこなうのは何度もスキャンが必要となるため遅い。リストの先頭への追加はとても速い(スキャンしないから)。これをおこなう演算子が:cons演算子とも呼ばれる。
ここの例で'h'など文字(Cでいうところのchar相当)指定が出てる。前回の中身でアポストロフィは名前の一部に使えるというのがあったけど、この感じだと先頭には使えない(letter表現とかぶるため)のだろう。
cons演算子(:)で第一引数になるのは連結対象リストの単一要素。++演算子では両方ともリストを渡す。
Haskellでは[1,2,3]は単に1:2:3:[]の糖衣構文と。なるほど。
リストの要素へのアクセスは!!でおこなう。0-origin。リストの端を超えた場所にアクセスすると例外が投げられる。リストの中に更にリストを入れられるけれど、違う型はダメ。
リストの比較は辞書順でおこなわれる。最初に見つかった異なる要素の順序が大小を決定する。空でないリストは常に空リストよりも大きいとみなされる。なので、リストAがリストBのスーパーセットである場合はリストAのほうが大きいとみなされる。ふむ。
その他のリスト操作としてhead/tail/last/initの紹介。結構頻繁に使いそう。
空のリストに対してこれらの操作をすると例外を吐く。length関数で要素数を調べたり、null関数で空か否かを調べたりして空リストへの操作が発生しないように処理すると。
take関数は要素数を指定して取り出す。0を指定して空リスト取り出しができる。試した感じ、take 0 []は問題ない。リスト要素数より多い要素を取り出そうとするとリスト全体が返される。だからtake 3 []は空リストを返すだけで特に問題ない。
maximumminimum関数は想像通りの挙動。elemは要素とリストを受け取って、指定要素がリストに含まれるかを返すもの。中置関数として使うことが多いという説明がされてる。
レンジ指定はまあまあ賢いよという話。[1..20]は自然数を刻んでくれるし、[a..z]は小文字アルファベットを列挙してくれる。
しかし[1.1..20.0]を試してみると
[1.1,2.1,3.1,4.1,5.1,6.1,7.1,8.1,9.1,10.1,11.1,12.1,13.1,14.1,15.1,16.1,17.1,18.1,19.1,20.1]
となったので、まあこんなもんかなと。
2つ目の要素を指定するとその差分を刻みとしてほどよくやってくれると。試しに[1,3..]とすると噂の無限リストがさっくり作れた。割と便利さを感じる。と思ったらすぐ後にそのまま無限リストの話が出てきた。
cycle関数は指定リストを無限にリピートする。おもしろい。repeatは単一要素を受け取ってそれをひたすら繰り返す(yesコマンドぽい)。replicateは指定要素からなる指定長のリストを作るもの。結果だけだとreplicate 3 10take 3(repeat 10)は同じになるけど、内部の処理的にはどっちのほうが効率的なのだろう、というのが気になったりした。
続けてリスト内包表記の話。Pythonなどでお馴染みという感じだけど、あるリストから要素列を取り出して指定の名前に束縛し、加工方法を記載する出力パートに投げ込んで結果リストを作っておしまい。
要素列の取り出しと束縛をおこなうパートにカンマで続けて条件(述語とも呼ぶ)をつけ、元リストからの取り出しをフィルタできると。条件はカンマで区切っていくつも書けるので、複数条件指定のためにわざわざリスト作成を何重にもおこなう必要はない。
複数のリストからの取り出しも構文的には条件と同様なんだけど、利用時をイメージするとイマイチ直感的ではなかった。取り出した要素の全組み合わせが結果リストに反映される(5要素のxと3要素のyなどを取り出すと15要素の結果リストができる)。慣れると便利な局面が多そう。
lengthっぽい関数を独自定義するくだりはゴリ押しすぎて面白かった。まさかの全要素を1に射影してsum*1
リストから取り出したけど使わない値を変数名_に突っ込んで捨てるのはお馴染みのイディオム。
試してみると複数のリストからの取り出しで全部同じ変数へ突っ込むこともできるんだけど、その場合実際に取り出せるのは最後に書いたものらしい。
ghci> [x | x <- [1,2,3], x <- [10,100]]
[10,100,10,100,10,100]
ふむ。どっかに仕様書いてあるだろうからそのうち調べそうなメモ。
リストを含むリストの操作で例示されている[ [ x | x <- xs, even x ] | xs <- xxs ]は慣れるとサクサク読み書きできそうだけど、あまり複雑になりすぎると脳スタックがあふれそうな気もした。外枠から把握していくとルール付ければ破綻はしないか。
と思ったら、入れ子の内包表記は複数行で書いたほうが読みやすいよと注記されていた。誰かのコードを読むなかで慣れよう。
[*1] C#+LINQやRxの文脈でも似たことをやるコードは見かけますが、たとえば極単純な配列長チェックに使う場合は確実に無駄ですね。特定条件でフィルタした結果の件数取得などでは合理的な可能性あります。

おしまい

リストについてはここまで。写経しつつメモを書きながら読み進めたところ、分量的にはあまり進まなかった。各関数の挙動を実際に試しつつなので、まあこんなものかなとも思う。
確実に「おっけー!」という部分がまだほとんどないので、出てきたものを片っ端からメモ取る状態になってるのが辛いところ。この先改善していくと良いけれど、あまり改善しないようなら何かしら方法を考える必要がある。
ふと思い立ってlast (reverse [1..])と叩いてみたけど、結果がかえってくる気配がなかったから^Cで止めた。こういうことは出来ないんだなぁ。

次回

次回はタプルから。

0 件のコメント:

コメントを投稿