methaneのブログ

このブログに乗せているサンプルコードはすべてNYSLです。

Python の正規表現で IGNORECASE するときは気をつけよう

Python 3 で文字列が Unicode になりました。というだけで感のいい人は分かるかもしれません。

はい、大文字小文字の判断も ASCII じゃなくて Unicode になります。

In [6]: re.match("[a-z]", 'ı', re.I)
Out[6]: <_sre.SRE_Match object; span=(0, 1), match='ı'>

この文字は LATIN SMALL LETTER DOTLESS I だそうです。

予想外のものにマッチするのは単純にバグになりやすいのもそうですが、この [...] にマッチする部分を作るのも非効率的になります。Python の標準ライブラリの正規表現は最終段階以外が全部 pure Python で書かれているので、正規表現コンパイルが遅く非効率になります。

なお Python の標準ライブラリの正規表現は文字列だけでなくバイト列にも使えて、その場合はこういった罠はありません。

Go が for ループをやめるために足りないもの

ジェネリクスの話題になると常に出てくるのが、 for ループの代わりに関数型スタイルで書きたいという要望です。 for ループで書くのは、可読性が悪く、筋力がいるとまで言う人もいます。

しかし、ジェネリクスが追加されても、このスタイルのプログラミングは実用的にはなりません。ジェネリクス以外にも足りない部分がたくさんあるのです。

例えば、次のようなコードを考えてみましょう。

type PointLog struct {
    ID     int64
    UserID int64
    Point  int32
}

// 今の書き方
func UserTotalScore(log []PointLog, userID int64) int64 {
    var t int64 = 0
    for _, p := range log {
        if p.UserID == userID {
            t += int64(p.Point)
        }
    }
    return t
}

ジェネリクスが入ったとして、 Filter, Map, Reduce で実装してみましょう。構文は仮とします。 なお、コンパイルが通らないコードなので、間違いがあっても気にしないでください。だいたいこういうコードになるという雰囲気だけ見てください。

func[T] Filter(f func(T) bool, xs []T) []T {...}
func[T,U] Map(f func(T) U, xs []T) []U {...}
func[T] Reduce(f func(T,T) T, start T, xs []T) T {...}

// 関数型スタイルの書き方?
func UserTotalScoreFP(log []PointLog, userID int64) int64 {
    return Reduce(func(x, y int64) int64 { return x + y }, 0,
        Map(func(p PointLog) int64 { return int64(p.Point) },
        Filter(func(p PointLog) bool { return p.UserID == userID }, log)))
}

可読性があがり、筋力がいらなくなり…ませんね。短い関数を省略形で書く、ラムダ式と呼ばれたりする記法が足りません。また、ラムダ式の引数や戻り値の型を書かなくて良いようにするためには、型推論の機能も全然足りません。

なお、ここまででも、「実用」するならコンパイラの最適化機能を向上しないといけないかも知れません。 ループで書いたときに比べて、大量の関数呼び出しと、それに伴う一時変数が増えます。上の例では簡単のために引数をスライスで受け渡ししていますが、こういう一時スライスはループで書くには明らかに冗長で、現在の典型的なGoのコードには出現しないものです。そのため、今のGoの最適化機能ではこの一時スライスを除去できません。

スライスの代わりに、値を逐次的に取り出せる一時オブジェクトを使う方式も考えられますが、それも今のGoの最適化機能では for ループと同じ効率になるとは限りません。

さて、ジェネリクスと最適化機能に加えて、ラムダ式とそのための型推論をGoに追加したとしましょう。ループで書いたときと比べてみてください。

// 関数型スタイルの書き方
func UserTotalScoreFP(log []PointLog, userID int64) int64 {。
    return Reduce((x,y) -> x+y, 0,
        Map((p) -> int64(p.Point),
        Filter((p) -> p.UserID == userID, log)))
}

大分スッキリしてきましたね。でも、処理の流れが右から左(サンプルコードでは改行してるので下から上)になっているので、これを左から右(上から下)に変えるJavaのStream APIのようなものを用意したほうがより多くの人にとって可読性が高くなるかも知れません。ついでに Stream API から mapToInt と IntStream も借りて、Reduceの代わりにSumを使いましょう。

// Stream APIもどきを使った書き方
func UserTotalScoreStream(log []PointLog, userID int64) int64 {
    return Stream(log).
        Filter((p) -> p.UserID == userID).
        MapToInt64((p) -> int64(p.Point)).
        Sum()
}

さて、元のコードをもう一度書いておきます。どれくらい可読性が悪く、筋力が必要だったでしょうか?

// 今の書き方
func UserTotalScore(log []PointLog, userID int64) int64 {
    var t int64 = 0
    for _, p := range log {
        if p.UserID == userID {
            t += int64(p.Point)
        }
    }
    return t
}

脱線しますが、個人的にはPythonHaskellの内包表記が、可読性が高く必要な筋力も少ないと思います。

user_totalscore = sum(x.point for x in log if x.userID == userID)

Goに移植するとしたら、上のジェネレータ内包表記(やHaskellの遅延リストの内包表記)は難しいので、リスト内包を追加するのが良さそうです。Pythonの構文を借りるなら、

func SumInt32(xs []int32) int64 {...}  // 戻り値が int64 なのがポイント

t := SumInt32([]int32{x.Point for _, x := range log if x.UserID == userID})

この SumInt32 が不格好に見えるかも知れませんが、それを Sum にするのにはジェネリクスではなくオーバーロードでも可能です。 例えば上の例では引数が int32 の配列ですが、オーバーフローを考えると合計は int64 にしたいかもしれません。こういった引数と戻り値の型が非対称な同名の関数を作りたいならオーバーロードのほうがシンプルです。

オーバーロードの proposal も出ていますが、さて、ジェネリクスオーバーロード、両方追加するべきでしょうか?僕にはわかりません。脱線はここまでにします。


さて、まとめます。関数型スタイルの書き方を現実的にするには、Goを次のように強化する必要がありました。

これらの機能を全部入れようとすると、コンパイラとリンカを複雑にし、コンパイル時間とバイナリサイズを大きくする危険性があります。

なお、私は言語設計者ではないので、まだ足りない大きな部分があるかも知れません。気づいた方は教えてください。


教えてもらったもの

今の panic() はあまり推奨されない機能だし、中断機構を実装するにもなにかしら言語に影響が出そうですね。

Re: Re: Go にジェネリクスがなくても構わない人たちに対する批判について

kmizu.hatenablog.com

Twitterである程度レスをしたのですが、やはり繰り返される話題なので残る形で書いておきたいと思います。

  • Goユーザーの中で、ジェネリクスがなくても構わないと主張するユーザーへの批判はしたけど、Goユーザー全てがそうだと思っているわけではない
  • Goユーザーの中でジェネリクス不要論を唱えているユーザーへの批判はしたけど、そういうユーザーを馬鹿にしているわけではない

私の前の記事は、まさに前者の批判に対する返答です。私はGoにジェネリクスを追加することに賛成ですが、別にそうならなかったとしても失望しない程度に「なくても構わない」人です。

一方で後者は、もしGoに限らず一般論としてのジェネリクス不要論だとすれば、批判にも値しないと思いますよ。話題にするつもりはありません。

Goは特に今で言うマイクロサービス的なものを(色んな意味で)効率よく開発するために作られた言語で、DBとかJSONとかRedisとか扱って HTTP API提供する簡単なサービスを書いてみたら分かると思うんだけど、本当にジェネリクスがなくて不便な場面ってメチャクチャ少ない。 interface {} (Javaでいう object) が出て来る場面って、ジェネリクスが無いせいで出て来ることは本当に稀。

とのことですが、まずこの前提がおかしいのではないかと思います。実際の利用シーンを視ても、コマンドラインツールやWebサービスGCありのC的な使い方、などなど、かなり様々な用途で使われている言語だと思いますし、Goが出たときのドキュメントにも、利用シーンをそれほど限定するような文言はなかったように思います。

マイクロサービスという言い方は良くなかったかも知れません。もちろんWebサービスも含むのですが、Webサービスの裏側で動くサーバーだったり、Webサービスと言ってもファイルのダウンロードやJSON APIだけとかのWebアプリではなかったりとか、そういったクラウド上で動くいろんなサーバー全般のことです。

Go の開発された背景やゴールは繰り返し紹介されてきました。

Goが解決しようとしている問題は大きくわけると2つの「スケール」です。1つはハードのスケール、つまりたくさんのコア、たくさんのサーバー、クラウドへの対応です。もう一つはGooglerの開発のスケール、ビルド時間短縮であったり、疎結合な開発チームへの対応だったり、先週までJavaC++Pythonを書いていた人が素早く生産的になれることでした。

もちろん、これらを目標にしたからと言って、用途がこれらに限定される訳ではありません。GCやコードフォーマッタ付きのC言語と見た時、OpenSSLへの依存とかしなくても https のクライアントが使えますし、autotools などのたくさんの周辺ツールを覚えなくても (Windows を含む!) クロスプラットフォーム対応できるし、クラウドへのデプロイを助けるシングルバイナリやクロスコンパイルはCLIツールのバイナリ配布も助けます。

開発のスケールについては、Googleの開発体制はほとんどそのままOSSの開発体制にも繋がります。週末にしかGoを使わない人が、世界中の異なるタイムゾーンで、フェイス・トゥ・フェイスのコミュニケーションは殆どない状況で協力しているのですから。

3つの記事から幾つか引用します。(ですができれば勝手にGoの役割を思い込みで決めつける前に記事を全部読んで下さいね)

Go, Open Source, Community

The first goal is to make a better language to meet the challenges of scalable concurrency. By scalable concurrency I mean software that deals with many concerns simultaneously, such as coordinating a thousand back end servers by sending network traffic back and forth.

Today, that kind of software has a shorter name: we call it cloud software. It’s fair to say that Go was designed for the cloud before clouds ran software.

Go at Google: Language Design in the Service of Software Engineering

The problems introduced by multicore processors, networked systems, massive computation clusters, and the web programming model were being worked around rather than addressed head-on.

When Go launched, some claimed it was missing particular features or methodologies that were regarded as de rigueur for a modern language. How could Go be worthwhile in the absence of these facilities? Our answer to that is that the properties Go does have address the issues that make large-scale software development difficult. These issues include:

  • slow builds
  • uncontrolled dependencies
  • each programmer using a different subset of the language
  • poor program understanding (code hard to read, poorly documented, and so on)
  • duplication of effort
  • cost of updates
  • version skew
  • difficulty of writing automatic tools
  • cross-language builds

Individual features of a language don’t address these issues. A larger view of software engineering is required, and in the design of Go we tried to focus on solutions to these problems.

Toward Go 2

We want to make programmers more effective at managing two kinds of scale: production scale, especially concurrent systems interacting with many other servers, exemplified today by cloud software; and development scale, especially large codebases worked on by many engineers coordinating only loosely, exemplified today by modern open-source development.


たとえば、Java 8以降で、整数のリストを降順ソートしたいとき、 Collections.sort(xs, (x, y) -> y.compareTo(x)); と書けば済みますが、ジェネリクスがないと Collections.sort(xs, (Object x, Object y) -> ((Integer)y).compareTo((Integer)x)); となって面倒さが大幅にあがります。

Twitter でも返信しましたが、最近のGoはもう少し楽な書き方ができています。

   // https://golang.org/pkg/sort/#Slice
    sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name })

でも、Goがジェネリクス持ってないことを非難するときにはJavaを「持ってる」側に分類しておいて、Javaレベルのジェネリクスはあんまり嬉しくない*2ことを説明すると 「ジェネリクスJavaだけの機能じゃない。Javaと比較すんな」みたいな反応されるのにはうんざり。

Javaレベルでのジェネリクスでも上記のように十分うれしいと思いますが、それはともかく、私が言ってないことを勝手に補完してそれに反論されるのはちょっと困惑です。ここに関しては引用がないので、一般論としての反論なのかもしれませんが。

はい、これは kmizu さんへの反論ではなく、この話題になるといっつも湧いて出てくるJavaで型を語るなマウントしてくる人に向けた牽制でした。 Javaでも持ってるマウント→Javaで語るなマウントの連携は、本当に本当にうんざりです。 でも、まぁそういった人は内容読んでくれませんもんね。ブコメにまた湧いていました。

 それ以外のコレクションが欲しくなったときに、Goの組み込み型だけでは対応できません。代表的な例として、集合を表す Set が Javaではジェネリックなコレクションとして提供されていますが、現状のGoでそれを実現しようとすると、必然的に要素型は interface{} になってしまって非常に嬉しくありません。

GoではX型の集合は map[X]bool で書きます。

if m[x] { ... } // 要素が含まれているかのテスト
m[x] = true // 要素の追加
delete(m, x) // 要素の削除

で、もちろんそれ以外に組込型が対応していないデータ構造もあるのですが、(Pythonphpプログラマーなら同意してくれると思うのですが)それらを使う頻度はとてもとても少ないです。

さらに、文字列や整数に関するデータ構造だったらGenerics要らなくてstringや(効率は少し犠牲にして)int64に特化して実装してしまうこともできます。 逆に大量のデータを扱いたいならそもそもGo上の struct 型を直接入れるのではなくシリアライズしてオンメモリDBに入れることで、メモリ効率が良くなるだけでなく、(GoのGCは精密でバイト配列の中身はスキャンしないので)GCのオーバーヘッドも格段に減らしたりします。

それでもジェネリックなデータ構造がほしいなら、たしかに interface{} 経由になったり、コードジェネレータに頼ったりする事になるのですが、本当にごくごく稀で、また他の理由で interface{} に頼ったりコードジェネレータに頼ったりすることだってあります。 (protobufとか、Swagger等JSON APIスキーマ言語からの生成とか)

そういった理由で、実際にGoで開発している人は、ジェネリックなデータ構造が無いことを大きな不便とは感じにくいのです。

また、コレクションに対して map したり filter したりしてコレクション操作を楽に書けるようになているのが、今時の言語だと一般的だと思いますが(自分もそのように書くのに慣れてしまって、ループでコレクション処理を書かなければいけないのは苦痛です)、このようなジェネリックな map メソッドや filter メソッドを定義しようと思うと、ジェネリックな関数を定義する機能が必要になります。

これは、ジェネリック以前の文化的なところからのすれ違いです。

もし、Goが filter, map, reduce を多用するスタイルを推奨する言語であれば、ユーザー定義関数のジェネリクスを待たずとも、組み込み関数に追加することができますが、Goはそれをしていません。末尾最適化も実装していません。高階関数は多用するものの、短い関数を省略して書けるラムダ式のようなものは持っていません。Goは関数型スタイルの書き方を推奨しておらず、単に for ループを書こうという言語なのです。

これについては直接的なソースが見つからなかったのですが、間接的なものであれば。

なぜGo言語 (golang) はよい言語なのか・Goでプログラムを書くべき理由

「知的でないプログラマ」のためにデザインされているというのは指摘自体は正しいと思います。実際Rob PikeもFrom Parallel to Concurrentの20:40~で次のように述べています
The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
要するにGoogle社員のような、研究者ではなく大学を出たばかりでC++, Java, Pythonの経験しかない「素晴らしいプログラミング言語」を理解する能力が欠けているエンジニアでも簡単に理解し、大きなシステムを構築するのに使えるようにGo言語はデザインされているのです。

GitHub - robpike/filter: Simple apply/filter/reduce package.

Having written it a couple of years ago, I haven’t had occasion to use it once. Instead, I just use “for” loops. You shouldn’t use it either.

私も、JavaのStream APIC#LINQは確かにモダンでクールな書き方だと思いますが、単にループを書く書き方に比べて劇的に生産性があがるとは思えません。「Goに入ってはGoに従え」で、Goらしい書き方になれることをおすすめします。

たとえばコードベースの一部をScalaからGoへ移植したMovioの記事でも、

movio.co

Simpler code is more readable code

No map, no flatMap, no fold, no generics, no inheritance… Do we miss them? Perhaps we did, for about two weeks.

It’s hard to explain why it’s preferable to obtain expressiveness without actually ‘Go’ing through the experience yourself - pun intended. However, Russ Cox, Golang’s Tech Lead, does a good job of it in the “Go Balance” section of his 2015 keynote at GopherCon.

のように書かれています。


Goの利用シーンを限定するなら、そのような欠点は大した問題にはならないかもしれませんが、一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

どれくらい「大幅に狭め」ているかどうかはさておき、もともとGoは他の汎用言語が解決しない問題を解決するために生まれた言語であり、例えばランタイムがマルチスレッド上に goroutine を動かす前提なのでLinux上でsetuidできないなど、特徴的と言うか色々割り切った言語であって、すべての用途をカバーしようという言語ではありません。それがたまたま(ジェネリクスを多用しないなど)いくつかの条件で git-lfs などのツールの開発にもフィットしたというだけです。

なので、Goが今使われていない分野ではなく、Goが今使われている分野での問題を解決することを考えるべきだと思います。 実際にGoの開発者は、今後の言語機能の拡張を考えるにあたって、Goのユーザーからの体験レポートを求めています。

イディオム以前に、後者だと間違ったpop操作をする可能性がないのに対して、後者は間違った操作をする可能性があるので、面倒以外にもコストを払っていると思います。本来なら必要なかった、コードのコピペというコストを払っている、といってもいいです。

はい、「面倒」といってたのはむしろそっちの意味です。多少のタイプ数なんて最初から気にしていません。 Pop() くらいならまだマシなのですが、スライスの途中への要素の追加削除など、もうちょっと込み入ったイディオムは、組み込み関数を用意するほどではないけれどもスライス操作のジェネリックなライブラリがあると嬉しいと思います。

一方で、ほんの2,3行で書けるコードをライブラリに頼るのが常に善かというと、そうでもありません。

イディオムなら動作を完全に理解している基本操作の組み合わせなのに対し、ライブラリだと微妙に期待と違うことがあります。

標準ライブラリにピッタリのものがない場合、サードパーティライブラリに頼るなら、コードを確認し、メンテナの実績を確認し、さらにバージョンアップのたびにそのライブラリが悪意あるユーザーに乗っ取られている危険があるので差分を確認し、、、と、イディオムなら30秒で読み書きできたコードが、ライブラリだと数時間から長期的には数日ものメンテナンスコストにつながることだってあります。

Go Proverbs

A little copying is better than a little dependency.


これは私に対する反論ではなく、mizchiさんへの反論ですが、正直この辺は非常に(開発チームの言っていることには)懐疑的です。そもそも、Goが公式に初登場した時期(2009年頃?)でも、ジェネリクスを入れることについては検討中といってたわけで、それから8年経った今でも検討中と言われても一体いつ入れるつもりなんだというのは疑問に思います(その間、いくつものGoにジェネリクスを入れるプロポーザルが登場しているにもかかわらず)。

「頑なに要らないと言っている」と「いつ入れるつもりなんだ」には相当の乖離があると思うのですが。。。

「懐疑的」というのが「本当は入れるつもり無いんだろ」という意味であれば、「そもそも入れる必要があるかどうかはこれから考える」段階です。

上でも紹介した “Toward Go 2” を、 ymotongpoo さんが日本語訳してくださったのでそちらを参照してください。

www.ymotongpoo.com

Go 1がリリースされた後、私たちはGoが設計された本来の目的である、本番環境での利用に時間を費やす必要があることを認識していました。それから言語の変更そのものからあえて離れ、私たちのプロジェクトでのGoの利用や実装の改善へと注力する対象を移しました。私たちはGoをさまざまな新しいシステムにポートし、Goがより効率的に実行するようにパフォーマンスに致命的なあらゆる箇所を再実装しました。また、競合条件検出ツール(race detector)といった新しい重要なツールも追加しました。 いま、私たちはGoを大規模の本番環境に耐える品質のシステムで使う経験を5年重ねてきました。私たちはどのような機能がそのような環境に適合し、またどのような機能がそうでないかの知見をためてきました。 いまこそ、Goの進化と成長の次なる一歩をすすめ、Goの未来を計画するときです。
すべてのGoへの大きな変更は、Goが今日どのように人々に利用されているか、なぜ十分にうまくいっていないかを記載した1つ以上の体験レポートによって動機づけられるべきです。 疑う余地もなく大きな変更に対して、そのような体験レポートを見ることは多くなく、その中でも実例を伴ったものはほとんどありません。
たとえば、最近はジェネリクスについて調査しているのですが、私にはGoユーザーがジェネリクスを使って解決する必要がある詳細で具体的な問題がいまいちうまく想像できないのです。 結果として、ジェネリクスメソッド、つまりレシーバーとは区別されてパラメータ化されたメソッドをサポートするかといった設計に関する質問に答えることができません。 もし実世界での利用例が多くあれば、深刻なものを調査することによってこのような問題に答えることができるでしょう。

私は Go 1.2 から使っていますが、その頃からほとんど言語には手は加わっておらず、一方でツールチェインやランタイムの進化は凄まじいものがあります。

半年ごとのリリースが、いつも待ち遠しく、しかも一度バージョンを上げるともう1つ前のバージョンには戻りたくありません。 コンパイラが賢くなったり、プロファイラが使いやすくなったり、スタックトレースが読みやすくなったり、ランタイムやライブラリがより多くのCPUコアにうまくスケールするようになったり、GCの停止時間が数桁小さくなる事が、Goユーザーの実際の問題を大きく解決してきたのです。

Googleジェネリクスを検討するよりも優先したそれらの改善は、私にとっても確かにジェネリクスよりも優先する価値のあるものに感じられます。

Goを大して実用していないのに、Goユーザーにとって何が重要かを勝手に決めつけて、それを優先して実装しないことに文句を言うの、相当に失礼ですよ。もうやめませんか?

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、 本当にジェネリクスが無いことで困ってない。
これはソースが欲しいです。現状、Goの利用シーンはかなり広がっているのに、ジェネリクスが無いことで不便な場面で他の言語にスイッチするというコストを多くのユーザーは払っているのでしょうか?(便利ライブラリやフレームワークの有無が原因ならわかりますが)

データがないので完全に印象論ですが、PythonRubyPHPJavaに比べると、Goは第n言語 (n>1) とされることが多いと思います。

私はもちろん、日本の著名なGopherの方たち、大抵Goより先にいくつも言語を使い込んでいる方たちばかりです。そしてその方たちからも、早くジェネリクスを!という声より、別に無くても、という意見を目にすることが多いです。 (日本のGoユーザーからジェネリクスを望む声があまり聞こえないことには同意いただけますよね?)

そしてGoから他の言語へのスイッチの事例は少ないですが、Goが面倒な場面ではそもそも最初からGo以外を選ぶのではないでしょうか?

僕は性能が要らない手元で動かすスクリプトPythonで書きますし、Python自体を開発するときはCを使います。Unity関連でC#のコードを読むことはありますが、ジェネリクスを多用する必要がある静的言語のプログラムを書く必要に迫られることは最近無いですね。昔はプログラミングコンテストではC++使っていましたが、そういった分野ではGoはあまり良い選択肢ではないと思います。実際にGoはISUCONに優勝したことはあっても、ICFPCで優勝したことはありません。


最後になりますが、「Goが大好きでもっとGoをいろんな人に布教したくて、そのためにはジェネリクスがないのがネックになっている」ならまだ分からなくもないのですが、「Goを大して実用もしてないしGoのゴールも理解してない部外者が勝手にGoユーザーに必要なものを決めつけて、Goユーザーがそれを対して重要じゃないということに対して批判する」のは、ナンセンスで、気分悪く、本当にうんざりです。

自分の問題を解決するのに適した言語を選びたいのではなく、自分の好きな言語機能を普及しているすべての言語に押し付けたいのですか?自宅を訪問してくる宗教勧誘並に迷惑ですよ。


(追記)

一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

に関連して、この考えかたがGoの設計思想と正反対だということを示すいくつかの資料を教えていただいたので紹介しておきます。

Simplicity is Complicated

Rob Pike 氏のスライドです。前半だけで良いので読んで下さい。

多くの言語が似たような機能をお互いに追加して近づいているのに対して、 Rob Pike 氏は異なる問題には異なるツールがあるのがいいと考えています。

  • Go is different.
  • Go does not try to be like the other languages.
  • Go does not compete on features.

GopherCon 2015: Russ Cox - Keynote - YouTube

Go Balance が取捨選択の基準の参考になります。

https://talks.golang.org/2014/hellogophers.slide#64

  • focus on the original goals

Goはゴールを設定しそこにフォーカスしている言語です。なのでそれ以外のユーザーや用途を取り込むために、フォーカスしている領域で重要とされない機能を取り込むことは、Goにとっては良いことではありません。

Go にジェネリクスがなくても構わない人たちに対する批判について

なんども繰り返される話でうんざりなんだけど、繰り返されるたびに反論するのもアレなので、URL貼れるように記事にしておく。

頑なに要らないと言ってる人が具体的にどの発言のことを差してるのか分からないけど、コア開発者たちはツールチェインやランタイムの進化を優先していただけで頑なに拒否してたりはしません。今はツールチェインやランタイムが大分進化したから、Goの適用範囲を広げるためにジェネリクスを含めて機能追加も検討し始めようかっていうフェーズです。

あとどの言語にもちょっと公平的な見方ができなくなった痛いファンはいるもので、そういった人たちをいちいちあげつらってこういう言い方で失笑するのは、別に止めはしないけど自分の格を下げるだけだと思う。

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、本当にジェネリクスが無いことで困ってない。そういった意味で「別にGoにはジェネリクス要らない」「ジェネリクスより先にXXX欲しい」人たちを失笑するのは、やはり笑ってる方がバカっぽい。RubyC++も使える人が「別にRubyに静的型要らない」って言ってるのに対して「こいつ静的型の便利さ理解してないよwww」と言ってるのと同じ。

Goは特に今で言うマイクロサービス的なものを(色んな意味で)効率よく開発するために作られた言語で、DBとかJSONとかRedisとか扱ってHTTP API提供する簡単なサービスを書いてみたら分かると思うんだけど、本当にジェネリクスがなくて不便な場面ってメチャクチャ少ない。 interface {} (Javaでいう object) が出て来る場面って、ジェネリクスが無いせいで出て来ることは本当に稀。

JSONRDB使っていてコードジェネレータを使わない場面では interface{} がたくさん出て来るけど、それはジェネリクス持ってる言語もジェネリクスじゃなくてリフレクションで解決しててGoと同じ不便さを持ってるだろう。

log.Printf("%v %v %v", a, b,c) みたいな場合、可変長引数が interface {} だけど、それもあんまりジェネリクス関係ないし、しかも静解析ツールがフォーマット文字列と引数の整合性チェックしてくれるのでやっぱり困ってる実感ない。

唯一面倒、あるいは不格好だなぁと思うのはソートだ。ジェネリックなソート関数を使うのが面倒で、便利さのために sort.Ints() とか sort.Strings() とかを提供している*1のが不格好なんだけど、この面倒臭さも単純にジェネリクスが無いせいとはいうわけでもない。

例えば Javaジェネリクスがプリミティブ型にそのまま対応したとしても、プリミティブ型が .compareTo() を持ってないし演算子オーバーロードとか「この演算子を定義している型」を宣言する方法を持ってないから、ジェネリック関数に渡すには「値の比較方法」「値の交換方法」を教えてあげないといけなくて面倒だよね。

今のGoのソートの面倒さはそれの方に近くて、比較方法と交換方法をいちいち教えないといけない部分が面倒くさい。もちろんC++のテンプレートみたいなのもあるわけで、Goに上手くフィットするジェネリクスならこの問題も解決するはずだ。

でも、Goがジェネリクス持ってないことを非難するときにはJavaを「持ってる」側に分類しておいて、Javaレベルのジェネリクスはあんまり嬉しくない*2ことを説明すると「ジェネリクスJavaだけの機能じゃない。Javaと比較すんな」みたいな反応されるのにはうんざり。

これは2割くらいしか同意できない。

昔にJavaジェネリクスが無いことを擁護していたユーザーは、きっとC++のテンプレートしか知らなくて、しかも当時のC++コンパイルエラーは読めたものじゃなかったのだろう。あのコンパイルエラーと戦うよりは、 int x = (int)vec.get(i) と書くほうが生産性が高く感じる人が多くても不思議ではない。

一方で今のGoユーザーは、 int v = (int)v.get(i) なんてコードはほぼ書かない。昔のJavaと違って、動的配列もマッピング型もジェネリックな組み込み型だから。たまに書く場合も、JSONmap[string]interface{} で扱うとか、どんな型でも値として入れられる Context から値を取り出すとか、ジェネリックがあっても便利になるとは思えないケースがほとんどだ。

なので、アプリ開発言語としてメジャーなのがC++Javaしか無かった当時のJavaユーザーと、そもそもジェネリクスがなくて困るケースにあまり遭遇しない、そういうケースは別にGo以外でも他のメジャーな言語使えばいい今のGoユーザーでは、「ジェネリクスいらない」の意味が全く異なる。どちらにしろ、両者とも「横目により良いものを見ながらやせ我慢」してたわけじゃないので「酸っぱいぶどう」というのは違うと思う。

逆に2割の同意できる部分としては、今ジェネリクス要らないと思ってるGoユーザーも、(Javaほどではないとはいえ)実際に追加されたら便利だと思うだろう。

さっきも書いたとおり、Goユーザーがジェネリクスが無い不便さを実感しにくいのは int v = (int)v.get(i) のような、ジェネリクスがあればダウンキャストが要らなくなるコードにほとんど遭遇しないからだ。

だけど、実際にはジェネリクスなしで型安全を手に入れるために、「イディオム」というコストを払っている。例えばFIFOキューから値を取り出すなら、今は v := q[0]; q = q[1:] なのが、ジェネリクスがあれば v, ok := q.Pop() になるだろう。後者に慣れたら、きっと前者は面倒に感じるはずだ。

なので、Goにジェネリクスを足す理由が適用範囲を広げるためだとしても、今Goがフィットしてる分野にもちゃんとメリットはあると思う。

*1:ちゃんと確認してないので、単に便利さのためだけじゃなくて最適化のために専用のアルゴリズムが使われているかもしれない

*2:いちいちboxingするコスト、Javaユーザーは許容するかもしれないけど、高性能サーバーを手早く書きたいGoユーザーは許容しないだろう

Heroku 上での Python 3 率

requestsなどの作者として有名な、HerokuのKenneth Reitzさんが、とてもうれしいグラフを公開してくれていたのでシェアさせていただきます

デフォルトが Python 3 になったらみんな Python 3 使うんだよね。