Rustの実用性が理解できる図を作成してみた 〜C/C++/Java/JS/Python/Go/TS/Elixirとの比較〜
この記事はRust Advent Calendar 2022 - Qiitaの11日目の記事です。
Rustはプログラマに愛されている言語だと言われています。でも、その愛されている理由をRustを知らない人に説明しようとしたとき苦労した経験はないでしょうか?たくさんの愛を語れば語るほど 「難しそう」 という一言に心を砕かれるのです。
私はRustが多くのプログラマに愛される理由はRustが多くの場面で実用的だからだと思っています。しかしプログラミング言語における「実用性」を定義するのも説明するのも非常に困難を極めるため、Rust布教の大きな壁になっていると感じています。
そこで本記事ではRustの実用性の図解にチャレンジしてみようと思います。
はじめに
本記事は、何らかのプログラミング言語の経験を持っているがRustを使ったことがない、またはRustの良さが分からない方を対象としています。またRust愛好者でRust未経験者にRustの良さを伝えるのに苦労した経験がある方にも読んで頂きたい内容となっております。Rustの実用性を図解を通して視覚的にお伝えできればと思います。
前提
まず 「実用性」 の定義ですが、本記事では 「プログラミング言語の様々なユースケースで実務レベルで対応できる広さ」 とします。要は一つのプログラミング言語で万能ナイフのように場面を選ばず使える言語の方が 「実用性が高い」 とします。
一方で特定の場面で利便性が高い言語というのも存在します。それらは 「DSL(ドメイン固有言語)」 と呼ばれていていますが、本記事はその有用性を否定するものではありません。本記事の対象はDSLとは正反対の 「汎用言語」 であり、その性能を語る上で適用範囲の広さを軸にした 「実用性」 測るのは意義深いことだと考えました。
言語比較
Rustの実用性を伝えるには、汎用プログラミング言語の実用性を見るときの観点が必要になってきます。ここでは私の経験と論理的思考(独断と偏見とも言う)に従って実用性を判断するための20の観点をピックアップしました。
比較する言語は現在多くのプログラマに利用されていて実用性が明白な言語を選択しました[1]。
以下はその観点に従って言語を比較したマトリックスになり、他の言語との比較によってRustの比較優位性を示したいと思います。
※ ASMはアセンブリ言語、JSはJavaScript、TSはTypeScriptを指す。
ASM | C | CPP | Java | JS | Python | Go | TS | Elixir | Rust | |
---|---|---|---|---|---|---|---|---|---|---|
エディタ支援 | o | o | o | |||||||
自動テスト | o | o | o | |||||||
リンタ | o | o | ||||||||
ビルドシステム | o | o | o | |||||||
パッケージマネージャ | o | o | o | o | ||||||
フォーマッタ | o | o | o | |||||||
手続き型 | o | o | o | o | o | o | o | o | ||
オブジェクト指向 | o | o | o | o | o | o | o | |||
関数型 | o | o | ||||||||
前処理性能 | o | △ | o | o | △ | △ | ||||
実行時性能 | o | o | o | △ | △ | △ | o | |||
メモリ効率 | o | o | o | △ | o | |||||
メモリ安全性 | o | o | o | o | o | o | o | |||
型安全性 | o | △ | △ | o | o | △ | o | |||
スレッド安全性 | △ | △ | △ | o | o | |||||
インタプリタ | o | o | o | |||||||
トランスパイラ | o | |||||||||
コンパイラ | o | o | o | o | o | o | ||||
VM | o | o | ||||||||
アセンブラ | o |
各行はプログラミングの実用性を判断するために必要な観点です。言語の特徴は正確な分類が困難なため多少の独断と偏見が含まれていることをご了承ください。
各行の意味
- エディタ支援
- 言語の公式がエディタ支援(LSP等)を提供しているかを示しています。
- 自動テスト
- 言語の公式が自動テスト(ユニットテスト等)を備えているかを示しています。
- リンタ
- 言語の公式がリンタを提供しているかを示しています。
- ビルドシステム
- 言語の公式がビルドシステムを提供しているかを示しています。
- パッケージマネージャ
- 言語の公式がパッケージマネージャを提供しているかを示しています。
- フォーマッタ
- 言語の公式がフォーマッタを提供しているかを示しています。
- 手続き型
- 言語のプログラミングパラダイムが手続き型プログラミングを強くサポートしているかどうかを示しています。
- オブジェクト指向
- 言語のプログラミングパラダイムがオブジェクト指向プログラミングを強くサポートしているかどうかを示しています。
- 関数型
- 言語のプログラミングパラダイムが関数型プログラミングを強くサポートしているかどうかを示しています。
- 前処理性能
- 実行の前処理の時間が短い程、前処理性能は良いものとここでは定義します。
- ここで言う「前処理の時間」とはユーザから見てプログラム実行前に必要な時間になります。
- 例えば、コンパイル言語ではコンパイル時間やリンク時間に該当します。
- スクリプト言語はすぐに実行されるので前処理時間が存在せず、前処理性能は良いものとします。
- 実行時性能
- プログラムの実行時間が短いほど実行性能が良いものと定義します。
- コンパイル型の言語では前処理で最適化処理をかけて実行性能を上げたりします。
- メモリ効率
- プログラムの実行時間帯でどれだけメモリを効率的に使えたかかを示します。
- メモリのフットプリントが大きいとメモリ効率が悪く、フットプリントが小さいとメモリ効率が良いものとします。
- メモリ安全性
- バッファオーバーフローやダングリングポインタ等のメモリアクセスに関するバグやセキュリティホールから守られている場合、メモリ安全性を満たしている状態になります。
- 型安全性
- 型によってプログラムの不正や脆弱性を検知できる場合、型安全性が高いものとここでは定義します。
- ここでは比較のために動的型よりも静的型の方が早く型エラーを検知できる分、型安全性が高いものとします。
- そのため動的型の型安全性はここでは△としています[2]。
- スレッド安全性
- 並行処理の共有リソースの競合に対する安全性がある場合にスレッド安全性があるものとします。
- リソース競合を回避する仕組みだけが用意されていて、実際に簡単に競合が起こせるものは△とします。
- スレッドやリソース競合に関する概念がそもそも言語本体にない場合は、スレッド安全性はないものとします。
- インタプリタ
- 言語の最も主要な処理系がインタプリタ型の処理系であることを示しています。
- スクリプト言語(LL言語)に多い処理系です。
- トランスパイラ
- 言語の最も主要な処理系がトランスパイラ型の処理系であることを示しています。
- TypeScriptやDartなどAltJSの処理系が含まれます。
- コンパイラ
- 言語の最も主要な処理系がコンパイラ型の処理系であることを示しています。
- VM
- 言語の最も主要な処理系がVM(仮想マシン、バイトコードインタプリタ)を採用していることを示しています。
- この場合VMが一般のユーザから見える状態のものを指します。
- 言語処理系の内部実装でVMと近い処理をしていもユーザから見えなければVMに該当しないものとします。
- アセンブラ
- 言語の最も主要な処理系がアセンブラ型の処理系であることを示しています。
プログラミング言語の実用性を判断する上で考慮した点は言語の特性もそうですが、 「言語が公式に提供するツール」 にも着目しています。それはプログラミングにおいてツールのサポートがなくては小規模ならまだしも大規模なプログラムを書くのは困難なのでだからです。ここで「公式」に絞っているのは、判断のしやすさもありますがやはり公式の実装は安定していて安心でき、迷わず利用可能なことから「実用性」の根拠に足るものだと判断したためです。
本当は実用性を判断するには 「ドキュメントの充実度」 や 「エコシステムの充実度・活発度」 や 「スポンサーの強さ、多さ」 なども判断基準にすべきだと思いますが、これらは判断が難しく様々な要因で揺れ動くので、今回は言語特性とその特性から本質的に影響を受けている中長期的に安定した性質にフォーカスすることにしました。
言語の実用性判定マップ
前節の表で 「完全に理解した」 という方は、恐らくプログラミング言語に造詣が深い方だと思われます。実際に分かる人にはあの表だけでプログラミング言語の得意なシーンや苦手な分野が見えて来るはずです。そして○が多い方が様々な分野で利用可能な言語だと言うことが何となく分かるかもしれません。
しかし現実的にあの表からプログラミング言語の実用性を判断するのは確かに難易度が高いのでもうひと工夫します。具体的には観点を5つのカテゴリに分類し、さらに一本の軸に沿ってそのカテゴリを整列させます。これを 「言語の実用性判定マップ」 と呼ぶことにします。以下がその図になります。
言語の実用性判定マップ
1つの軸
言語の実用性を判断する要は ユーザビリティ/効率・信頼性 の軸です。これら2つの要素は必ずしも背反するものではありませんが、多くの場合 トレードオフの関係 になります。
ここで言うユーザビリティは単純にユーザ(人間)に分かりやすいという意味だけでなく、プログラムの規模が大きくなってもユーザビリティが落ちにくいという意味も含まれています。一般にプログラムの規模が小さい場合にはどの言語も容易にユーザが把握可能ですが、規模が大きなるほどユーザが把握しづらくなり、言語の抽象化の能力やツールの力が発揮されることになります。
効率・信頼性はプログラムが ハードウェア(CPU/メモリ等)にとってどれだけ扱いやすいか を示しています。CPUは非常に単純な実行モデルなので、CPUに優しく効率の良いプログラムを書こうとすると究極的には「0」と「1」の2値だけでプログラミングするハメになります。またCPUは曖昧さを許してくれないので、人間が犯しやすそうなミスに(メモリアクセス違反等)を検知できるような信頼性が必要になります。
5つのカテゴリ
「ユーザビリティ/効率・信頼性」の軸に沿って並べられるのは、 「ツール」、「パラダイム」、「性能」、「安全性」、「言語基盤」 の5つのカテゴリです。
「ツール」 人間が実際に操作するものでそもそもユーザを補助する目的で作られるもので、高いユーザビリティが必要とされるため図では上に配置されます。
「パラダイム」 はユーザビリティ/効率・信頼性の軸の中間に位置して、人間(ユーザ)と機械(CPU)をつなぐ役割を果たします。サポートするパラダイムは多ければ良いというものではありませんが、ここに上げた「手続き型」、「オブジェクト指向」、「関数型」の3つのパラダイムは広く認知されており多くの有用性が認められているので、全てのパラダイムを上手に取り入れた言語の実用範囲は広がることになります。
「性能」 と 「安全性」 はハードウェア(CPUやメモリ等)を活かすために必要な特性なので一番下(効率・信頼性重視)として配置されています。
「言語基盤」 は 「ユーザビリティ/効率・信頼性」の全般に関わるので縦長の配置になっています。上の方の特徴(インタプリタ・トランスパイラ)はユーザビリティを重視した設計が求められており、下の方のVM・アセンブラは効率・信頼性を重視した設計が行われることが多いので内部で軸を意識した配置にはしています。
さて、ここまででようやく各言語の比較ができるようになりました。
言語比較
早速、言語の実用性判定マップを使って言語比較を行ってみたいと思います。簡単な言語の説明も入れていますが、主題ではないのでかなり大味で簡略したものになっていることをご了承ください。
C言語とC++
まずは定番のC/C++の比較から行っていきます。
CとC++は同じコンパイル型の言語ですが、C++はオブジェクト指向言語であり、Cと比較して大規模なコードベースや抽象化に向いています。C++の特徴としてはオブジェクト指向を追加するにあたって、実行時性能とメモリ効率を犠牲にしないという選択肢を取っています。そのためにコンパイラが非常に頑張っていますが、エラーメッセージが難解になるパターンも多いです。
JavaとJavaScript
これもまたよくネタにされやすいJavaとJavaScriptの比較をやっていきます。
JavaとJavaScriptはパラダイムこそ同じですが、性能と安全性の守備範囲が大きく異なります。Javaはコンパイル型言語で静的型付けなのでメモリ安全性と型安全性はしっかり守られます。また言語自体に並行性に関する機能も取り入れられています。しかしリソース競合に関してはユーザの注意努力に頼るところが大きいので、スレッド安全性は半分になっています。実行時性能はJava VMの最適化がすごいおかげでそこそこ良好ですがC/C++と比較すると明らかに劣ります。
JavaScriptはインタプリタ型の言語でコンパイル等の前処理がいらず、すぐに実行できることから、前処理性能は良好です。ただその皺寄せは実行時性能にいっています。安全性に関してはメモリ安全性は確保していますが動的型付けなので型安全性は半分にしています。JavaScriptは伝統的に言語仕様に並行性を含まないので、スレッド安全性はなしにしています[3]。
PythonとGo
最近はライバル認定されやすいPythonとGo言語も比較してみます。
Pythonは典型的なインタプリタ言語でLL(LightWight)言語の特性を持ち、日々のタスクを回すのに最適なスクリプト言語です。スクリプト言語の特徴は前処理性能でコードを書いて即実行できます。コンパイル処理の間にコーヒーを入れる必要はありません(笑)。物議を醸しそうなのはパッケージマネージャでしょうか。Pythonのパッケージングには紆余曲折あり今後も変わりゆく予感はありますが、 「pip」 が標準添付されているので「パッケージマネージャ」を○にしました。
Go言語も典型的なコンパイル言語と言いたいところですが、天下のGoogle様が設計しただけあって性能が見慣れない光景になっています。よく言えばバランスが取れており悪く言えば中途半端になっています。この性能の特徴からシステムプログラミングにおいてはミドルウェアで良く用いられますが、OSや組み込みプログラミングなどの性能要件がシビアな世界ではC/C++の代替にはなれていません。ツールは後発の言語だけあって充実しており、大規模なコードベースでも、複数人の開発でもユーザビリティを損なうことなく開発可能です。
TypeScriptとElixir
最後によく話題に登る熱い言語を持ってきました。
TypeScriptはフロントエンド界隈では飛ぶ鳥落とす勢いで、人気だけ見るとJavaScriptを大きく引き離しており、OSSでもTypeScriptの採用が目立ってきました。TypeScriptはトランスパイル[4]という処理でJavaScriptに変換されて、JavaScriptのインタプリタで実行される言語です。JavaScriptは動的型付けのため人間のミスによる実行時エラーに悩まされましたが、TypeScriptは静的型のため実行前に型チェックして未然に多くのミスを防ぎます[5]。そのためJavaScriptと比較して実行前性能を犠牲にしていますが、それを上回る利点を型安全性から得ているという評価です。
ちなみに静的型付けは 「型安全性」 にカテゴライズされて、どちらかというと「機械」のための性質に見えますが、結果的にツールや言語基盤のサポートを受けやすくユーザビリティに間接的に影響を与えるという複雑な構造になっています。このようなところが言語比較でなかなか理解しづらい難しいところです。
Elixirはこれまでの言語と比較すると比較的マイナーですが、Stack Overflowの2O22年の調査でプログラマに愛される言語として Rustに次ぐ2位の座を奪い取ったアチアチの言語 です。Elixirは関数型言語として唯一のエントリーですが、注目すべき特徴はスレッド安全性です。これはElixirの基盤となっているErlang言語とBEAMと呼ばれるErlang VMの特性に由来します[6]。Elixirも後発の言語らしくツールが非常に充実しており、その辺りも人気に繋がっていると考えられます。
Rustの実用性を確認する
そして満を持してのRustの登場です。
ここまで辿り着いた方は図を見ただけで一目瞭然でしょう。性能と安全性をここまで両立させてしまったことに驚愕せざるを得ません。 これは 「マルチパラダイム」 [7]という豊富な抽象化の武器を活かして、性能を殺さずに安全性を提供するという言語の創意工夫とコンパイラの多大な尽力 のおかげです。
そして標準で装備されているツールの豊富さも実用性が優れている根拠の一つです。ある程度の規模の開発ではこの図に出てくるツール一式を揃えてようやく 「開発環境」 として成立しますが、これらが標準で備わっていない言語ではツール探しやその組み合わせ/連携で非常に苦労することが多い印象です。そしてようやく覚えたツール群も流行り廃りが激しくて数年後にはオワコンになっていたりと心配は尽きません。その点Rustでは言語を覚えたてでも実務レベルですぐに使える開発環境を悩むことなく手に入れることができるので、 「ブラボー」 としか言いようがありません。
図にも載せてある 効率的で信頼できるソフトウェアを誰もがつくれる言語というのはRust本家が掲げている公式のテーゼですが、この言葉からもRustが高い実用性を自負している様子が窺えます。
言語の得意分野をレーダーチャートにしてみる
本来なら前節で終わりにする予定でしたが、コードを載せずにTech記事を名乗るとイマイチな感じがしたので強引にRustコードを載せるべくネタを考えました。
プログラミングが活躍する適用分野を5つ選択してかなり強引ですが以下のような特色があると仮定します。
適用分野 | 前処理 性能 |
実行時 性能 |
メモリ 効率 |
メモリ 安全性 |
型 安全性 |
スレッド 安全性 |
抽象度 | コード 規模 |
---|---|---|---|---|---|---|---|---|
Webアプリケーション | 4 | 1 | 1 | 3 | 2 | 1 | 4 | 2 |
システム開発 | 1 | 3 | 4 | 1 | 1 | 2 | 1 | 3 |
日常タスク自動化 | 4 | 0 | 0 | 3 | 1 | 0 | 4 | 1 |
ゲーム開発 | 1 | 2 | 2 | 2 | 2 | 2 | 4 | 3 |
分析・機械学習 | 2 | 2 | 1 | 3 | 2 | 2 | 4 | 1 |
コード規模はツール、抽象度はパラダイムの特性を反映させることにします。逆に言語基盤の特性はその他の特性に反映されていると仮定します。上記の表と言語比較で紹介した表を利用して各言語がどの分野に適しているのかを計算してみます。その結果をレーダーチャートにしたのが以下になります。細かく見るとツッコミどころは多々ありますが、感触は悪くないと思いました。
ソースコードは以下になります。言語毎に適用分野のスコアを10段階で計算しています。強引な計算も色々入っているのでつっこみは不要です。
Rustコード
#[derive(Debug)]
struct LangProp {
editor_support: u8,
auto_test: u8,
linter: u8,
build_system: u8,
package_manager: u8,
formatter: u8,
procedural: u8,
object_oriented: u8,
functional: u8,
preparation_performance: u8,
execution_performance: u8,
memory_efficiency: u8,
memory_safety: u8,
type_safety: u8,
thread_safety: u8
}
impl LangProp {
fn new(editor_support: u8, auto_test: u8, linter: u8, build_system: u8, package_manager: u8, formatter: u8, procedural: u8, object_oriented: u8, functional: u8, preparation_performance: u8, execution_performance: u8, memory_efficiency: u8, memory_safety: u8, type_safety: u8, thread_safety: u8 ) -> LangProp {
LangProp { editor_support, auto_test, linter, build_system, package_manager, formatter, procedural, object_oriented, functional, preparation_performance, execution_performance, memory_efficiency, memory_safety, type_safety, thread_safety}
}
}
#[derive(Debug)]
struct FieldProp {
preparation_performance: u8,
execution_performance: u8,
memory_efficiency: u8,
memory_safety: u8,
type_safety: u8,
thread_safety: u8,
abstraction: u8,
code_size: u8
}
impl FieldProp {
fn new(preparation_performance: u8, execution_performance: u8, memory_efficiency: u8, memory_safety: u8, type_safety: u8, thread_safety: u8, abstraction: u8,code_size: u8) -> FieldProp {
FieldProp { preparation_performance, execution_performance, memory_efficiency, memory_safety, type_safety, thread_safety, abstraction, code_size }
}
}
fn calc_practicality_score(field: &FieldProp, lang: &LangProp) -> u8 {
let code_size_score = ((lang.editor_support + lang.auto_test + lang.linter + lang.build_system + lang.package_manager + lang.formatter) as f32 / 6.) as u8;
let abstraction_score = (((lang.procedural + lang.object_oriented + lang.functional) as f32) / 2.) as u8;
let perfect_score = (field.preparation_performance + field.execution_performance + field.memory_efficiency + field.memory_safety + field.type_safety + field.thread_safety + field.abstraction + field.code_size) + 10;
let real_score = field.preparation_performance * lang.preparation_performance + field.execution_performance * lang.execution_performance + field.memory_efficiency * lang.memory_efficiency + field.memory_safety * lang.memory_safety + field.type_safety * lang.type_safety + field.thread_safety * lang.thread_safety + field.abstraction * abstraction_score + field.code_size * code_size_score;
let practicality_score = ((real_score as f32 / perfect_score as f32) * 10.) as u8;
if practicality_score > 10 {10} else {practicality_score}
}
use std::collections::HashMap;
fn main() {
let mut langs = HashMap::new();
langs.insert("asm", LangProp::new(0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0));
langs.insert("c", LangProp::new(0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 2, 2, 0, 0, 0));
langs.insert("cpp", LangProp::new(0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 0));
langs.insert("java", LangProp::new(0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 2, 2, 1));
langs.insert("js", LangProp::new(0, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0, 2, 1, 0));
langs.insert("python", LangProp::new(0, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0, 2, 1, 1));
langs.insert("go", LangProp::new(2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 1));
langs.insert("ts", LangProp::new(0, 0, 0, 0, 2, 0, 2, 2, 0, 1, 0, 0, 2, 2, 0));
langs.insert("elixir", LangProp::new(0, 2, 0, 2, 2, 2, 0, 0, 2, 1, 1, 0, 2, 1, 2));
langs.insert("rust", LangProp::new(2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2));
let mut fields = HashMap::new();
fields.insert("web", FieldProp::new(4, 1, 1, 3, 2, 1, 4, 2));
fields.insert("system", FieldProp::new(1, 3, 4, 1, 1, 2, 1, 3));
fields.insert("task", FieldProp::new(4, 0, 0, 3, 1, 0,4, 1));
fields.insert("game", FieldProp::new(1, 2, 2, 2, 2, 2, 4, 3));
fields.insert("analysis", FieldProp::new(2, 2, 1, 3, 2, 2, 4, 1));
for lang_name in ["asm", "c", "cpp", "java", "js", "python", "go", "ts", "elixir", "rust"] {
for field_name in ["web", "system", "task", "game", "analysis"] {
println!("lang:{: >7}, field:{: >9}, practicality score:{: >3}", lang_name, field_name, calc_practicality_score(fields.get(field_name).unwrap(), langs.get(lang_name).unwrap()));
}
println!();
}
}
まとめ
長文を読んで頂きありがとうございました。
Rustは所有権や借用といった特徴が目立ちすぎている感じがしますが、本記事ではそういう際立った特徴には触れずに言語間で比較可能な特性をもとにRustの実用性を考察してみました。
「実用性」 の定義は、本記事では一つのプログラミング言語で万能ナイフのように場面を選ばず使える言語の方が 「実用性が高い」 としています。その前提のもとで実用性を判定する20のキーファクターを1つの軸と5つのカテゴリに分類した 言語の実用性判定マップ を考案して、汎用言語であるC/C++/Java/JS/Python/Go/TS/Elixirを比較してみました[8]。
そして実際に言語の実用性判定マップを用いることで、Rustの実用性を浮き彫りにすることができたのではないかと思っています。言語の実用性判定マップは言語の特徴を大雑把に捉えるのに適していると思われるのでぜひ活用してみてください。
本記事がRustの実用性に関してより良い理解に繋がれば幸いです。
おまけ
Rustの機能に着目した言語の比較も行っています。興味がある方は御覧ください。
-
異論は認めます。付け加えると実用性の他に言語の比較のために特徴のバランスを考えて選択しています。特徴が似ている言語に関しては知名度が高いものを選択しています。Rubyを入れたかったのですがPythonに負けました・・・ ↩︎
-
一般的な型安全性の定義では強い型付けであれば型安全性はあると判断されますが、ここでは言語比較をしやすくするためにこの定義を採用します。 ↩︎
-
最近はWebWorkerという仕組みもあり並行性を扱えるようになってきておりECMAScriptにもWebWorkerを実現するための仕様は定義されていますが、その仕様はDOMに関する仕様なので本記事で扱う「実用性」に関する言語仕様には該当しないと判断しました。またWebWorkerのユーザが利用するAPI自体はユーザインタフェースを妨げずにバックグラウンドで処理するためのシンプルなものであり、他の言語が提供する一般的な並行性と比較すると一段劣ると考えられるので、「WebWorker」を以ってJavaScriptに一般的な並行性があるとするのも違和感があると思いました。従って本記事では私の独断によりJavaScriptには限定された並行性は認められるが「一般的な並行性を扱うことはできない」とさせて頂きました。(異論は認めます) ↩︎
-
トランスパイルは抽象度が同レベルの言語の変換に用いられる処理です。 ↩︎
-
TypeScriptは容易に型チェックの抜け穴を利用できるので型安全かどうか疑問符を持たれる方もいるかと思われますが、これはJavaScriptとの互換性を崩さないための必要悪的な措置でTypeScript自体は非常に強固な型システムを提供しています。 ↩︎
-
ここでは詳しく説明しませんが、「プロセス」同士が「メッセージパッシング」によって通信を行い、そもそもメモリ共有を許さないモデルになっています。 ↩︎
-
Rustには手続き型やオブジェクト指向や関数型の良いところを後付で取り入れたというのではなく、言語の本質として「マルチパラダイム」になるように設計したと感じられる機能が随所に見られます。 ↩︎
-
「はじめに」にも書かせて頂いたとおり、今回の比較は特定の言語の意義や有用性を否定するものではありません。また比較表はこれが完璧だと言うつもりもなく、比較表の節でもお断りしたとおり独断と偏見を含んだ個人的な見解になっています。私的には8割方は外していないと思っていますが、様々な角度や見方があるので完璧な比較表は存在し得ないと考えています。そしてその比較表の上に更に仮定を積み重ねて簡易な計算式でスコアを出しているレーダーチャートに関しては、その前提条件を理解せずに鵜呑みにするのは危険です。ご利用はあくまで個人の判断と責任でお願いいたします。 ↩︎
Discussion
TypeScriptは公式でtsserverを提供しているのでエディタ支援○なのでは? 逆にパッケージマネージャはnpmに乗っかっているので言語公式じゃない気がします。
ご意見ありがとうございます。おっしゃるとおりtssserverがあるので修正させて頂きました。パッケージマネージャに関しては、説明が足りておらず申し訳ございません。親言語があり、エコシステムを共有しているものは准公式扱いにして○にしておりました。この件は人によって様々な意見があると思われますのでこのままにさせてください。
そもそもnpm自体がJavaScriptの標準(公式によるもの)ではありません。
デファクトスタンダードになっているだけのサードパーティーのツールです。
npmの存在でJS/TSでのパッケージマネージャーの項目が○になっているなら、デファクトスタンダードのパッケージマネージャー兼ビルドツールのMaven/Gradleが存在するJavaも○になって然るべきでしょう。
ご意見ありがとうございます。確かにnpmはデファクトスタンダートなだけなので、JS,TSからパッケージマネージャを削除させて頂きました。
rustはやっぱり素で優秀ですねー
js,tsのツールチェーンの混沌具合と比較するとすごい楽でした
個人的に記事を見てて思ったことを一つ。
eclipseやintelljといった非常に強力で事実上の標準となっているIDEがあるjavaは、エディタ支援を○にしてもいいのかなと思いました。特にjava(kotlin)+intellijの完成度は他にないぐらいですし。
ご意見ありがとうございます。おっしゃる通りEclipse、intelijはすごい便利ですね。この記事を書くにあたって、「事実上標準」をどう扱うか非常に悩んでいて事実上標準がある場合は○でも良いとも思ったりしたのですが、正直な話私の力不足で全てのツールで事実上標準を調べる力がなかったので今回は「言語公式が提供もしくは推奨」しているものだけ取り上げました。そのためここだけ変えると全体のポリシーに影響して整合が取れなくなるので、今回はご提案は見送らせてください。別途、機会があれば考えてみたいと思います。
ECMAScriptの仕様として、SharedArrayBuffer(異なるスレッド間での共有メモリモデル)のためなどにAtomicsやAgentsなどThreadを扱うための仕組みが定義されています。DOMの仕様から参照するための定義なので、スレッドを直接的に利用するAPIがECMAScriptにあるという感じではないですが。
なので、仕様としてはスレッドを抽象化したAgentやAgent Clusterなどの概念がありますが、実際に使うAPIはDOMの仕様などで定義され、それがホスト環境に実装されたものというイメージです。
1つのAgentが1つの実行スレッドにあたるようなイメージなので、通常のJavaScriptは1つのAgent(Single Thread)内で動いているものという概念で整理されています。
この辺が分かりやすいと思います。
ご意見ありがとうございます。説明が足りておらず申し訳ございません。私もこの記事を書くにあたってWebWorkerについて調べて見たのですが、おっしゃる通りWebWorkerを実現するための仕組みは仕様にあってもユーザにはそのAPIが自由に使える感じには見えませんでした。そのため一般ユーザからは「WebWorker」という機能しか見えていません。WebWorkerはユーザインタフェースを妨げずにバックグラウンドで処理するためのシンプルなAPIなので、他の言語が提供する一般的な並行性と比較すると一段劣ると考えられます。そしてここが語弊があった部分だと思う場所ですが「『基本的』な言語仕様」と書いたのは一般ユーザ見えの表面上の仕様には一般的に想定される「スレッド」を扱う仕組みはないと言う意味で、言外にはWebWorkerを実現するための仕様はあるという意味を込めていました。そこで以下のように修正させて頂くことにしました。
Rustは関数型言語のいい点を取り込んでいる言語ではありますが、しかしRustでは関数は第一級オブジェクトではなく制約も厳しいため関数型言語ではありません。
逆に、JSやTSの方が第一級関数や高階関数などのサポートが厚く、標準ライブラリにおいて関数型言語のような書き方ができるメソッドが多数用意されているため関数型言語と言えます。
ご意見ありがとうございます。
折りたたみだったため見落とされたのかもしれませんが、表内の「関数型」は「関数型言語」を意味しておりません。関数型言語の意味は以下のとおりとなります。
従って、Rustが関数型言語でないことは確かですが、この記事で主張しているのは関数型プログラミングを強くサポートしているということです。Rustが関数型プログラミングを強くサポートしていると主張するのは人によって意見が分かれるところなので異論は認めます。例えばおっしゃるとおり第一級オブジェクトをサポートしていなければ関数型プログラミングをサポートしていないという主張も十分ありえると思います。私がRustが関数型プログラミングを強くサポートしていると個人的に思っている点は、以下の点です。
これも異論は認めます。本記事では言語の「実用性」に着目した特性に絞っており、どれだけ関数型の特徴を満たしているかは重視していなくて、「実用的」な範囲で便利な関数型プログラミングの特性をどれだけ自然に利用できるかで判断しています。
また、JSやTSは「関数型言語と言える」かどうかですが、私は関数型言語かどうかは以下のWikipediaにあるとおり、関数型プログラミングを推奨してるかどうかで決まると考えているので、私の個人的見解としてはJS、TSは関数型言語ではなくオブジェクト指向言語だと判断しております。
残りの論点はJavaScriptが関数型プログラミングを強くサポートしているかどうかですが、前の主張と繰り返しになりますが、実用的な範囲ではオブジェクト指向言語として使われる場面が多いと思って、外したのですがこれは正直、個人のスタイルによるかもしれません。これも異論は認めます。
ご期待に添えない回答かもしれませんが、注釈に以下の通り書かせて頂きましたが、私の独断と偏見を含んでおり、様々な角度や見方があるのは承知しておりますので、解釈の不一致ということでご容赦ください。
それと、Rustはオブジェクト指向ではありません。Rustはis-a型の継承をサポートしておらず、一般のオブジェクト指向の定義とは外れています。寧ろRustは継承の概念により複雑化していたオブジェクト指向の風潮から脱するような設計になっています。
ご意見ありがとうございます。
Rustは「オブジェクト指向言語」ではないとは思います。「一般のオブジェクト指向言語」ではなく「マルチパラダイム言語」だというのが私の見解です。オブジェクト指向言語の影響を明白に受けていて、オブジェクト指向言語のいくつかの特徴を受け継いでいます。
JavaScriptは公式としてはマルチパラダイムであり、オブジェクト指向としての使い方も関数型プログラミングでの使い方も両方推奨しています。
JavaScriptを関数型プログラミング言語のように扱う文化は広くに渡っており、例えばReactのReact Hookの設計はモナド操作の概念に強く影響を受けています。
そのうえ、JavaScriptをオブジェクト指向として見ようとする面で言えば元来JavaScriptにはprototype-baseなオブジェクト指向しか実装されておらず最初から強力にサポートしようという意思はあまりなかったという主張もできます。JavaScriptは最初からどのような使い方も想定しているのです。ES6でclassが追加されたことは「JavaScriptはオブジェクト指向を強く推しており関数型プログラミングを推していない」理由にはなりません。なぜならばES6ではアロー演算子もサポートされており関数型プログラミングとしてサポートする公式の意向も見えるためです。
ご意見ありがとうございます。
JavaScriptは歴史の長い言語で、最初はJavaの影響を受けてオブジェクト指向を強く意識した見た目になり、徐々にマルチパラダイムを意識して推奨するようになったという理解をしています。前の回答にも書きましたが、多様な使い方をされている言語の分類はそもそも困難です。以下に書かれているとおり比較表の正確性を担保するつもりはなく、独断と偏見が含まれており、気に入らない方はスルーして頂ければと思います。
JavaScriptが現在マルチパラダイムを意識していることに同意為されるのであれば、以下のあなたの論理に従うとJavaScriptの関数型のところに〇を付けていないことに疑問を感じます。
ご意見ありがとうございます。
私としての論理をあえて言うなら、「JavaScriptは歴史の長い言語で評価が難しい」です。再掲になりますが、以下をお読みください。
アセンブリやC言語やC++、JavaやPythonなどよりも歴史が短い言語ではあるのですがね。それでは大半の評価がさぞ難しかったのであろうとお察しいたします。
ところで、エディタ支援、自動テスト、ビルドシステム、パッケージマネージャ、フォーマッタが公式で用意されていて、手続き型、オブジェクト指向、関数型のパラダイムを取り入れており、前処理性能も実行時性能も高く、メモリ効率の良さとメモリ安全性の両方を備えており、型安全で、しかもインタプリタ、トランスパイラ、コンパイラの全ての性質を持っている、この評価軸ではあなたにお勧めの言語がありまして。
V言語って言うんですけど。
そもそも「実用性」という語で語るなら、サードパーティーの標準を無視する事自体ナンセンスでしょう。
現実の開発においてサードパーティーのライブラリ等を一切使わないというのは全く「実用的」ではありません。
また、一般に、特定のプログラミング言語の実用性を語る上で重要視される項目の一つとして(標準・サードパーティーを問わない)「ライブラリの量と質」もあります。サードパーティーを切り捨てるとこのような重要な観点も欠落します。
と別なコメントへの返信で書かれていますが、厳しい事を言うとその程度の理解度で比較記事を書かない方が良いと思います。このように多方面からツッコミが入りますし、このような記事を主に参考にするために見るのは各言語に詳しく無い初学者が多く、その人達を勘違いさせてしまうので。
ご意見ありがとうございます。
「実用性」に関してこの記事が正しいというつもりはありません。私の考察であり個人的見解なので、「こんなこと考えている人もいるんだ」くらいに捉えて頂ければと思います。
ご忠告ありがとうございます。しかしこれは比較はおまけで、本質は「実用性」に対する考察記事です。正しく言語比較をやるつもりで書いた記事ではないですし、それは誰でも無理だと思います。
建設的なご意見は歓迎します。人格攻撃までくると通報するだけですが・・・
初学者かどうかは関係なく言語比較には「独断と偏見」が入っていると最初に明言しており、以下のような脚注も入れています。あとはリテラシーの問題かと思います。ここまで書いて鵜呑みにされると打つ手はないです。
提案ですが、Rustは静的言語の中でもとりわけNull安全性が〇の部類なので、ぜひ比較項目にほしいです。
ご提案ありがとうございます。
RustのOptionはとても便利ですね。null安全性について少し調べたのですが、union typeやNon-nullable TypesやOptionalなどnullを表現できる型を持つだけでnull安全だとしている記事を見かけました。そうなるとJavaやTypeScript等もnull安全な部類に入ります。もちろんRustはそれより強いnull安全だとは思います。ただ昔、他の人に言われたのですが、Rustのnull安全を宣伝したら、「簡単にunwrapできるけどそれで安全性を保証しているの?」と言われました。メモリ安全性やスレッド安全性もunsafeを使えば簡単に壊せますが、unsafeは普段は使わないのでセーフと言えるかもしれませんが、unwrapは比較的カジュアルに使われているので、「保証」とまで言い切れるのか自信がなく、少なくとも「メモリ安全性」と比べると安全性は1段劣ってしまうのかなと感じました。
比較目的の記事だったらnull安全性を是非ともいれたいところですが、本記事は実用性の考察が目的であり、null安全性がなくても実用性は浮き彫りにできたので、無理していれることはないと判断させて頂きました。もちろんこれは個人的な意見であり、今後null安全に対する私の理解が深まればまた変わるかもしれませんが、今回はこのままとさせてください。
承知しました。あくまで参考までに説明いたしますが、Null安全性はそんな難しい話じゃないはずです。
Option/Optionalなどの型はNull安全性の本質ではなく、いかにNull値の代入できない型を構築できるかが重要です。
その前提を踏まえて、TypeScriptは基本的にstrictNullChecks前提であればNull安全です。Javaの場合は、Nullable(NonNull)アノテーションをつけた上、コンパイル時にエラーを出すように静的解析の設定をする必要があり、一般的にはNull安全性がない見解です。ご参考になりましたら幸いです。
ご説明ありがとうございます。
null安全性を考えるにあたっての参考にさせて頂きます。私自身もnull安全性が実用性を判断する上での観点になり得るという点では疑問はないと思っています。現在考察しているのは仮にnull安全性をいれた場合の観点のバランスです。1つ目はnull安全性は静的型が前提となっており、null安全性があると必然的に型安全性があることになります。この相関関係があることをどのようなバランスで表現するのか良いのかという点で議論の余地があると考えています。例えば独立の観点として扱うと、動的型の実用性が想定より低く見積もられないかという点です。動的型は型安全性で△にしており、すでにハンデはつけてあるのでこれ以上のハンデが必要かという点です。点数を3段階でなくもう少し細かくする方法もありますが、そうなると全体の点数の付け方に影響があり、その意味づけが難しくなります。独立とせずに、型安全性に従属させるのも違うかなともなので、もう少し考えを整理したいと思っています。2つ目は、性能とのバランスもあります。性能と安全性の観点を3つに揃えているのは、どちらの観点もバランス良く配合されるためのものでもあるのでこのバランスも見直す必要があるかもしれません。3つ目は、前にも書いたnull安全性はメモリ安全性と比較して安全性や重要度に差がある説です(スレッド安全性や型安全性も同じく)。ここも評点方法を工夫して安全性にも度合いをつける工夫とかで乗り切れるかもしれません。どちらにせよもう少し踏み込んだ考察が必要なので、いろいろと考えて見たいと思います。