なんとな~くしあわせ?の日記

ClojureとかAWSの設定とかをメモする技術ブログ

すごいHaskell たのしく学ぼう 読解2



はい、すごいHaskell後半戦です。
あまりHaskell自体を使う気はないので、概念だけ読み取って実際的な話は飛ばす(IO/モジュール)。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

第7章 型や型クラスを自分で作ろう

7.4 型引数

ここを読み飛ばすとファンクターがわからなくなる

コンストラクタは型を引数に取って新しい型を作るものです。

簡単な例として以下が挙げられている

data Maybe a = Nothing | Just a

このMaybeがコンストラクタ。 後で使うので覚えておく。
Haskellにおいてはdataは新しいデータ型を作るための宣言。Maybeのあとに'a'という型引数がとられている。これにより、内部の型が決定される。

型引数はまさにC++におけるtemplateと同じものである。こういったモノの内、コンパイル時に挙動が決定するものを静的多相(static polymorphism)というらしい。 → c++ - Static polymorphism definition and implementation - Stack Overflow

さてさて、しかしC++だとこれは実装できるかと考えたが存外難しそうだ。

というのは…Haskell側で[Nothing or Just a]という宣言がなされているので、これはScalaのOption型のような動きをすることがわかると思う。つまり、単純な型[a]をラップしてやり、そのラップした型を呼び出したら「ない(Nothing)」、「ある(a)」という状態を返す事ができるということだ。C++には標準でそのようなOptionalの機能は入ってないので、実装にはboostが必要だ。Java8にはある。

つまりこうなる、以下のブログから引用
C++ で Maybeモナドを書いてみた - C++でゲームプログラミング*1

template<typename a>
using Maybe = boost::optional<a>;

Haskellerが箱とか言ってるのは、変数をラップするもののことである。

7.6 型シノニム

型シノニム 別名をつける処理です。
Haskellではtypeがキーワードですし、Scalaも同じ感じ。D言語だとalias、C++だとusingでしょうか。

type String = [Char]

7.10 Functor型クラス

ここからfmapの理解が必要なので難解になる。

Functorは、全体を写せる(map over)ものの型クラスです

???…よくわからない現代魔法だ。そして、Functorの日本語訳は関手 - Wikipedia
どうやらFunctor(関手)からは外の数学の世界(圏論)に通じているらしい(プログラマーに必要かどうかはともかく)

Functor型の実装は以下の通りらしい

class Functor f where
    fmap :: ( a -> b ) -> f a -> f b

a -> bHaskellでは 「aを受け取ってbを返す関数」を表す。アロー演算子複数あるということはそのような式がネストしているのだ。なおかつ "f""型コンストラクタ"である。「f a」の「a」部分は、"f"の型を決定する"型引数"。「f b」の「b」も同様の関係だ。

これを自然言語で説明すると

どうやらfmapは、「ある型aから別の型bへの関数」と、「ある型aに適用されたファンクター値」を取り、「別の型bのほうに適用されたファンクター値」を返す関数のようです。

関手則(Functor則)

Functorには守らなくてはいけない規則がある。Functor(関手)について モナド - ウォークスルー Haskell から引用

数学的には,関手は次の 2 つの規則 (関手則) を満たすことが要求されます。
Maybe 関手やリスト関手は,確かに関手則を満足しています (簡単に証明できます)。

恒等射の保存

射とかいうワードを出すからよくわからなくなる。コードを見よう。
idというのは「何かの型の値を受け取り、その値をそのまま返す」関数。FunctorはFunctor則に従う場合、これを満たさなくてはならない。Nothingを渡したらNothingを返さなくてはならない。

fmap id = id
合成の保存

これはまだ簡単で、関数合成した値=それぞれを順番に計算した値であれかし、という規則。

fmap (f . g) = fmap f . fmap g

このへんの話はすごいHaskellの中でも第11章で述べられているので、ここではまだ触れなくていいと思う。
それに、数学的にはその2つの規則は重要かもしれないが実装上はそんなにそれは大事ではない気がする。*2
(…それらが重要になるのは後のアプリカティブファンクターになってから)

ここで他のFunctorにあたるものが列挙される

Functor 説明
Maybe Nothingが渡されたらNothingを返す。Justが渡されたらJustを返す。
参考 What does the “Just” syntax mean in Haskell?
Tree あとでわかったような説明を書く
Either 2つ引数とるけど関数の部分適用で1つになるので他2つと同じようにFunctorになるとかなんとか
ちなみに独習Scalazに同じような記事がある

http://eed3si9n.com/learning-scalaz/ja/Functor.html

Functorの理解のために

ある言語からある言語への概念の翻訳を行うことは、その概念の理解に役立つ。面白いブログの投稿があったので引用してみる。
The Functor Pattern in C++ | FP Complete

さきほどのHaskellコード

class Functor f where
    fmap :: ( a -> b ) -> f a -> f b

これをC++で表したものが、以下らしい。なんとなく理解できないだろうか?functionstd::functionのことです。

template<class A, class B>
function<B()> fmap(function<B(A)> f, function<A()> fA)
{
    return [f, fA]() {
        return f(fA());
    };
}

そして、なぜHaskellではそのコードを短く書けるかについての理由に当たるのが以下です

引用

Functor as a Haskell typeclass

In C++ we'll just work with the functor pattern without abstracting it further. But in Haskell such patterns can be generalized using type classes. The closest thing to type classes in C++ were concepts, which didn't make it into the C++11 Standard.

拙訳

Haskellの型クラスとしてのFunctor

C++では、我々はそれ以上の抽象化することなしにFunctorを使って仕事をした。しかしHaskellではそのような類型は"型クラス"を用いて生成することが可能だ。C++における類似の概念はConceptsだろう、それはC++11の標準には追加されなかった。

C++のコンセプトについての記述は以下の通り
コンセプト (C++) - Wikipedia

まとめ - 自分で Functor を定義する

ここまで来てようやくFunctorを理解できた気がします。
Functor(関手)は、コンストラクタ、型引数、静的多相を意識しなければ動きを理解できず
加えて、実装上はあまり関係ないが、厳密な理解のために
関手則(Functor則)、射、恒等射などの数学ワードが頻出する*3
のでとても大変ですね

以下のサイトの手順で独自のFunctorを定義可能なので、試してみてください。www.geocities.jp

自分も最後まで説明しないと理解した気分にならないのでまた引用します。

1. Mfunctorの定義
・Functorクラスの代わりにMfunctorを定義
・fmapは予約済みなので代わりに<$>を関数として定義、これはMfunctorに生える関数

まずこのようにclass定義をして、

class Mfunctor f where
  (<$>) :: (a -> b) -> f a -> f b

2. インスタンスごとの動作を規定
・Mfunctorにリストが渡されたらmapで返す

instance Mfunctor [] where
  (<$>) = map

3. 実演
・作成した<$>に2つの要素を渡す
・"a -> b" への変換を行う関数 = (*2)
・[1, 2, 3, 4, 5] というリスト = f a
・aの型はリストなので、最後に返る型もリスト
・<$>はmapとして実体化 → "map (*2) [1,2,3,4,5]" と同じになる

*Main> (*2) <$> [1,2,3,4,5]
[2,4,6,8,10]

第8章 入出力

省略

第9章 もっと入力、もっと出力

省略

第10章 関数型問題解決法

省略

とりあえずここまで、Functorの概念の理解がかなり重たかったですね。
しかしこれでアプリカティブファンクター、モナド、モノイドへの重い扉が開かれました!

*1:using宣言はtypedef宣言の後裔にあたるものである。詳しくはこの記事 typedef は C++11 ではオワコン

*2:数学畑の人たち or Haskellしか知らない人たちはここにこだわりがちに見える。私のエントリを見て欲しいのだがC++では単なるコールバック関数をもファンクターと呼ぶ vector<クラス>をソートしたいとき

*3:たぶん圏論は必要ないよね