外部記憶

他人に取って価値がないが、自分が忘れることに忍びないため残したい記憶・記録を記す外部記憶装置

読書感想 「すごいHaskellたのしく学ぼう!」

たしか3回目のトライ。

遅延評価

基本的には嬉しいんだけど、計算コストを先払いしたいときは困ることも。 特にマルチコアCPUではコアが遊んでいるケースが多々あるので。

とてつもなく退屈なHaskellの関数succを呼び出してみます。

代数学では非常に重要なインクリメントなのに・・・

正格(遅延じゃない)版の関数

haskellは文字列を文字のリストとして扱ってる

UTF-8扱うとき困りそう。

イラストのハナちゃんに草

今気づいたけどhead []"abc" !! 10などでHaskellでも例外出るんだなあ

これらは、いわば多相定数です。

ああ、なるほど。引数を受けずに型引数のみで1つの値のみを返すから実質定数ってことか。

しかしHaskellには、データ宣言には決して型クラス制約を付けないという、とても強いコーディング規約があります。

まじで!?絶対欲しくなる気がするけど、廃止方向というのはやってみたけど駄目だったということか。 どういう背景なのだろう・・・。

あと読んでなるほど、data宣言だけよねって納得した。 でも、データの性質を表す意味でついていても良い気もする。

このdata宣言に型クラス制約を付けないのは読んでて初めてそれで良いのか違和感を持った。 これだとその後出てくるVectorなどへStringやCharも入れて初期化そのものはできてしまうわけで、違和感が激しい。 まあ初期化時にコンパイルエラーを検出しなくてもvplusなど演算呼び出しのときに静的にコンパイルエラーになるから本質的には同じことか。

Scala的にはコンストラクタの型クラス引数にするか、メソッドの型クラス引数にするかに相当すると思うけど、Scalaではどっちも書かれている気がする。というか前者を取りたいことが多い。なぜかというとScalaの場合は型クラスがimplicitオブジェクトで表現されているため、メソッドの型クラス引数にするとimplicitのimportを利用する個所すべてで引き回さないと駄目で煩雑に感じるから。

型シノニムについて

haskellでもシノニムはあくまで表記・記述上の別名であり、シノニムあり・なしの型間で相互に代入可能。 これScalaのtypeもそうだけど違和感あるんだよなあ。 まあhaskellであればcase classよりさらに型コンストラクタが簡単に・自然に使えるのでそちらを使えばいいんだろうけど。

結合性宣言

面白い、Haskellでは演算子の優先順位をコードで自由に設定できるのか! これ構文解析のときに難しそうだけど2回コンパイルしてるのかなあ。

最小完全定義

scalaでもthin implement thick interfaceみたいな用語で同様のことが言われていた気がする。

サブクラス宣言

これがOKでdata宣言の型クラス制約がNGなのはなんか納得できない。 たしかに説明の通りdata宣言の型クラス制約はそこに書いても関数宣言のところでも宣言が必要だからあんまり意味なし、サブクラス宣言はwhereの中なので再度書く必要がないからOKっていうのは一貫しているが、単なる記述上の理由だけ?

型は値のラベルであり、種類は方のラベルである、という対応関係があるわけです。

値のメタが型であるなら、種類は型のメタである。

IOアクションは侵食的である。これはモナドであるため?

他の言語と異なり、HaskellのreturnにはI/Oのdoブロックの実行を終わらせる働きはありません。

他の言語に慣れているとここは間違えやすいところ。returnじゃなくて別の用語にしてくれればよかったのにとも思う。

コイントスの辺り読んでいて、ああ、Haskellは逆方向の2ホップ以上の推論が効くのかと思った。 Scalaではこれは今は無理だなあ。

最初はファンクター則なんて意味が分からないし、 不必要と思うかもしれません。 でも、 もしある型がファンクター則を両方とも満たすと分かれば、 その型の挙動についてある種の信頼がおけます。 fmapを使っても、 その関数でファンクター値の「中身」が写されるだけで、 それ以外のことは何も起こらないと確信できるのです。 この前提のおかげで、 より抽象的で応用の利くコードが書けます。 すべてのファンクターが満たしているはずの法則を根拠に、 どんなファンクターに適用しても間違いなく動作する関数を作れるからです。

なるほど。おそらく*や/は結合則を満たす、と同様のルール付けなんだろうと感じた。

newtype、これはこの前scalaで求めていたものっぽい。

newtypeはもっぱら既存の型をすでに定義されている型クラスの別のインスタンスにしたい場合に使う。

ほー。単純にStringの別名を与えて相互代入を禁止したい、などの目的のときはnewtypeでもdataでもいいのかなあ。

あー、今継承による機能追加と型クラスによる機能追加の差異を認識しつつある。 継承による機能追加は内発的で後から追加することができないのに対して型クラスによる追加は外挿的で後からの拡張が自由なんだな。 オブジェクト指向で同様のことをしようとすると継承したクラスを定義などとなるが、その場合元となったクラスのインスタンスは以前のまま。 こういった点で確かに型クラスの拡張には柔軟性がある。

ファンクタ則もアプリカティブファンクタ則もモノイド則もすべてこっちが勝手に定めたルールとメソッドのセット。 そのルールに沿っているならそれらのメソッドを便利に使えるという、ただそれだけの話。 本質的には結合則を満たしているなら演算(*)はどの順番でもできるよね、便利だよね、といった話。

うーむ、 面倒です。 足し算や掛け算のときもそうでしたが、 こんなふうにnewtypeにくるんでmappendとmemptyを使ったりするんだったら、 普通はBoolの関数を直接呼び出しますよね。

たしかに。newtypeの利用は冗長に思っていた。

モノイドと畳込み

標準のfoldrなどはlist専用の畳込み関数で、Foldableモジュール下のはFoldableインスタンスの汎用のそれ。 listもFoldableなんだからFoldable側に寄せればよかったのに。なぜそうしなかったんだろう。

あれ、 でもモナドはアプリカティブファンクターの強化版のはずでは? Monadインスタンスは必ずApplicativeのインスタンスでもあるよう、 クラス宣言の前に、 class (Applicative m) = > Monad m whereという型クラス制約があるべきではないでしょうか。 うむ、 確かにそうなんです。 でも、 Haskellの誕生当時には、 アプリカティブファンクターがHaskellと相性がいいとは誰も思わなかったのです。 それでも、 すべてのモナドはアプリカティブファンクターであることに間違いはありません。 たとえMonadのクラス宣言にそうは書いていないとしても。

えええ、まじか。Haskellってかなりの熟考ののちに作られた言語だと勝手に思い込んでいたけど、こんな後悔があるのか。 というかファンクタ、アプリカティブファンクタ、モナドの概念整理ってHaskell由来なの?

Reader, Writerモナド

対応してなくね!? 全然両者が対応関係になくね!?

やっぱりReaderモナドはWriterモナドと対応しているようには見えない・・・。なんというか、すべてに対して適用するような、Listモナドっぽい性質?

やはりモナドという概念が生まれた経緯がわからない。オブジェクト指向のように複数の値を複雑に組み合わせることが簡単にできない関数型言語という中で、変数(状態、値)にリッチな状態や変数そのものを組み合わせたいというニーズがあって、それを実現するためにモナドが生まれてきたのか?

あかん、Stateモナドから頭にさっぱり入ってこない。

HaskellのEitherはLeftにErrorの型クラス制約があるのがScalaと違う点。

アプリカティブとモナドのデフォルトの表現がreturnとpureで対応しているように、fmapとliftMもまた対応している。

liftMってこんな重要な関数だったのか。普通にスルーしてたわ。

この辺でやっとファンクタ、アプリカティブファンクタ、モナドの強弱関係がしっくりきた。

この辺まで読んでやっとscalaでも定義されているliftM, liftA2などがしっくり来たけど、これScalaでliftM, liftA2など使っている人Haskellを勉強せずにこれ理解できる人いんの?

なるほど、liftっていうのはそういうことか。そういうことか?liftM2とfmapは本質的には同じ?(取る引数の数が違うだけ?) そういう意味ではfmap = liftM1 = liftA1か。

入れ子になったモナドは平らにできる(flattenのイメージ)。

これは>>= の定義を考えると納得。

m >>= fとjoin(fmap f m)は常に同じ。 これはアプリカティブファンクタの<*>とモナドの>>=の定義を見比べれば自明。

Zipper

これはLensと似たような概念?

あああ、ついに読み切った。買って数年かかったがとにかくたどり着いた。