Last Updated: 2/6/2024, 5:44:57 AM

# Rust なのか Go なのか

ひとりごと。色々、漁ってると、こんな雰囲気。




並列処理で
もっと高速に書きたい
→ Go

Go で大事なのは
→ goroutine



ガシガシ書き込んで
もっと高速に書きたい
→ Rust

Rust で大事なのは
→ 所有権

# 1. 概観

Go と Rsut は全く用途が違うそうです。なので、 自分の用途に合わせて選ぶことになるかなと思いますが...

Go は文法がとてもシンプルです。 誰が書いても同じようなコードになるようなことを意識しているのかなと思われます。 それが元でよく Go を貶す記事が見受けられます。

Rust はコードが複雑になっても、ガシガシ書き込むような感じらしいです。その分だけ Go よりも高速に動作します。

Rustと競合? - Goへの誤解について (opens new window)
似てるのは例外機構を捨てたこととバイナリがポータブルなことぐらい。 得意な用途は全く異なります。...

Go は非同期処理をシンプルに書きつつマルチコア性能を引き出すのが強みです。

Rust は徹底してオーバーヘッドを排除してシングルコアの限界性能を引き出せるのが強みです。... これらの強みが関係ない分野だけが競合しています(CLIツールなど)。

Rust と Go は競合しない (opens new window)
Rust and Go are not competitors

Go は、アプリケーションの厳格なシンプルさを通して、 規模の大きなチームが効率的にプログラミングできるようにすることを目標にしています。 ー Go は、シンプルでないもの、あるいは 非直交性 (opens new window) を招く恐れがあるアイディアを採用しません。
Go is focused on making large teams of programmers efficient, though rigid application of simplicity — ideas which are not simple or lead to non orthogonal corner cases are rejected.

Rust は、安全でないメモリ参照やランタイムオーバヘッドを許容せずにプログラミングできるようにすることを目標にしています (Go もまた安全でないメモリ参照を許容していません、新しいメモリ不安全な言語に需要があるとは思えません)。 ー Rust は安全でない、あるいはオーバーヘッドが生じるようなアイディアを採用しない か、 少なくともそのようなアイディアを言語の中心部に据えるようなことはしません。
Rust is focused on a class of programs which cannot tolerate unsafe memory access (neither can Go, I don’t think there is any appetite for a new unsafe programming languages) or runtime overhead — ideas which are unsafe, or come with overhead, are rejected or at least pushed out of the core language.

# 2. 用途

# ◯ Go

Go は Google で開発されました。

Go は、Google での問題を解決するために Google で開発されました。 その問題の大部分というのは、  ネットワークサービスにどのように対応するのか  です。
Go was created at Google to solve Google problems, which mostly involves dealing with networked services.
Why Go and not Rust? (opens new window)

Go の背景とかは以下のページによくまとめていただいている気がします。

いままでは1台のサーバで対処していました。簡単だったので。 これを「モノリシックアーキテクチャ」と呼ばれているのを目にします。 1台では負荷もそうだし、人間の役割分担も難しいので2台以上のサーバで対処するようになりました。 これを 「マイクロサービスアーキテクチャ」 (opens new window) と呼ばれているのを目にします。

マイクロサービスアーキテクチャのサーバ間通信で 「gRPC」 (opens new window) を使うみたいな記事をよく見かけます。

モノリシックの Rails のサービスを、徐々にマイクロサービス化 Go に置き換えて、 成功したもの、失敗したものが書かれていてとても勉強になります。理解はしていません。

Wantedly というサービスはもともと、一つの Ruby on Rails のサーバーで全てのサービスを作っていました。 したがって、全てのエンジニアは Rails を読めるし、全てのバックエンドエンジニアは Rails が書ける、という状態でした。 そういう環境で、なぜ Go を使うことに意味があったのでしょうか (適切な技術選定であるとは、そこに根拠があるということで、この文章はGoの導入を根拠付けるためのものです)。
Wantedly における Go 導入にまつわる技術背景- Qiita (opens new window)

Go は非同期処理が得意らしいです。 並列分散を土台に据えたブロックチェーンなどで採用されています。 対して Rust は非同期処理の導入に向けていくらか苦闘した形跡が見受けられます。

ブロックチェーンとかの分散処理系は Go が強いのかなと...

並列処理が得意という認識の中で Go が採用されたりするらしいです。

隠れマルコフモデルという統計機械翻訳のアルゴリズムを使ってC++で実装されていた機械翻訳エンジンを、 パフォーマンスを上げるために並列処理が得意なGo言語に書き換えるという仕事をいただきました。
サンフランシスコで創業したスタートアップを解散した話 - note (opens new window)

あと並列処理では無いですが (関係ありました) 仮想コンテナを作る Docker も Go で実装されているらしいです。

コラム: Docker と Go 言語 - Docker 実践活用ガイド (opens new window)

Go 言語の特徴としては、言語の名前にもなっている go ルーチンがあります。 go 関数名と書くだけで、その関数は平行に動作し、しかも非常に軽量です。 Docker では、コンテナやクライアントとの非同期なイベント処理も多くありますので、 go ルーチンにより記述できます。

Go 言語は、Docker でも必要なネットワークや OS に関連したライブラリが充実しているのも特徴です。 パッケージ管理システムがある点、実行ファイルも1つにまとまっており配布やテストが簡単な点や、 クロスプラットフォームでの開発が容易な点も特徴です。

ウェブに強いので、もともと Python で書いていたものを Go で書き直したみたいな記事も見受けられます。

今年の初め、我々は Stream の主要言語を Python から Go に切り替えました。
Why we switched from Python to Go (opens new window)

# ◯ Rust

マイクロソフトがシステムプログラミングに C, C++ の代わりに Rust を使うことを検討している様子があります。

Microsoft Security Response Center(MSRC)は2019年7月22日(米国時間)、 「C」や「C++」に代わる  システムプログラミング言語  には何がふさわしいのか、最有力の選択肢として「Rust」を挙げ、理由を解説した
Microsoft、安全で高効率のプログラミング言語として「Rust」を高く評価 (opens new window)

トリプレットは、  システムプログラミング  を「アプリではないもの」として広く定義しています。 システムプログラミングには、  BIOS、ファームウェア、ブートローダー、オペレーティングシステムカーネル、 組み込みおよび類似の低レベルコード、仮想マシンの実装などが含まれます。  トリプレットはまた、「単なるアプリ」以上のものであるため、ウェブブラウザをシステムソフトウェアとして数えています。 実際、「ウェブサイトとウェブアプリのプラットフォーム」です、と彼は言います。
Triplett broadly defines systems programming as “anything that isn’t an app.” It includes things like BIOS, firmware, boot loaders, operating systems kernels, embedded and similar types of low-level code, virtual machine implementations. Triplett also counts a web browser as a system software as it is more than “just an app,” they are actually “platforms for websites and web apps,” he says.
“Rust is the future of systems programming, C is the new Assembly”: Intel principal engineer, Josh Triplett (opens new window)

ちなみに Rust はマイクロソフトが作っているのではなく、Mozilla が中心になって作っています。

RustはMozillaとコミュニティによって作成された、オープンソースのプログラミング言語です。
Rust - MDN Web docs (opens new window)

WASM (opens new window) のようなクライアントでシングルに動かすような場合は、Rust が強そう。 megumin1 氏は、はてブ読んでると、とても怖そうな方ですが、 仰られてることはとても勉強になります。

Go はランタイムを肥大化させるデザインをとってしまったので、 C, C++, Rustに比べて wasm では根本的に不利。 確実に負債になるといわれていた Go の欠点が wasm ではっきりと表面化しているので、 あまり期待しないほうがよいです。 (どういうデザインなんだろう...)
megumin1 氏のコメント2018/04/16 15:32 (opens new window)

@mizchi 先生は、JavaScript 界隈でとても、とてもすごい人らしいです。 @mizchi 先生の Twitter を追っていると Rust に流れていきそうな雰囲気を感じます。

2019-10-27 での Rust の利用例が記載していただいてとても勉強になります。

# 3. 非同期

# ◯ Go

# 文法

いままでコールバック、改善されて async/await だったのが、それをさらに改良するような書き方ができるらしいです。 goroutine を使うと。イメージ的に コールバック > async/await > goroutine ということでしょうか。

Go の同時実行モデルは、複雑な結果の受け渡し組み立てに参加させることなく、 主に複数の独立した要求を処理する必要があるサーバー側アプリケーションに適しています。 これが、await ではなく go が与えられた理由の1つです。
Go’s concurrency model is a good fit for server-side applications that must primarily handle multiple independent requests, rather than participate in complex result-passing schemes. This is one reason why you’re given go and not await.
Why Go and not Rust? (opens new window)

Go が他の多くの言語での非同期プログラミングよりも優れている理由 - Qiita (opens new window)
Go の場合、アプリケーションは普通の同期プログラミングをするだけで、 非同期プログラミングのメリットを享受することができます。...
「他の多くの言語」とは、 Python (asyncio), node.js, C# などを想定しています。 Erlang や GHC なんかは Go に近いかも知れません。 async / await がない言語では、「コールバック地獄」や「deferred地獄」のような問題もありますが この記事では扱っていません。 async / await のメリットを解説した他の記事を参照してください。

JavaScript の記事になりますが、コールバック > then チェーン > async/await の変遷が、記述されています。 XMLHttpRequest はコールバックで非同期を処理を実装しています。 まず、コールバックの悲惨な例を示しています。 次に、Promise でラップして then チェーンに書き直しています。 最後に、async/await に書き直すと言う3ステップを踏んでくれています。

ちなみに XMLHttpRequest, fetchapi, axios の 3 種類を見かけます。 それぞれの立ち位置とかは、全くわかっていません。

# 性能

性能というと表現がおかしいのですが、「プロセス」、プロセスの軽量版として「スレッド」があり、 async/await あるいは goroutine は、スレッドよりもさらに軽量なもの「コルーチン」で対処するそうです。

「コルーチン」とかっこよく書きましたが Python で言う所の「ジェネレータ関数」です。 関数、サブルーチンは1度起動したら途中で処理を止めずに最後 return まで動作します。 対して、ジェネレータ関数、コルーチンは、中断と再開を yield を起点にして繰り返します。 これが非同期関数 async なら await を起点にします。

プロセス、スレッドは、OS の機能です。 コルーチンは、プログラミング言語の機能です。 「コルーチン」は、より上のレイヤーで対応するから軽い、みたいなそんなニュアンスのようです。

そういった性能面での変遷については、以下の本にいくらか書かれていました。 当該箇所は 1.1. 節 「リクエスト処理ループの数を増やす」で、 15P ないくらいなのですが、端的によくまとめていただけているような気がします。 お礼広告です。ググラビリティも低かったので。理解はしていません。

# ◯ Rust

最近 async, await が来たらしいです。

Big news! As of this writing, syntactic support for async-await is available in the Rust beta channel! It will be available in the 1.39 release, which is expected to be released on November 7th, 2019.
Async-await hits beta! - Rust Blog (opens new window)

Before delving into the current status, it’s worth taking a moment to recap the core motivations for async/await, and its special importance for Rust.
Async in Rust, circa 2018 (opens new window)

Rust は過去に非同期処理に対して苦闘した形跡が見られます。

Rustは昨年に非同期サポートを標準から切り離しました。事実上システム記述用途に特化していく模様です。
Rustと競合? - Goへの誤解について (opens new window)

非同期が苦手だからマルチコアも苦手なのかなって思ってたのですが、 その理解は間違っていました。Rust は、どういうアプローチをとっているんだろう...。 ご指摘いただき、誠にありがとうございます。 せっかく、ご指摘いただいたのですが、こちらもまだ理解しきれていません。

# 4. 速度

速度は Rust の方が、出るみたいです。 Go の方が言語的な機能を削っているから、速度が出るのかなと思っていたのですが、違いました。 Rust の方が、ガリガリに書き込むようなイメージです。

Benchmark of Rust and Go

議論の余地は全く無い: Rust は Go より速い。 上記のベンチマークでは Rust は Go より速かった、 いくつかのケースでは段違いに速かった。
There really isn’t any argument: Rust is faster than Go. In the benchmarks above, Rust was faster, and in some cases, an order of magnitude faster.
The Computer Language Benchmarks Game (opens new window)

今後、Go言語がC/C++の役割を置き換えられるかというと、筆者はそうは思いません。 Go言語はスクリプト系言語よりは遥かに高速であるとはいえ、 C/C++と比べると性能は落ちますし、バイナリサイズもかなり大きくなります。
Goで覗くシステムプログラミングの世界 (opens new window)

Denoは当初はGoで実装され、特権側 (Goとシステムコールアクセス) と非特権側 (V8) の間のシリアライズにProtocol Buffersを使用していた[8]。しかしながら、実行時間が2倍になることとガベージコレクションの圧力から、コードは直ぐにRustで再実装された[9]。
Deno - Wikipedia (opens new window)

C と Rust を比べると若干 C の方が速いらしいです。リンク先にグラフがあります。

「Rust版がC版より遅いのは、当然ながらRustの安全機能に起因する。だが、それを定量化できるのだろうか」
「Rust」言語はCよりも遅いのか、研究者がベンチマーク結果を解説 (opens new window)

Rust のオーバーヘッドが生じるようなアイディアを採用しない 例を2つ見つけました。

# 1. 代入を制限する。

Rust は代入、再代入を厳しく管理しなければなりません。 Rust は、ガベレージコレクションでのゼロコストを狙っているためです。 ガベレージコレクションとは使わなくなったオブジェクトを破棄する機能です。

Go は使わなくなったオブジェクトを自動的に破棄してくれますが、この処理が意外と重いというわけです。 以下の記事は後半は門外漢には厳しいですが、前半は割と読めます。

RustはGCなし(ゼロコスト)でリソースを自動的に解放する仕組みを持っていて、それを実現する概念が所有権です。
Rustの所有権に親しむ - Qiita (opens new window)

全く詳しいことはわかりませんが、 副次的な効果として副作用の範囲も限定的にしているという、 なかなか素敵な実装になっているらしいです。

# 2. 例外が無い。

Python から来た方に向けて言えば、try 文を使わず if 文で例外を処理します。 Java から来た方に向けて言えば、 例外を投げずに Optional を返します。

Optional は検査例外と同じ考えです(Item 71)、 Optinal を使えば、値が何も返されないかもしれないという事実を API に直視させます。
項目 55: Optional を慎重に返す - Effective Java
Optional are similar in sprit to checked exceptions(Item 71), in taht they force the ensure of an API to confront the fact that there may be no balue returned.
Item 55: Return optional judicitously - Effective Java

if 文の例えよりも Optional の方が、より近いかなと思います。 Python で型アノテーションを使えば Optional を使えます。

以下の文章を読んで try 文による例外処理は、きっと重いんだろうなくらいの理解をしています。

Rust におけるエラー処理 - Hacker News (opens new window)
Error handling in Rust - Hacker News

例外を使うことが許されない状況下で Rust を使わなければならない人がいる。 (なぜなら unwind table と cleanup code が大きすぎるからである)。 そのような人々は、実質的に全てのブラウザベンダやゲーム開発者が含まれる。
Some people need to use Rust in places where exceptions aren't allowed (because the unwind tables and cleanup code are too big). Those people include virtually all browser vendors and game developers.

さらには、例外は、この厄介な codegen tradeoff があります。 ソフトウェアを zero-cost にするか(例えば典型的に C++, Obj-C そして Swift コンパイラがそのように動作します)、 zero-cost の場合、例外を投げることは重たい処理になります。 さもなくばソフトウェアを non-zero-cost にします(例えば Java HotSpot や Go 6g/8g がそのように動作します)、 non-zero-cost の場合、たとえ例外が投げられなかったとしても、 各 try ブロックに対してパフォーマンスペナルティを喰らいます(Go の defer においては)。 RAII (opens new window) を持つ言語に対しては、デストラクタを伴う各 single stack object は 暗黙的な try ブロックを生成します、そのため例外は実質的に実用的ではありません。
Furthermore, exceptions have this nasty codegen tradeoff. Either you make them zero-cost (as C++, Obj-C, and Swift compilers typically do), in which case throwing an exception is very expensive at runtime, or you make them non-zero-cost (as Java HotSpot and Go 6g/8g do), in which case you eat a performance penalty for every single try block (in Go, defer) even if no exception is thrown. For a language with RAII, every single stack object with a destructor forms an implicit try block, so this is impractical in practice.

zero-cost 例外によるパフォーマンスへのオーバーヘッドは、理論的な問題というわけではありません。 私は、起動時に数千に及ぶ例外を投げていたために、 zero-cost 例外が使われた GCJ でコンパイルされた Eclipse が起動に 30 秒も要したのを覚えています。
The performance overhead of zero-cost exceptions is not a theoretical issue. I remember stories of Eclipse taking 30 seconds to start up when compiled with GCJ (which used zero-cost exceptions) because it throws thousands of exceptions while starting.

エラーが生じたときの経路と成功したときの経路の両方について考えたとき、 C 言語のエラー処理に対するアプローチは、例外に比べて素晴らしいパフォーマンスと code size story を持っています、 このことは C 言語のエラー処理に対するアプローチがシステムコードに圧倒的に適していることを示しています。 しかしながら C 言語のエラー処理に対するアプローチは人間工学に反しかつ安全ではありません、 Rust は Result でこれに対応します。 Rust のアプーロチは C のエラーハンドリングのパフォーマンスを獲得しつつ、 C 言語のエラーハンドリングが持つ煩雑さを取り除いています。
The C approach to error handling has a great performance and code size story relative to exceptions when you consider both the error and success paths, which is why systems code overwhelmingly prefers it. It has poor ergonomics and safety, however, which Rust addresses with Result. Rust's approach forms a hybrid that's designed to achieve the performance of C error handling while eliminating its gotchas.

しかしながら C 言語のエラー処理に対するアプローチは人間工学に反しかつ安全ではありません というのは、おそらく null 安全について言及しています。

# Go の例外

Go にも、例外はありません。 Rust が1つの変数に「正常な値」か「例外」を詰め込むのに対して、 Go は2つの変数、「正常な値のための変数」と「例外のための変数」を用意して対応します。

ただ実際のところ Go も Rust も panic という例外と似たような機能を持っているらしいです。 Rust も Go も panic は基本使わない方向らしいですが。

ただ、上記引用した英語の記事によると、Go の場合は、 たとえ例外が投げられなかったとしても、各 try ブロックに対してパフォーマンスペナルティを喰らいます(Go の defer においては)。 とのことです。これがオーバーヘッドになってしまうそうです。

Python の例外が「ズドンと爆発」して try 文で捕まえるイメージなのに対して、 Go の 「panic を起こすと巻き戻」って、それを recover で回収するイメージです。 The Go Blog を翻訳しました。

Rust の方の panic は、全くよくわかっていません。

# 例外は不要なのか?

Go にも Rust にも例外はありません。 では例外は不要なのでしょうか。そうでは無いと思います。 例外には、欠点もあれば長所もあります。 以下の記事で例外を使ってコードが綺麗になる例を示してくれています。 本文は読まないでコードだけ流し読みしました。

Rust に例外が無いのは、ゼロコストを狙っているからです。 Go に例外が無いのは、例外で書かせるのか、それとも if 文で書かせるのか、 という判断をプログラマにさせて疲れさせないためだと思っています。

以下、再度引用します。

Rustと競合? - Goへの誤解について (opens new window)
似てるのは例外機構を捨てたこととバイナリがポータブルなことぐらい。 得意な用途は全く異なります。...

Go は非同期処理をシンプルに書きつつマルチコア性能を引き出すのが強みです。

Rust は徹底してオーバーヘッドを排除してシングルコアの限界性能を引き出せるのが強みです。... これらの強みが関係ない分野だけが競合しています(CLIツールなど)。

# 5. 人気

技術的なトレンドとかはよくわからない。Go は登場してから Rust に比べれば急激に人気を増やしたような気配がある。 後発の Rust はじっくり、じんわりな感じだ。

Trend of Rust and Go for years

Go が一気に Rust を突き放して駆逐しそうに見えるけど、ここ1年に限定すれば Rust と Go は横ばい。 数字だけ見ると、このまま安定してお互い共存しそうな気配がある。

Trend of Rust and Go for a year

Rust はとても素晴らしい言語だと聞くのですが、 結構、難易度が高いらしく、使おうと思う人が少ないのかなと感じます。 あまりよくはわからないのですが...。

技術的には、優れているものよりも簡単なものが、受け入れられやすい気がします。 例えば、通信の規格でガチガチな ATM, SONET/SDH がなくなって、 ベストフォワードでとりあえず送る Ethernet が受け入れられました。

それでもプログラミング言語は、別にインフラではないですから、どちらかが一方を駆逐するということはないかなと思います。 適材適所で Rust が適している箇所は Rust が Go が適している箇所は Go が使われ、 どちらでもいい場合は、好き方を Go を使ったりしてるのかなと思ったり思わなかったりします。

難易度の高いものは、大抵破綻しやすい気がするのですが、 その難易度の高いものを綺麗にまとめ上げたという噂を聞く Rust に、どこか魅力を感じたりもします。

# 6. 文法

# ◯ ジェネリクス

WARNING

Go にジェネリクスが採用されました。ジェネリクスが採用される以前に書かれた節です。 導入の背景等は以下の記事に書かれているようです。




よく議論をされているのを見かけるのが、ジェネリクスがないことに対する指摘。 ジェネリクスとか、正直、全然よくわかっていないです。

ちなみに Google の Python のコーディング規約では、 単純な場合にだけジェネレータ式、言い換えれば map, filter を使うことを指示しています。

単純なら使ってもいいよ!
Okay to use for simple cases.
2.7 Comprehensions & Generator Expressions (opens new window)

Go では map, filter を使うか for 文で書くのか考えず、 男は黙って for 文で書くというのを意図しているのかなと思ったりします。

いや、むしろ何でもかんでもループで書くから、 「ちょうどいい関数が無い時」に書き方を変える必要すらないんですよ。 golang と Generics と吾 - Qiita (opens new window)

Goの単純さのために、コードのコピー/ペーストがさらに必要になる場合があります。 たとえば、Go 1.13の時点では、Go はGenerics をサポートしていません。 つまり、特定のシナリオではコードのコピー/ペーストが必要になる場合があります。 ここでは、Generics のような機能を失うことにより、 シンプルさとパフォーマンスの向上を好みます。 言語を理解することは、それを使用してソフトウェアシステムを作成する前に非常に重要です。
Because of the simplicity of Go, sometimes, you may need more copy/paste of code. For example, as of Go 1.13, Go doesn’t support Generics, which means that you may need some copy/paste of code in some specific scenarios. Here you prefer simplicity and better performance by losing a feature like Generics. Understanding a language is very important before you write software systems with it.
The Java Baggage: Biggest Bottleneck to Adopt Go in Enterprises (opens new window)

ただ、この男は黙って for 文というアプローチは、 バックエンドでは良さそうですが、 フロントエンドや、スマホのアプリの開発とは相性が良くなさそうな気配を感じます。

RedMonkプログラミング言語ランキング (opens new window)
そういったなかには、バックエンドシステム言語として急上昇していたGoがほぼ横ばいになっていることも含まれている。 Goは2015年6月期のランキングから15位を保っていたが、 今回TypeScriptが躍進した結果、初めて順位を落として16位となった。 O'Grady氏はGoがバックエンドの言語としての役割を超え、 例えば Java のような言語ほど多くの用途に使われるようになるとは考えていない。

なぜ「多くの用途に使われるようになるとは考えていない」のか、 教えて欲しいです...

ジェネリクスに対する優先順位は低そうです。 なんで低いのか、Go の文法に対する設計背景みたいなのを探したのですが、 まだ見つけられずにいます。

どこかの時点で、ジェネリックは追加されることでしょう。 我々は、ジェネリックを急いで実装する必要はないと思っていますが、 一部のプログラマにとって必要であることは理解しています。
Generic型がない理由は? (opens new window)

ただ Go をのぞいていると、 色々な書き方をできないようにしている雰囲気を感じます。 しかし、ここでも megumin1 氏はキレッキレ...

この筆者に限った話ではないですけど、 ジェネリクスについて語るときに「Javaのジェネリクスもどき」という 極めて不適切な例を出しそれ以外の例を出せない人は、 ジェネリクスのことを全く理解していないど素人です。
megumin1 氏のコメント (opens new window)

# ◯ 多値

Python は tuple にするのかそれともユーザ定義クラスにするのか、というところで悩んだりします。 Python の tuple は、元々は ABC 言語の Compounds 型に由来しています。 tuple はデータ型の一種です。

以下の方が言う Go の多値周りが扱いにくいというのは、 そういったことを考える煩雑さから解放してくれる Go の長所だと、個人的に思います。 この方の記事を全部読んだわけではないのですが...

Go言語には多値という機能*1が言語に組み込まれています。 しかし、これが中途半端なうえ、 多値っぽい構文で多値ではないものがいくつかあるため、初心者を混乱させる原因になっています。
Go言語のイケてない部分 (opens new window)

# ◯ 抽象化

Go は色々機能を削って作られた言語らしい。 Google はサービスの機能を意図的に削除したりする傾向にある。 GAE は JOIN を削ったし、Chrome は綺麗さっぱりいろんなボタンをブラウザから削った、 検索サイトのトップ画面も Yahoo! やら msn とは違う、 検索欄だけのページを設計したりした。*1

リファクタリングしていて関数やらコードを削れたときは、結構気持ちいいものだし、 誰が書いても同じようなコードになるというもちろんそれはすごく惹かれる..

けど、やや辛い時がどうしても生じてくる気がする.. 初期の GAE みたいに。Python から Go に書き直すみたいに、 ある程度先が、開発するものの全体像が見えていれば採用もできるかもしれないけど。

Go はいろんな機能が削られているので、たまに "物凄い" 貶され方をしたりするのを、たまに見かける。

Go言語がダメな理由 - POSTD (opens new window)

  • Goは何も新しいことをしない。
  • Goは全てにおいてうまく設計されているとは言えない。
  • Goは他のプログラミング言語から退化したものである。

こんな "物凄い" 貶され方をする理由は、 その人たちのアイデンティティを挑発してるからっていうのは、面白かった。

[翻訳]なんでGoってみんなに嫌われてるの? - Qiita (opens new window)
それって、Goのシンプルな言語哲学が、ML系言語好きのアイデンティティを挑発しちゃってるからじゃないの?

ただ、高機能で抽象化して簡潔にできることは、必ずしもいいことだけではない。 色々な書き方ができてしまうと言うのは、それなりに弊害もあったりする。

まずコードの可読性を最適化しよう - POSTD (opens new window)
コードが理解しづらい
  4. コメントなしに低レベルの最適化が施されている
  5. コードが賢すぎる
コードが追いかけづらい
  4. 全てが抽象化されすぎている

Google Python スタイルガイド (opens new window)
強力な機能
このような機能は避ける。

定義:
Python はとても柔軟な言語であり、 メタクラス、バイトコードへのアクセス、高速コンパイル、動的な継承、オブジェクトの親の変更、インポートハック、リフレクション、内部システムの変更 など多くの素敵 (変態的) な機能があります。

例えば Python は、「インデントを強制して誰が書いても同じような」と主張されてるよりは、 意外といろんな書き方ができる。もちろん、どんな言語だって書こうと思えばいろんな書き方ができる。 「同じような」なんて言うこと自体若干怪しい。

ただ、意外といろんな書き方ができる一つの要因としては Python が高機能であることが挙げられると思う。 これらの機能を使うことで、抽象化を上げて、より簡潔な記述で記載することができる。 反面、その機能を使うか使わないかで、書き方に幅が出てくる。

実直に and で全部書き上げてしまのかとか、それとも all 関数で条件文を書くのか。 実直に 関数で処理するのか、それともメソッドで処理するのか。

# 
# 矩形の等号 == を表現する書き方について考えます。
#

def sample_code():
    # 書き方1 実直に..
    assert eq(Rectangle(1, 1, 2, 2), Rectangle(1, 1, 2, 2))
    
    # 書き方2 Python の機能をフルに使って..
    assert Rectangle(1, 1, 2, 2) == Rectangle(1, 1, 2, 2)

#
# こんな矩形のクラス
#
class Rectangle:
    def __init__(self, x1, y1, x2, y2):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2


#
# 書き方1 実直に..
#
def eq(rectangle_a, rectangle_b):
    return \
        rectangle_a.x1 == rectangle_b.x1 and \
        rectangle_a.y1 == rectangle_b.y1 and \
        rectangle_a.x2 == rectangle_b.x2 and \
        rectangle_a.y2 == rectangle_b.y2


#
# 書き方2 Python の機能をフルに使って..
#     1. 演算子オーバロード __eq__
#         all 関数, zip 関数, ジェネレータ式
#     2. イテレータ __iter__
#         yiled 文
#     3. メソッドの動的な追加
#
def __eq__(rectangle_a, rectangle_b):
    return all(a == b for a, b
                    in zip(rectangle_a, rectangle_b))


def __iter__(rectangle):
    yield rectangle.x1
    yield rectangle.y1
    yield rectangle.x2
    yield rectangle.y2


Rectangle.__eq__ = __eq__
Rectangle.__iter__ = __iter__


#
# 実行する。
#
if __name__ == '__main__':
    sample_code()

書き方2は、本来 iterable でないものを無理やり iterable にして抽象化の度合いを上げてるので読み辛い。 ここでは可読性は置いておいて、書き方に幅があることだけを示しました。 適切に抽象化したものは、表現も簡潔になり可読性も上がる。 yield 文, generator なんかも、うまく使えばコード数を半分以下にすることができる。

Go は、こういった抽象化の度合いを悩まなくて済みそうです。

# ◯ 間違った高速化

狂気の沙汰だけど Python でも実行速度を求め出すと、 稀に低レベルに記述したりし出したりする。関数呼び出しのオーバーヘッドを削るためにベタがきするとか。

$ # 関数を使ったコード
$ python -m timeit "max(2, 1)"
1000000 loops, best of 3: 0.209 usec per loop
$
$ # ベタ書きのコード
$ python -m timeit "2 if 2 > 1 else 1"
10000000 loops, best of 3: 0.0395 usec per loop
$ # 関数を使ったコード
$ python -m timeit -s "import math" "math.floor(4 / 3)"
10000000 loops, best of 3: 0.169 usec per loop
$
$ # ベタ書きのコード
$ python -m timeit "4 // 3"
100000000 loops, best of 3: 0.0132 usec per loop
$ # 書き方1 実直に..
$ python -m timeit -s "from rectangle import Rectangle,eq" "eq(Rectangle(1, 1, 2, 2),Rectangle(1, 1, 2, 2))"
1000000 loops, best of 3: 1.94 usec per loop
$
$ # 書き方2 Python の機能をフルに使って..
$ python -m timeit -s "from rectangle import Rectangle,eq" "Rectangle(1, 1, 2, 2)==Rectangle(1, 1, 2, 2)"
100000 loops, best of 3: 4.33 usec per loop
$

処理を速くしようとして、こんなこととか、 for 文で書こうかなとか while 文で書こうかなとか、 そんなことを気にしだしたらいっそ思いっきり Java などの静的言語に切り替えを検討する時だと思う、あるいは Rust か Go か。

なぜなら、抽象化の一貫性を犠牲にしてまで工夫しても、 部分的に見れば何倍も速くできるけど、全体から見れば高が知れていて数%も速くできるかどうか..。 でも、言語を切り替えれば何倍も速くなる。

Python は、適切に抽象化具合をあげて短く書くことを前提にしている。 何故なら、Python がそういった機能を提供しているし、 PEP 8 - Style Guide for Python Code でも 1 行最大 79 文字とごく短く制限されているから。

低レベルに書き込むことは、PEP 20 - The Zen of Python の言う、 たったひとつだけあるはずのストレートなやり方ではないと思う。

何かをやるには、ストレートなやり方がひとつ、たったひとつだけあるはず
There should be one-- and preferably only one --obvious way to do it.
Python にまつわるアイデア: PEP 20 | Life with Python (opens new window)

Go のリスクは、抽象化ができ無いことが原因で可読性を欠いたコードになってしまう可能性があること。 それに耐えられなくなるったある日、突然 Rust の光を浴びてしまうのかもしれない。

私は何かを書くたびに古くて濁ったプログラミング知識の膜が自分の目からキレイに拭き取られていくのを感じました。 その光を見てしまったらこの約束の地に降り立ってしまったらもう引き返すことはできません。
Rust と DNS の1年 - POSTD (opens new window)

Rust のリスクは、抽象化の一貫性にばらつきが生じてしまう可能性があること。 ちなみに Rust は抽象化のコストも0コストらしいです。 Python は、抽象化するといくらかオーバーヘッドがあります。 Python はもともとが遅いので、あまり気にはなりませんが。

# 7. Ruby

ただ、学習するっていう意味で言えば、Go と Python という組み合わせは、両極にあっていいのかもしれない..。 Go と Python で切り替えて気分転換しながらみたいな。 人間、抽象具合をあげて簡潔に書きたい時もあれば、 抽象化しないでガリガリ低レベルに書きたい時もある。

でも、もっと Go と対極にあるのは Ruby だと思う..。

昔、Python と Ruby を比較して Ruby は、 自由に書けるから遊びやって just for fun や見たいな感じで YouTube でディスってる人がいて、炎上してる動画を見たことがある。

でも、楽しいってすごく大事だと思う。もし気の合う人とだけで少人数でプログラミングができるなら、 きっと Ruby は楽しい言語だと思う。 Rails が出てきて Web サービス組むことに対して Python に比して1日の長が得られたのもそういった所にあるのかなと思う。

Python の framework を探してたときに、ほんの少しだけ CakePHP をかじってたから、 当然、インデントを強制して誰が書いても同じようにと歌っている Python の framework も当然、設定より規約的なものだろうと思っていたけど、 現実はそうではなかった。

Django は大きすぎて触ってて違和感があって。色々と自由に作れるように、ちゃんと設定してねっていう感じ。 でも、そこまで作り込むなら Java なりなんなりに逃げると思うし。 別にそんなに大きいもの作らなくていいから、程よく小さいものが作れるのが欲しかった。

だから Rails どうなんだろうって思ってコード見たときは、Rails の光が差し込んできて慌ててドアを閉めた記憶がある。 自分の人間的な余裕がなくて Ruby を触れないから..

でも、面白いのは Ruby は自由にプログラミンを楽しもうって言ってたのに、出てきた framework は設定より規約で、 反対に Python はそうではない。Django がデカすぎるからって出てきた Flask とかは、 今度は小さすぎて、自分で好きなものをツギハギして自由に書いてねって感じになっていて、そうじゃないって思った。

お互い歩み寄ってるような感じがして、心地よくプログラミングができるって言うのは、単純にこっちがいいとは言えず、 繊細さがあるのかもしれない。極端に規約とかに縛られるのが好きな自分は、やっぱりキチガイなのかもしれない。

プログラミング言語に限らず、自然言語も楽しさとかを元に変化したりする。例えば黒人英語とかはその最たるものだと思う。 文法の一貫性よりも音感の良さを取り入れて変化してる*2

さらに言えば人は使う言語によって、思考に影響を受けたりもするらしい。難しくてよくわからない。

日本語も主語とか目的語を省略したりするから、黒人英語が音感を大事にするのとは違うけど、 相手とのなんとなくのフィーリングを共有する、会話そのものを楽しむ言語だと思う。 そう言う意味ではプログラミングそのものを楽しむって言う Ruby は、どこか日本語的な言語だなと思ったりする...








*1 SNS系のシステムについては、余分な機能を削り必須の機能に絞ることが、 必ずしもいい結果をもたらすとは限らない。蛇足と思われる機能を付け加えたり(mixi の足跡)、 あるいは本来必要なはずの機能を削ったりして(Twitter の文字数の制限, 増田の匿名)、 コミュニケーションの活性化を測ることができる。SNS では、 何をコミュニケーションするかよりも、どうコミュニケーションをするかの方が重要なのかもしれない。 Google が何故か SNS 系のサービスだと苦戦する傾向があるのは (Blogger, Google+)、 こういったところにあるのかなと思ったりする。

*2 全然話は違うけど中国語の動詞には過去形がなかったりする(時制, テンス)。 中国 4,000 年の歴史とは一体何だったのか。かわりに、昨日(昨天)とか時間を表す単語で過去を表現したりする。 でも、過去形ではないけど、完了形(了) は存在する(相, アスペクト)。単純に動詞に活用がない孤立語だからという話でもあるけれど。 相があるなら時制だって補語、助動詞として存在していいはずだと思うけど。 同じ孤立語でも、チベット語やタイ語には、それに近いものがあるらしい。 それが無いということは、中国の人たちは、いまという歴史の中に生きているのかなと思ったりする。