はじめに : 意味から紐解くRust
Rustは難しい言語だと言われます。同時に美しい言語だと言われます(僕調べ)。
Rustは2016年から2022年にかけてStack Overflowが行った調査で、「開発者がもっとも愛するプログラミング言語」1位に選ばれました。
Rustという言語は、それまでの過去のいくつかのパラダイムを取り込み、最適化された低級(的)言語です。それは堅牢で、高速で、難解で、パワフルです。
『Rustを書くために必要なこと』は、『プログラムを書くために必要なこと』だと言っても過言ではありません(勝手に言っています)。その理解においてまず大切なのは、仕組みを理解することではなく『意味と意図』を理解することです。
プログラマに理解と手順を強制することで、Rustは僕たちに『プログラムの書き方』を教えてくれます。Rustの理想を理解することは、Rustを利用すること以上に、僕たちに豊かさをもたらしてくれます。
ところで、この文章の中ですでに抽象的で比喩的な言葉を選んできました。それはつまり、美しさ、についてのことです。それがRustの力と魅力を語る一つの観点になるからです。
これから先のAI時代においても、それはまだAIには中々再現不能なものになるでしょう。つまり、プログラムという文字の羅列に対して、人が歴史と文化によって獲得する美しさへの感覚こそが究極の保守性なのだと。
暗黙に、優れたシステムエンジニアはその事を知っているはずです。
言語とは、そして統計と計算の集合物たるAIとは、それらを説明し補佐し再現するために後から生まれたものに過ぎないのだと。
さてさて、前置きが長くなりましたが、そんな観点でRustの特徴的な機能をいくつか切り取って説明しつつ、『Rustとはどんな言語であるか』の一つのご紹介とさせていただければと思います。
Rustに存在する学びのあれこれ
ここでは、Rustで特徴的な3つの機能について、プログラミングの目指す方向をうすらぼんやりと考えながらご紹介していきたいと思います。
1.所有権 : 今、どんなサイズのデータが、どのくらい作られ、どこで利用され、どこで破棄されているのかを意識すること。
Rustには噂に名高い所有権というシステムがあります。一番有名で、なんだかんだ難しいと度々言われるところですね。
これはシンプルに基本を言えば、『一つの値を保持しているのは一つの変数だけ』というルールです。
ざっと下記のソースコードを流し見てください。
fn main() {
let user_name = "Alice".to_string(); // Stringの所有権はuser_nameに
let welcome_message_user = user_name; // Stringの所有権はwelcome_message_userに
println!("Message: {}", welcome_message_user);
println!("Original: {}", user_name); // コンパイルエラー!user_nameは所有権を持たない。
}
上記のソースコードはコンパイルエラーになります。以下に簡単に解説します。
- Alice、というStringは、初めuser_nameが所有権を持ちます。
- しかし、その次の行でwelcome_message_userにその所有権は移ります。
- welcome_message_userに値を格納した時点で、値の所有権が移譲されてしまいますから、最終行のuser_nameは値の所有権を持っておらず、値を使えません。
これがRustの第一の特徴的仕様、所有権です!
所有権には他にも以下のような特徴があります。
- 関数の引数としてデータを渡しても所有権は移譲される。
fn greet(name: String) {
println!("Hello, {}!", name);
}
fn main() {
let user_name = String::from("Alice");
greet(user_name);
println!("Still usable: {}", user_name); // コンパイルエラー!
}
- スコープを抜けると所有権は破棄され、メモリが解放される
{
let s = String::from("temporary"); // 所有開始
} // sがスコープを抜け、メモリ解放
- 参照(借用)によって所有権を移さず使える
fn greet(name: &String) {
println!("Hello, {}!", name);
}
fn main() {
let user_name = String::from("Alice");
greet(&user_name);
println!("Still usable: {}", user_name); // コンパイルエラーにならない
}
つまりどういうことなのか
他にも様々な詳細仕様がありますが、説明はここでは省きます。ただ、この言語仕様をぜひ抽象的に捉えてみてほしいのです。
僕なりにRustという言語のこの仕様を噛み砕くならば、『データの所在が明らかであることが強制された言語』です。所有権にまつわるあらゆる仕組みがそうです。
Rustという言語はlet句によって変数を作れますが、これはデフォルトで不変です。この仕組みも、所有権と重ねて覚えておく概念です。
fn main() {
let name = "Alice";
name = "Bob"; // コンパイルエラー!
}
余計なことを書けばこれを変えられます。
fn main() {
let mut name = "Alice";
name = "Bob"; // エラーにならない
}
ただし、前記した『参照』でも、一度に可変となる変数は一つだけです。
デフォルトで不変であり、新しく変数に入れれば元の変数は使えない。そして、変数には所有権があり、誰がどうこの変数を利用しているか、寿命を含めて厳密に管理される。
Rustという言語の初めのメッセージです。
無駄に変えるな。無駄に作るな。値の変化を明確に追え。
これは処理速度に還元されるだけの問題ではありません。これは優秀なコンパイラやGCが超高速に動き、PCの性能が上がれば代替的に解決する問題ではないのです。
それは、読み物としてのコードの抱える保守性の問題でもあるからです。
かつて多くの言語は、不変である場合に特別な句の利用が必要でした。しかしRustは可変である場合に特別な句(mut)が必要になります。
それは勿論速度の問題ではなく、美しさの問題なのです。
2.列挙型 : ある文脈が異なる状態を持つ可能性があるとき、その状態の列挙が明らかであり、『状態に値と処理が依存する』こと。
次に特徴的なのは、Rustの列挙型:enumです。
あらゆるデータと処理は論理的な分類を持つことができます。荒く言えば、enumはこの分類を列挙によって提供するものです。
例えば掃き掃除と拭き掃除と物の移動という処理の分類を、『掃除』という分類名とともに提供してみます。
enum CleaningTask {
Sweeping,
Wiping,
Relocating,
}
この列挙に価値があるのは、ある状態を一つの視点で分類し、『そのどれか一つ』であることを保証することができるためです。『どれでもない』はあり得ず、どれか一つなのです。
表題にも書いた通り、『ある文脈が異なる状態を持つ可能性がある場合』において、その可能性が列挙としてまとまっていることは『美しい事』です。どのような可能性がある中で、どこに分類されているのかという事実は、極めて直感的なエクスキューズになり得るからです。
さて、ここまでは他の言語のenumでもほぼ同じです。Rustのenumの何より大きな特徴は、『列挙それぞれに異なる値を内包できること』です。
struct SweepingState{
target: String,
handle: String,
}
struct WipingState{
target: String,
cloth: String,
}
enum CleaningTask {
Sweeping(SweepingState),
Wiping(WipingState),
Relocating,
}
構造体を保持することができるので、もちろん値だけでなく処理も内包できます。
一言で言えば、Rustはある特定の状態(分類)に対して、『その文脈でしかあり得ない値』を保持できます。
そしてその状態が明らかであるとき、内方した値・処理を利用することができるのです。
if let CleaningTask::Sweeping(state) = cleaning_task {
println!("Sweeping the {}", state.target);
}
『この中のいずれか一つである』というセンテンスがその先の具体的な値を持っているということは、特定の状態にしか依存しない値(処理)が存在するならば非常に有用です。
『利用する側』が不要な値を管理する必要がなくなるためです。それは必要最低限で、直感的です。
抽象度を上げれば、そこにはこのような発見があります。
『処理や値が"状態"をもつ』のは醜いが、『"状態"が処理や値をもつ』のは美しいことである。特に、その状態が明瞭な視点で分類されたものならば。
私たちが様々な経験をし、認知する世界とはそうだからです。視点と分類により、私たちは世界を見ます。
認知の初めに明らかであるものは、人の思考結果たる分析と分類を端的に理解させるものであるべきです。視点と分類がある中で考えることと、考える中で視点と分類を見出そうとすることは違います。
子どもの見る世界が新鮮なものに溢れ、
大人の見る世界が分析と分類に溢れていると考えた時、
プログラミングは、幼さを捨てて理解と再現性をとる試みです。(もちろん、それだけではいけないときもありますが。)
また、Rustは網羅を必須とする分岐であるmatchという仕組みを持ちます。
let state = WipingState {
target: "hoge".to_string(),
cloth: "kinu".to_string(),
};
let cleaning_task = CleaningTask::Wiping(state);
match cleaning_task {
CleaningTask::Sweeping(state_tmp) => println!("handle :{}", state_tmp.handle),
CleaningTask::Wiping(state_tmp) => println!("cloth :{}", state_tmp.cloth),
CleaningTask::Relocating => println!("Relocating"),
}
matchはenumの選択肢を網羅していなければエラーになってしまいます。
つまりこの文脈において、enumを利用する側にとってもenumの持ちえる状態は明確な前提なのです。多態性でも連想配列でもなく、『enum』によって異なる状態が1つに表現されている利点は、この『利用する側にとっての明瞭さ』に他なりません。
Rustにおいては、null的な値もエラーハンドリングも、全てこのenumによって表現されます(!)。
『状態』がそれに依存した『値(処理)』を持てることで表現できる幅の広さは非常に多彩なのです。
3. 抽象型 : あるデータ型の持つ実装が意図によって区分され、自分自身の実装にすら疎結合であること。
さて、3つ目にRustのtraitについてお話しします。そのほかの言語の利用者の方々には、いわゆるインターフェースと言った方がわかりやすい人もいるでしょうか。
まずは実際の構文をご覧ください。traitの定義は以下です。プロパティは持てません。使えるのはメソッドだけです。
trait Cleanable{
fn clean_area(&self) -> String;
}
この抽象型の実装する場合は、以下のように書きます。
impl Cleanable for SweepingState {
fn clean_area(&self) -> String {
"Living Room".to_string()
}
}
構文だけで、その最も重要な思想の一つを見ることができます。
その他の言語の抽象型の実装に対して、明確に違う点はどこでしょうか?
フィールドプロパティが持てないこと?&selfという書きっぷり?
いえ!違うのは『何に対して、何を実装するのか』にあたる、『文字順』です。
impl Cleanable for SweepingState
通常、かなり多くの言語で、この順番は逆です。
class SweepingState implements Cleanable
struct SweepingState: Cleanable
class SweepingState(Cleanable)
他の言語における抽象型の実装は、『あるデータ型は、記載の抽象型を実装している』という型注釈的な形式です。『実装対象のデータ型の定義』に、元ネタとしての抽象型が指定されている形式なのです。
Rustの実装はその逆です。『特定の構造を、とあるデータ型に対して実装する』という、実装指示的な形式なのです。
この考え方は僕にとって素晴らしいものです。
頭の中に一つ一つのパーツが簡単に取り付け・取り外し可能なおもちゃを想像してください。それがRustのトレイト実装です。実装の一つ一つが、構造体に対して柔軟に取り付けられていくのです。
Rustの抽象型の実装は、構造体の定義後に一つずつ行います。
impl Cleanable for SweepingState {
fn clean_area(&self) -> String {
"Living Room".to_string()
}
}
impl Noize for SweepingState {
fn ring(&self){
println!("rinrin");
}
}
これは複数実装可能ですが、同時実装して相互依存させる事はできません。ほかの抽象型の実装を前提としてメソッドを呼び出したい場合は、抽象型自体に注釈が必要になります。
trait Loggable: Debug { // Debugトレイトを実装している前提のLoggableトレイト
fn log(&self) -> String
}
実装対象のデータ型が主である実装においては、データ型は1つの『塊』の様に動きます。組み立ての際に、抽象型の実装は相互に依存しても良いからです。しかし、Rustの実装では抽象型同士は基本的に疎結合です。
この仕組みがあるために、Rustは誰かの作ったライブラリの構造体にすら、自作のトレイトを実装する事ができます!さらには逆に、誰かが作ったトレイトを自作の構造体に実装する事で、特定の機能に対応させる事も可能です。
これにより、一つの構造体に対して非常に多彩なtrait実装が行えるのです。
機能(役割)を小さな単位で抽象化する。実装はその他の機能と疎結合にし、実装の取り付けを容易くする。
継承と多態性のないRustですが、taritとenumの利用により、似た、そしてより安全で可読性の高いソースコードを作成する事ができます。
この一つ一つは小さな差であるものが、最終的にできたソースコードの保守性を大きく変えていくことになるでしょう。
Rustとはどのような言語であるか?
さて、ほんの一部ではありますが、Rustの特徴的な言語仕様について説明してきました。
所有権・列挙型・抽象型と言う仕組みは、それぞれに『最低限で、分類された』プログラミング製造の仕組みです。これらを組み合わせていくことで、一つの大きな毛並みを持ったソースコードを作り出すことができるでしょう。
僕にはRustの言語的仕様の制限、文法の一つ一つが綺麗なコードに関する思案の入り口にあるように思えます。
言語的仕様は、考えの『かたち』を作るのです。
一方で、Rustは全く想像もしなかった新しい言語...と言うわけではありません。様々な言語仕様に影響を受け、その考え方を継承し、捉え直す形で作られた言語です。
あらゆるソースコードは(極端に言ってしまえば)文学です。人というブラックボックスが責任をとって捉え直し、それを読み解きます。そこには絶対はなく、観点だけがあります。
Rustの前提は(まだ)新鮮です。成り立ちや影響は、僕たちに多くの学びを与えてくれます。
終わりの言葉にかえて : これからのこと
言語的仕様の進化はやがて先細りしていくかもしれませんが、これから先も環境の変化の中で最適化された言語は変わっていくことでしょう。
スマホ端末はあと何年この形式が維持されるか分かりませんし、国家間の争いや企業のパワーバランスで、非合理的な制限が課されていく事もあるでしょう。
AIによって単純作業は淘汰されていくかもしれません。しかし現在の延長線上にある『人間用に作られたプログラミング言語』の理解と責任を持てるのは人間だけで、現状のAIが全量の責任を持つには異なるアプローチが必要になると思いますから、もしかしたらAIのコーディングに最適化されたプログラミング言語が出てくるかもしれません。
この記事で語られているのは、Rustの一部に関する非常に偏った視点です。
数学的理解や一般的な解説はあまり気にせず書きました。観点という意味ではあまりに比喩的であり、スットンキョーな事を言い出していると思われるかもしれません。(人によっては怒られるかもとも思っています)
なんにせよ、『良いプログラム』のあり方も大きく変わっていきます。そこで大切なのは、その『観点』をそれぞれに捉え直すことだと思っています。人の仕事は無くなりません。理想論を言っているのではなく、最後に責任を取ることになるのは、人間であるからです。
そして人は、しぶとくこの『美しさ』を追い求めることでしょう。
この記事でRustに興味を持った人は、ぜひ色々触ってみてください。
公式ドキュメント
また、この記事が誰かの思考のきっかけになると幸いです。