情報隠蔽とモジュールとシグネチャファイル (F# Advent Calendar 2016)
いまさらですが、2016年12月のF# Advent Calendar記事の締め切りに間に合わなかったこともあり、アクセス数が少ないので、ここで再紹介します。
http://qiita.com/matarillo/items/8444db7b97abd29194c2
シグネチャファイルを書くことで、型の実装を隠蔽する方法について書きました。
……といいつつ、記事のほとんどは、横浜へなちょこプログラミング勉強会さんによるコーディング問題 http://nabetani.sakura.ne.jp/hena/orde09_penwa/ の回答になっています。 ^^
コードの説明というより、解法の説明が長いです。
もしミスチルがF#だったら
love : 'Ego -> 'Ego -> seq<int> when 'Ego : comparison
記念に。
多相リテラルはないけど、演算子オーバーロードとユーザ定義型変換とカスタム数値リテラルとジェネリック単位元と
この記事はF# Advent Calendar 2015の9日目の記事です。
記事の発端は、id:m-hiyama さんの3つの記事です。
http://d.hatena.ne.jp/m-hiyama/20151130/1448843034 http://d.hatena.ne.jp/m-hiyama/20151202/1449016930 http://d.hatena.ne.jp/m-hiyama/20151208/1449544751
3行で無理やりまとめると
- ジェネリクスと演算子オーバーロードと型クラスが便利に使えたらいいなあ。
- C++には演算子オーバーロードがあるし、暗黙の型変換も便利だけど、ユーザー定義リテラルはいまいち。
- 空気を読んでくれる、多相的な定数(数値リテラル)や記号(演算子)があるともっといいのに。
2つ目の記事は、Min-Plus半環という、和と積の定義がちょっとおもしろい代数をネタにしています。
F# は
| ジェネリクス | ある |
|---|---|
| 演算子オーバーロード | ある |
| 型クラス | ない |
| ユーザー定義リテラル | ある |
| 暗黙の型変換 | ない(明示的にしかできない) |
| 多相的な数値リテラル | ない |
なのだけど、Min-Plus半環のコードを書いてみたらどうなるの?というのをやってみました。
まずは、m-hiyamaさんの2番目の記事のコードを参考に
を入れてみたのが、12/7の記事に載せたコードです。ここに再掲します。
次に、m-hiyamaさんの3番目の記事を参考にして、多相的な数値リテラルはないけれども演算子オーバーロードをたくさん作ってみたらどんな使い勝手になるかを試してみました。
簡単に解説します。
- 演算子
@+と@*がMin-Plus半環のための演算子です。これは(?<-演算子を使うハックによって)int型、float型、およびMinPlusカスタム型を引数にとることができます。 - なお、このハックについては、この記事から始まる5つのエントリーで解説していますので、興味がある人はそちらを読んでください。
main関数では、シャドーイング機能を使って、+演算子と*演算子の実装を@+と@*に付け替えています。
ちなみに、fstropicalモジュール(fstropical.fs)では、Min-Plus半環のための演算子として@+と@*を使っていますが、ここを+と*に置き換えることもできます。すると、fstropicalモジュールをopenするだけで、ファイル全体で+演算子と*演算子の動きが置き換わります。
m-hiyamaさんは
僕が名前(記号やリテラルも含む)のオーバーロードに拘るのは、名前の増加に耐えられないからです。 と書いていましたから、
@+と@*を使わないコードの方を好まれるかもしれませんね。
というわけで、「空気を読んでくれる演算子」ぐらいならF#でなんとか実現できそう、という感じでした。
演算子オーバーロードとユーザ定義型変換とカスタム数値リテラルとジェネリック単位元と
(12/8追記: 檜山さんの新しい記事を受けて、別バージョンを書きました。https://gist.github.com/matarillo/d9861bcce64b4b6ae4da 明日には新しいブログエントリーを書くつもりです。)
http://haskell.g.hatena.ne.jp/matarillo/20151202/1449027369の続き。
もともとの http://d.hatena.ne.jp/m-hiyama/20151202/1449016930に近しいコードに書き直しました。
Qをプリフィックスとするカスタム数値リテラルを定義してみました。
0Qと1Qの扱いをどうするか悩みましたが、実数 0.0および1.0からの変換と同じ扱いにしました。
MinPlus半環の単位元が必要な場合は、MinPlus.ZeroおよびMinPlus.Oneを使う想定です。
この2つが定義されているので、LanguagePrimitives.GenericZeroとLanguagePrimitives.GenericOneを使うこともできます。といってもインライン関数以外で使ってもそんなにうれしくないですが……
Re: コンピュータは「掛け算は足し算とする」を理解できるか
Re: お手軽で実用的なジェネリックスへの道は遠い
「TypeScriptジェネリックス:可能性が見えると不満がつのる」において、クラス定義や関数定義に型パラメータを渡せるだけでは、ジェネリック・プログラミングは難しいと述べました。そのときの例題はリストの総和だったのですが、より簡単な累乗(ベキ乗、power)計算を例にしてもう一度問題点を説明します。
最近のプログラミング言語はジェネリックス機能を持っているものが多いですが、言語の進化に伴って後から付けたものがほとんどで、最初からジェネリックスありきで設計したものって、(少なくともメジャーどころでは)ないんじゃないの。ジェネリックスと演算子オーバーロードと型クラスを中核にしたプログラミング言語が出てきたら面白いのにね。
F# はTIOBEランキングで31位 なのでメジャーどころとは言いにくいところですが、ジェネリックスがあって演算子オーバーロードもあって、型クラスはないけどインライン関数の力でポリモーフィックなべき乗を書くことができます。
実行結果はIdeOneで見ることができます。
LanguagePrimitives.GenericOne関数は、プリミティブな数値型、もしくはOneという名前の静的メンバーが定義されている任意の型を引数にとって、その型の「1」を返すというもの。
インライン関数として定義したpowの引数や戻り値の型や型制約を省略しているのでよくわからないかもしれませんが、F#コンパイラが適切な型や型制約を推論してくれます。
たとえばpowの引数xの型については、
- 演算子
*がオーバーロード定義されていること LanguagePrimitives.GenericOne関数と同じ制約を持つこと、すなわち、プリミティブな数値型、もしくはOneという名前の静的メンバーが定義されている任意の型であること
の2つが要請されます。
その上で、powの呼び出し側で、実際にどんな型を引数として渡しているかが、コンパイル時にチェックされます。今回の例ではint型とfloat型を渡していますが、どちらもプリミティブな数値型であり、*演算子がオーバーロード定義されているので、コンパイルに通ります。
「F#すごいじゃん」と思うかもしれませんが、実は注意点があって、F#ではジェネリクスと(多相)インライン関数は別の仕組みなのでシームレスに使うことはできないというところ。そのあたりのことがMSDNに書いてありますが、一言でいえば「インライン関数はコンパイル時に、ジェネリクスは実行時に型が解決される」という感じです。
(追記)ちなみにScalaの場合は「staticなんてものはないのだ。シングルトンオブジェクトがあるだけだ。」「型からシングルトンオブジェクトのインスタンスが決まる仕組みが整備されれば、それが型クラスになるのだ。」という感じの解決法で、(自分で好んで書きたいとはあまり思わないものの)興味深い仕掛けです。