2012-05-16

Go言語の型宣言をHaskellから理解する

という誰得記事を書いてみたいと思います。

Goの型システム……というかtype宣言はなかなか面白いんじゃないか、ということに最近気づきました。type宣言は新しい型を宣言するものです。たとえば、A Tour of Goのだと、
type Vertex struct {
X int
Y int
}
のように宣言します。これは当然のように思われるかもしれません。
ところが、type MyFloat float64 のように宣言することがあります。これはどういう意味があるのかというと、実態としてはfloat64なのだが新しい型として定義したい、という意思表示となります。

具体的に、これが意味するものとしてメソッド宣言を見てみましょう。VertexにAbsというメソッドを定義した場合:
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
このように表記します。このとき、*Vertex 型の変数 v について v.Abs() とメソッド呼び出しができるわけです。
同じように、type MyFloat float64 として宣言したMyFloatに対しても、メソッドの宣言ができます():
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
というか、どんな型に対してもメソッドの定義ができます。ただし、異なるパッケージの型に対して勝手にメソッドを追加したりできない、ビルトインの型も同様に挙動を変更させることはできない、という制限があります。そのため、既存の型と実態は同じだが独自の意味合いを持たせたい(実態としては数値だが特別な意味を持たせたい、というような場合)にこういう型定義によって別の型をつくることになります。この場合、Abs()というメソッドはMyFloatという型に対してついているのであって、float64に対して呼ぶことはできません。
つまり、float64MyFloatはほんとうに別な型なのです。たとえば上のコードサンプルでreturn fとすることはできません。Abs()が返すべきのはfloat64だからです。MyFloatfloat64は加減乗除等の演算も行えません。事前に型変換を行う必要があります。

たしか『入門OCaml』だったかとおもいますが、会計処理的なシステムで、消費税込みの金額とまだ税を組み入れてない金額が両方あるので混乱しがちになるという話が出てきて、phantom typeを導入して型安全を保つという事例があったとおもいます。同じことがGoのtype宣言では可能です。

この挙動は面白いし便利だと思う一方、C/C++のtypedefを知っていると、とても奇妙に思えます。C/C++のtypedefは簡単に言うと型に別名をつけるだけだからです。typedef float MyFloat、などと書いてもMyFloatfloatは区別されません。floatを引数に取るメソッドにMyFloatを与えても構いません。C/C++を使っている場合、これはこれで便利なものです。

ここでC/C++のtypedef相当の言語とGoのtype相当の言語を両方併せ持つ言語といえば、そう、皆さんご存知のHaskellですね(やっと出せた)。Haskellでいうとtype宣言がC/C++のtypedefであり、Goのtypeに相当するのはnewtype宣言だと言うことができるでしょう。
Haskellのnewtype宣言というのは、実態としては他の型と同等であるが、全く別の新しい型として定義したい、という場合に使います。たとえばA Gentle Introduction to Haskellでは、Integerと実態は同じであるが正数のみを扱うNatural型を定義しています(日本語訳)。Goのtypeはまさにこれと同じことをします。Natural型に対して定義したメソッドがIntegerに影響を与えないところも同じです。

ところで、Goにはtypedefはありません。既存の型に別名をつけることはできません。例えば上でVertexという構造体が出てきました。ここで、type Vertex2 Vertex などとしても、*Vertexに対して宣言されたAbs()メソッドは、*Vertex2型の変数にたいして呼ぶことはできません。
不便じゃないんでしょうか。そもそもtypedefってなぜ必要なんでしょうか。

Haskellのtype宣言は、既存の型に別名をつけるという意味で、まさにtypedefです。実例としても、String型は実際のところ文字のリスト[Char]型である、という代表例もあります。これはまさに同じであって、String型の値はリストとして扱うことができます。リストを引数に取る関数はすべてStringを扱うことができるし、パターンマッチもできます。これがnewtypeとして別の型になってしまうと、Haskellのリストを扱うためのパワーをスポイルしかねません。いっぽう、Stateモナドは、実態としてはただの関数だけども、newtype宣言でState型として宣言されています。こういうものはHaskellではtype宣言では扱いづらいし、ややこしい問題もありますが、newtype宣言によって状況はかなりクリアになります。
ようするに、「メソッドは引き継がれない」「別の型として扱われる」という点がメリットになるかデメリットになるか、というのがnewtypeを使うか、typeを使うかの違いになっているといえるでしょう。

Go言語では、どうして他の型のエイリアスを作らなくても問題にならないのでしょうか。たとえば、
var f1 MyFloat = MyFloat(1)
var f2 float64 = 1.0
f1 + f2
などとするとコンパイルエラーが発生します。f1f2は異なる型なので演算はできません。実際、上で挙げた例のように、税抜の金額型と税込の金額型を別の型として宣言している場合、この2つの型の変数同士を気軽に足せてしまったら分けた意味が薄れるので困る。f1 + MyFloat(f2)などとしなければならないわけです。

ところが、
var f1 MyFloat = MyFloat(1)
f1 + 1.0
これはエラーになりません。
Go言語には暗黙の型変換はないのだけど、数値リテラルは特定の型の値を持つわけではなく、文脈によって適切な数値型となり、実態として数値の型であるモノ(MyFloatとか)も数値型とみなされます。演算子もまた、数値型に対して有効であるという定義であり、オーバーロードや再定義ができません。このように演算子やリテラルを特別処理にすることで、暗黙の型変換なしに、異なる型であっても、こういうよくある表記では問題を起こさないようになっています。

また、メソッドについてはどうでしょうか。HaskellのString[Char]と同等なのはリストに対するオペレーションを使いたいからだと思います。ただ、Go言語のメソッドや関数では、多くの場合インタフェースが一致しているかどうかだけが大事で、継承関係を必ずしも持つ必要がありません。このため、既存の型と構文上も同等に扱いたいというモチベーションがそれほど高くないのではないか、と推測しています。
Go言語を設計するにあたり、どれぐらい慎重にこういったことが決められたのかはぼくは知らないのですが、こういった事例からは、「こうしておけば実用上問題ないでしょ」という思い切りのようなものがあるな、と思うのです。
実際のところ Haskell にしたって type 宣言を書くことはそう多くない気がします。なくてもかまわないといえばかまわない、Stringだけがうまくいけばそれで良い、そういうヤツなのではないでしょうか。だが、既存の言語の枠組みで「String[Char]のように扱う」ためには、type宣言のようなものを持ち出すか、さもなくばStringを特別扱いしなければなりません。特別扱いはイヤだ、言語の定義で直交性を保ちたい、という美的センスがあるのではないかと思うのです。

直交性というのはプログラミング言語の「美しさ」を語る上では大事なキーワードだと思うのですが、Go言語では、そこまで最重要なポイントとみなされていないんじゃないかという疑念を抱いています。もちろん軽視しているのではないでしょうが、「大事だけどほかに重視すべきことがある」という価値観があるように思います。
でもそれでいいじゃん、動くし実用上は問題ない、といえるのがGoの面白いところだなと思った、というところで締めたいと思います。