以下の記事は Java について触れていますが、Java を dis っている訳でもありませんし、冗長に見える例を意図的に使っています。
最近 Twitter で golang に Generics が無い事についてずいぶんと盛り上がったのですが、僕の意見をこのブログにも書いておこうと思います。
golang に多相が無いのはアレだとか開発者の怠慢だみたいな話はだいたい他の言語を覚えた人から出る感想で、静的型付言語である golang を見ると確かにそう見えるかもしれない。ただ golang は Java や他の言語と違って Duck Type を採用している。
— Vim芸人 (@mattn_jp) March 7, 2017
スクリプト言語の多くに多相が求められないのと同じ様に golang を深く触る人達から多相が欲しいという意見がそれほど出ないのは golang の型が Duck Type だからだと思ってる。
— Vim芸人 (@mattn_jp) March 7, 2017
これが何を意味しているかというと、例えば比較可能な数値型 Numeric を作ったとしてそれを実装する型 Int(中身はint) が Numeric を継承しなくても良いって事になる。 例: https://t.co/Joe4u30l4M
— Vim芸人 (@mattn_jp) March 7, 2017
これが出来てしまうと多相であるメリットはコンパイル時の型拘束くらいになる。例えば Less(T, T) の形。これが欲しい場合は golang ではその型で実装する事になる。この頻度がどれほど高いかというと、個人差もあるけど実はそれほど無いんじゃないかと思ってる。
— Vim芸人 (@mattn_jp) March 7, 2017
これは硬い型システムを提供するか比較的柔らかい(と見える)型システムを提供するかのアプローチの違いの話なので件の「多相が無いのは××だ」って話も言ってみれば「俺の大好きな C# や Java と違う」って話とそれほど変わらないと思ってる。
— Vim芸人 (@mattn_jp) March 7, 2017
Java は多相が欲しいというニーズに Generics で答えた。一方 golang は Duck Type と type assertion で答えた、その違いでしかない。
— Vim芸人 (@mattn_jp) March 7, 2017
僕も煽り言葉で「golang に generics は要らない」と書いた事もありましたが、本心は「Generics 欲しいと思った事はあるけど無くても生きてこれた」というところです。
Go言語、7年くらい書いてるけどGenerics欲しいと思った事一度もないかと言われると嘘になる。が、無くても生きて来れた。
— Vim芸人 (@mattn_jp) March 8, 2017
「golang の型システムは貧弱だ、Generics が無いから駄目だ」、そんな意見をたまに見ます。実際どんな場合に Generics が欲しいかというと、ある特定の意味を持った型を手続き処理に渡したい場合です。例えばその一つがコンテナです。Java の List<SomethingType>
などがそれにあたります。この List の中身をぐるっと回して処理したい、または処理する関数を以降ほかの型でも使用したい、といった物です。golang に Generics が欲しいと言っておられる方の多くはこれが欲しいと言っておられるのだと思います(違っていたらごめんなさい)。例えば整数か浮動小数点か分からない数値をコンテナに持たせてその合計を出す処理を考えてみます。
import java.util.List;
import java.util.Arrays;
public class Foo {
public static Integer sum(List<Integer> list) {
int ret = 0;
for (Integer i : list) {
ret += i;
}
return ret;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5,2,3,1,4);
System.out.println(sum(list));
}
}
Java だとこんなところでしょうか。golang だと container パッケージもありますが、slice で処理できる物は slice のまま扱いますね。さて、この Java のコードを再利用可能にしてみます。
import java.util.List;
import java.util.Arrays;
public class Foo {
public static <T> T sum(List<T> list) {
int ret = 0;
for (T i : list) {
ret += i; // コンパイルエラー
}
return ret;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5,2,3,1,4);
System.out.println(sum<Integer>(list));
}
}
ここまでは想像できますよね。コメントにも書きましたが、Java もアドホック多相のまま演算子を処理するコードはコンパイル出来ないのです。そうなると「足し算する関数を持ったインタフェース」が必要になる訳です。java.lang.Number にもそんな物は定義されてないので、自分で意味づけする必要がある訳です。アドホック性を保ったまま Java で実装するとどうなるでしょう?
import java.util.List;
import java.util.Arrays;
interface MyNumber {
MyNumber add(MyNumber rhs);
}
class MyInt implements MyNumber {
private int x;
MyInt(int n) {
this.x = n;
}
public MyNumber add(MyNumber rhs) {
return new MyInt(this.x + ((MyInt)rhs).x);
}
public int value() {
return this.x;
}
}
public class Foo {
public static MyNumber sum(List<MyNumber> list) {
MyNumber ret = list.get(0);
for (int i = 1; i < list.size(); i++) {
ret = ret.add(list.get(i));
}
return ret;
}
public static void main(String[] args) {
List<MyNumber> list = Arrays.asList(
new MyInt(5),
new MyInt(2),
new MyInt(3),
new MyInt(1),
new MyInt(4)
);
System.out.println(((MyInt)sum(list)).value());
}
}
おや、なんかめんどくさくなりましたね。Double 版も作りたくなったとしたらなんか大変そうだしもし Int と Double を足せる物を実装するとなったらもっと大変そうに見えませんか?golang でも同じ事をやってみましょう。
package main
import (
"fmt"
)
type Numeric interface {
Add(Numeric) Numeric
}
type Int int
func (i Int) Add(n Numeric) Numeric {
switch t := n.(type) {
case Int:
return i + t
}
panic("unknown type")
}
func sum(list []Numeric) Numeric {
ret := list[0]
for i := 1; i < len(list); i++ {
ret = ret.Add(list[i])
}
return ret
}
func main() {
list := []Numeric {
Int(5),
Int(2),
Int(3),
Int(1),
Int(4),
}
fmt.Println(sum(list))
}
Java も golang も意図的に冗長に書いているので、なんだそのコードはといったご意見もあるかと思います。
上記で Int と Double を足せる物を作る場合について触れましたが、これ golang でやってみたいと思います。
package main
import (
"fmt"
)
type Numeric interface {
Add(Numeric) Numeric
}
func sum(list []Numeric) Numeric {
ret := list[0]
for i := 1; i < len(list); i++ {
ret = ret.Add(list[i])
}
return ret
}
type Int int
func (i Int) Add(n Numeric) Numeric {
switch t := n.(type) {
case Int:
return i + t
case Float:
return i + Int(t)
}
panic("unknown type")
}
type Float float64
func (f Float) Add(n Numeric) Numeric {
switch t := n.(type) {
case Float:
return f + t
case Int:
return f + Float(t)
}
panic("unknown type")
}
func main() {
list := []Numeric {
Float(5),
Int(2),
Int(3),
Float(1),
Int(4),
}
fmt.Println(sum(list))
}
そんなに複雑じゃないと思いませんか?上記でも書いた通り、Java もアドホック性を保ったままコンテナを使うには interface を作らないといけないのです。それは golang も同じ話なのです。
やりたい内容によっては Java も golang もフェアなのです。さらに言うなら Duck Type を使ってシグネチャさえ満たせば interface を引数に持った手続き処理に渡せるのは、Java にないメリットになり得ます。
僕は golang も Java も C++ も好きです。Generics が便利なのも知ってます。この記事も Java を dis っている訳ではないです。ただ言いたいのは「golang は型が貧弱だ、Generics を実装しないのは開発者の怠慢だ」といった意見に同調する人たちに「golang は Generics が無くても事足りてしまう事がある」という1例を見せたいだけなのです。もちろんリフレクションや Object のまま扱えばもっと短くできる事は知っています。また本当に Generics が欲しくなるケースもあるのは事実です。ただ、そんな荒さがししてるくらいなら、まず golang に触れてみたらいいんじゃないか、そう思う訳です。
追記
例えが偏り過ぎた事もあって反応頂いてるみたいです。
golang と Generics と吾 - Qiita
http://qiita.com/yuroyoro/items/6bf33f3cd4bb35469e0b
Java の Generics にもの思い - Qiita
http://qiita.com/t2y/items/139c6a38173d7750ddfc
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧 大輔, 鈴木健太, 稲葉貴洋
技術評論社 大型本 / ¥351 (2016年09月09日)
発送可能時間: