コード
目的
ポストC言語の比較をしたい。
基準
処理速度や実行ファイルのサイズ、メモリ管理はこの際、置いておく。プログラマとして書きやすい言語はどれか。これを知りたい。
対象
- golang
- rust
- nim
- vlang
golang
令和のC言語と言っても過言でない、泥臭い言語規約
言語規約が古臭い。これは長所でもあり、短所でもあると思う。私は元組み込み屋で長らくアセンブラとC言語を使用してきた。その私でもgolangは使いやすく、すぐに馴染める。
func (c *Cotoha) post(url string, header map[string]string, param map[string]string) (result []byte, err error) {
jsonData, err := json.Marshal(param)
if err != nil {
return nil, fmt.Errorf("post:%s", err.Error())
}
req, err := http.NewRequest(
http.MethodPost,
url,
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, fmt.Errorf("post:%s", err.Error())
}
for k, v := range header {
req.Header.Set(k, v)
}
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("post:%s", err.Error())
}
defer res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 {
return nil, fmt.Errorf("post:status is bad:%d", res.StatusCode)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("post:%s", err.Error())
}
return body, nil
}
例えば、上記はgolangのpost処理を関数化したもの。見ての通り、エラーハンドリングが異常に多い。post処理を都度書くのではなく関数化したのも、エラーハンドリングのせいで行数が多くなりすぎたからである。このエラーハンドリングには強い既視感を抱く。
この記法はC言語と全く同じである。C言語では返り値にステータスコードを入れ、引数にポインタを入れて中身を取り出す。これがgolangでは返り値①に中身を入れて、返り値②にステータスコードを入れている。後述する言語ではResult型など様々な工夫もあるが、golangは良くも悪くも変わっていない。
「golangは大嫌い」という人が世の中にはいるが。たいていの人はこの古臭すぎる言語規約に言及している。その気持ちもわからなくはないが、それは新言語に適応できかねるエンジニアをすくい上げる要素でもあると私は思う。
病的なほどlintがしっかりしている
golangはlintがしっかりしている。ちょっとした表記ゆれも決して許さない。改行位置もスペースの数もコメントの配置も、逐一チェックしてくれる。ctrl+sを押すだけで勝手に整形してくれるばかりか、コメントに至っては延々と波線を出して修正するまで文句を言い続ける。
これも「golangは大嫌い」な人にとっては「自由がない」として批判の対象になっている。開発中は過去のプロジェクトやサンプルからコピペして、現状に合わせて修正していくということが得てしてあると思うが。仮で書いているだけなのに、延々と記法を直せと要求してくるのだから、いら立つ気持ちもわからなくはない。
総評:開発現場の大半が馬鹿であることを確信した言語
開発者の大半は馬鹿である。特に大規模なプロジェクトになるほど様々な出自のエンジニアが集まるため、結果的に、馬鹿ばかりになる。単純に能力が低いというだけでなく、互いの流儀を調整するということが困難だからだ。インデントの数だとかifの後ろにスペースを入れるか入れないか、switchを使うか使わないか、みたいな一個ずつは細かい違いが。累積すると大きな負債になる。そして得てして人間は現場ごとに器用にルールなど変えられないから、馬鹿になる。
この問題に最も直面してきたのはC言語だろう。だから、それを克服するためにgolangは設計されているように、私には見える。大規模なプロジェクトに採用された際に、どんな出自の人間でも馴染めるよう、いわば最低限の現代化で済ませている。コーディングルールなど有名無実化することを見越し、lintで事細かにお仕着せする。
開発は自由を持って創造的に仕事をするべきだ、と考えるエンジニアは多い。それは理想の一つではあると思うが。炎上した金融案件だとか、大規模な泥沼案件だとか、入れ替わり立ち替わり、色んな派遣から寄ってたかって人が集まって、とにかく仕事をする、という現場も世の中にはある。
創造性ではなく、這うように一歩ずつでも進まないといけない。そういった現場が現実には存在するわけで、golangは地獄向きの言語だ、と思う。そんなものない方がいいわけだが、あったら感謝すべきだろう。
rust
メモリ管理に起因する独特の記法
rustには所有権という概念が存在する。
C言語ではメモリ管理は開発者が手動で管理していた。使用前にメモリを確保し、関数などにポインタを引き渡し、受け取り、使用済みになったら解放する。特に確保と解放は必ずセットになっていなければならず、これを忘れるとシステム全体を再起動するまでメモリが確保されたままになる。
これは言うまでもなく危険であるということでGCという仕組みが導入される。細かいところまで把握していないが、ようするにメモリ確保をラッパーして、参照回数などを調査し、不要になった段階で自動的にメモリ解放する仕組みである。いくつか手法が存在するが、循環参照していると解放されなかったり、安全性の確認が取れるまでメモリ解放が先延ばしになったりするため、効率面や速度面に課題があった。
というところで、rustの所有権という概念が出てくる。私も理解しきれていないが。ある関数内で確保されたメモリは別の関数に持ち越さない、というルールを前提としている。その上で、別の関数にポインタを渡す際には「その関数用として別ポインタを作成して、値を返す際に解放する」か「所有権そのものを引き渡す」といった処理をすることで、常にメモリをクリーンに管理するという機能である、らしい。
この機能を使いこなすためには、かなりの習熟を要する。全ての変数を可変か固定かといった観念で管理しなければならず、その変数がどこの所有権に属するかも常に把握していなければならない。厳密なメモリ管理を要するC言語でさえ、だいたいの現場ではヘッダに構造体を定義し、自作のコンストラクタやデストラクタでメモリ管理を行い、運用上は構造体定義を修正するだけで済むようになっていることが多いように思う。厳密な管理というのはそれほどに難しい。
こなれていない言語規約
2021/01/30:誤った知識だったため削除しました。
総評:天才以外は使わない前提の言語
そもそもrust最大の特徴は上述のメモリ管理にあると思うが。このメモリ管理手法自体が既に万人に向けて作られた仕様だとは思えない。古来多くの人類がC言語へ迂闊に触れることでセグフォを引き起こしてきた。これを教訓に「馬鹿でも何も考えずにメモリ管理をする」手法としてGCが作られた。golangはまさに、その思想の延長にあると私は思う。
ただし、C言語は敗北したわけではない。C言語は適切に書くことができれば最速だし、メモリ管理も完璧にこなせば破綻は起きない。理論上は何も問題はない。だから、「ルールを追加してC言語における最適なメモリ管理を徹底させよう」と考えたのがrustなのだと私には思える。最強のCをそのまま追及したような形だ。そのアシストとして強固な番人であるコンパイラが控えている。誤った書き方をすればコンパイラがとがめてくれる。
fn __access( &self ) -> Result<String, String> {
let res = ureq::post("https://api.ce-cotoha.com/v1/oauth/accesstokens")
.set("Content-Type", "application/json")
.send_json(
json!({
"grantType": "client_credentials",
"clientId": self.client_id,
"clientSecret": self.client_secret,
})
);
if ! res.ok() {
return Err(self.__concat("access:status is bad", res.status_line()));
}
let result = res.into_json();
if ! result.is_ok() {
return Err("access:Failed to parse response of cotoha api".to_string());
}
let json_data = result.ok().unwrap();
let mut access_token = json_data["access_token"].to_string();
if access_token == "" {
return Err("access:Authentication failed for cotoha api".to_string());
}
access_token.retain(|c| c != '"');
Ok(access_token)
}
golangのpost処理と同じものをrustで書いてみると、こうなる。golangに比べて、とてもスマートである。スマートすぎて、golangのように関数としてくくりだす必要もなかった。スマートではあるのだが、このコードを見て、ぱっと直感的に何をしているのか、理解できるだろうか。
使いこなせれば理想的な言語なのだろうと思える。自社開発などでエンジニアの質が明確にわかっていて、途中で人員が入れ替わる心配も薄いプロジェクトであれば実用できるかもしれない。ただ、質を保ったエンジニアが一定数いる現場というのは、簡単なようで、なかなかないというのが現実であるようにも思う。
nim
ふんわりやわらかいゆるい記法
proc post(url: string, header: openArray[tuple[key: string, val: string]], param: JsonNode): JsonNode =
var client = newHttpClient()
client.headers = newHttpHeaders(header)
var res: Response = client.request(
url = url,
httpMethod = HttpPost,
body = $param
)
if not res.status.contains("200") and not res.status.contains("201"):
return %*{ "err": fmt"post:status is bad:{res.status}" }
try:
return parseJson(res.body)
except:
return %*{ "err": fmt"post:parseJson is failed" }
真っ先にコードから載せてしまうが。見てわかる通り、かなりpythonに近い。変数に型宣言の必要なpythonである。規約という規約もない。例えば、nimには本来classというものは存在しない。だが、macro機能が標準で存在しており、ここに定義を記載することであたかもclassがあるかのように記述することができる。golangにもrustにもclassはないのだから、nimにだけはその自由が許されているというのは、書きやすさという意味では大きい。(golangもrustもclassらしきものを書くことはできるが、nimの疑似classに比べれば不自由にすぎると思う)。
行き届いた仕様
nimにはtryがある。tryがあれば優秀か、なければ欠陥品なのか、という話ではないのだが。golangの地獄のようなエラーハンドリングを見ると、こうしてtry一発で集約できる言語のありがたさは身に染みる。焦点はtryがあるかないかではなく、あったら便利レベルの仕様を軽々に入れてくれるという事実である。
golangにもrustにも明確な思想が存在しており、便利であるとかないとか、そういったレベルでは容易に機能を入れてくれないイメージがある。nimはストレスフリーを規約にしたいんじゃないかというくらい、すごく気軽に手軽に書くことを許してくれる。
総評:あらゆることを許してくれる自由度の高い言語
nimは書き方自体をmacroでいじることができ、便利そうな機能もほいほい入れてくれる。tryのような怠惰なエラーハンドリングも許してくれるわけで、エンジニアをとことん甘やかしてくれる言語だ。趣味でコンパイル言語の開発がしたい、という人には最適な言語かもしれない。
一方で、これを仕事で使うとなると二の足を踏んでしまう。あまりにも自由度が高すぎるため、おそらく個々人が自由気ままにコードを書くことを許してしまうからである。golangは病的な性質で全員に同じものを強制するし、rustも優秀なコンパイラがエラーだけは防いでくれる。nimにはそういった厳格さがないため、強い自己管理が求められる。
vlang
ドキュメントが未整備、直接ソースコードを見ながら実装する必要あり
新しい言語のため致し方ない部分もあるが、ドキュメントが機能していない。ドキュメントにはサンプルしか書かれておらず、いわゆるdocに相当するものがない。そのサンプルですら古いようで、コピペしても動かない。ドキュメントは「検索キーワードを知るためのもの」と割り切る必要がある。
幸いなことにソースコード:vlibは整備されていて、読みやすい。これは当たり前のようでいて、当たり前ではない。以前にPHPの実装を確かめるためにソースコードを読んでいたことがあったけれど、独自の定義などがあまりに多く、最低限必要な部分だけを盗み読むというのも難しかった。
エラー対応が楽
vlangはgolangに影響を受けた言語らしい。そのせいか記法自体はgolangによく似ているが、エラーハンドリングは非常に簡易になっている。詳しくは公式ドキュメント / Result 型とエラーハンドリングを読んで欲しい。
resp := http.get(url) or {
panic(err)
}
println(resp.body)
基本はこの形。orをつけて、エラー時の対応を記述できる。
resp := http.get(url) or {
return error(err)
}
println(resp.text)
もっとマシな対応がしたければ、こう。
fn http_get() ?resp {
resp := http.get(url) or {
return error(err)
}
println(resp.text)
}
返り値に?をつけておくと、意識してreturnを書かなくても、成功/失敗をそのまま返送してくれる。
resp := http.get(url)?
println(resp.body)
そういう細かい制御がいらない、勝手ツールであれば、これでもOK。一番上のコード(panic)と同じ動作をしてくれる、らしい。golangのヘイトを生んできたエラーハンドリングが改善されているのは朗報だろうと思う。
仕様バグらしきものが残ってる
jsonエンコードが辞書配列と接続していないため、使い勝手が悪い。というのはおそらく今後改善されていく部分だろうから、いいとして。
例えば、vlangはシングルクオーテーションを優先度の高い記法と見なしている。このため、ダブルクォーテーションの文字列を引き渡した際、自動でシングルクォーテーションに変換する仕様がある模様。
mut data := {
"s1": s1,
"s2": s2,
}
if senType != "" {
data["type"] = senType
}
if dicType != "" {
data["dic_type"] = dicType
}
ret := json.encode(data.str())
上記はmapを一旦stringにしてから、jsonエンコードをかけているのだが、
"{'s1':'s1', 's2':'s2'}"
結果は、こう。キーであるs1とs2がシングルクオーテーションになっている。
resp := http.fetch(
"https://api.ce-cotoha.com/api/dev/nlp/v1/" + target,
{
method: "POST",
data: data,
headers: {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Bearer " + access_token,
}
}
)?
これをdataとして送り込むと、さらにvlangがダブルクォーテーションをシングルクオーテーションに変換してくれる。
data: '{'s1':'s1', 's2':'s2'}'
結果、あらゆるクオーテーションがシングルクォーテーションになってしまうため、区切りの分からない文字列になってしまうのである。
fn (c Cotoha) json_encode(m map[string]string) string {
mut ret := "{"
for k, v in m {
ret += '"$k":"$v",'
}
ret = ret.trim_right(",")
ret += "}"
return ret
}
これを回避するために私は自作の関数を用意した。これで動くようにはなったが、思わぬトラップだった。
問題は、そもそも「シングルクオーテーションに統一する」思想や、それに伴って「ダブルクォーテーションをシングルクオーテーションに自動で変換する」仕様が必要なのか、ということである。この思想はgolangがエラーハンドリングを押し付けてきたり、rustがメモリ管理に複雑なルールを押し付けてくるのとは違う。効率化や最適化に寄与していないからである。かといって、nimのように利便性が高まっているかといえば、むしろ自由度を下げるだけの結果になっている。
何が言いたいかといえば、必然性のない仕様を入れた結果、不要なバグを生んでいるようにしか見えないということである。これは発展途上というだけでなく、開発言語としては迂闊というか軽薄なのではないか、と個人的には思う。
総評:方向性の定まらないgolang亜種
golangよりも利便性が高まっている部分は確実にある。これは評価したいが、golangの思想を継承するならエラーハンドリングの選択肢を複数用意するべきではないようにも思える。orか?のいずれかだけを実装し、無用な選択肢はむしろなくしておくべきではないか。エンジニアによる表記ゆれを極力避けるということが、golangがあえて目指した泥臭さだと思うからである。クオーテーションについての思想にしても、統一されていた方が便利に思える、となんとなく入れてみた感じがぬぐえない。
ただ、1.0も出ていない現状ではなんとも言えない。期待はするが、望み薄という感じをうける。