Rust
melangの各noteで他言語と比較した、Rustの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
型注釈も型推論も両方使える。
let age: i32 = 20;let age: i32 = 20;
let name = "Alice"; // 型推論で String と判定される型注釈は省略でき、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が文脈から型を推論する。Generics や trait と組み合わせた表現力の高い型システムを持つ。
型注釈の位置
ローカル変数は型推論が強いが、関数シグネチャは常に明示が必要。
let age: i32 = 20;let age: i32 = 20; // 変数宣言は省略可能
fn greet(name: String) -> String { // 関数シグネチャは常に明示
format!("Hello, {name}")
}`let`束縛はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が型推論するため型注釈を省略できるが、関数の引数・戻り値は常に`: Type`/`-> Type`という後置構文で明示しなければならない。「ローカルでは推論に任せ、公開インターフェースでは明示する」という境界線を言語仕様として強制している設計。
型変換
変換をtraitで表し、失敗し得る変換をTryFromとして区別する。
let y: i64 = i64::from(x);let x: i32 = 10;
let a: i64 = i64::from(x); // 失敗しない変換(From)
let b: i64 = x.into(); // Intoは自動で実装される
let big: i64 = 3_000_000_000;
let c: Result<i32, _> = i32::try_from(big); // Err(範囲外)
let d = big as i32; // 黙って切り捨てられる
let n: Result<i32, _> = "42".parse::<i32>(); // 文字列 -> 数値もResult`From`は失敗しない変換、`TryFrom`は失敗し得る変換にのみ実装され、変換の安全性が型で区別される。一方で`as`は縮小変換でも黙って値を切り捨てるため、失敗を見逃す抜け道が残っている。melangの「変換をinterfaceで定義し、失敗し得るものは`Result`で返す」方針の主な参考元。
暗黙の型変換
数値型間の暗黙変換を一切持たない。
let x: i32 = 10;let x: i32 = 10;
let y: i64 = x; // コンパイルエラー
let y: i64 = x as i64; // OK
let y: i64 = i64::from(x); // OK(Fromで安全性が保証される変換)i32からi64のような拡大変換であっても暗黙には行われず、`as`や`From`/`Into`による明示が必須。`as`は情報を失う変換も許すが、`From`は失敗しない変換にのみ実装される、というように変換の安全性を型で区別している。
Enum / Sum Type
enum自体がpayload付きのdiscriminated union。
enum Result<T, E> { Ok(T), Err(E) }enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
} // 全variantを網羅しないとコンパイルエラー
}enum自体がdiscriminated unionであり、各variantが異なるフィールド構成を持てる。matchは全variantの網羅をコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が要求し、variant追加時に既存matchの不足を検出できる。melangが目指すモデルに最も近い。
値の不変性
束縛は不変が基本で、`mut`を付けたときだけ再代入・変更できる。
let x = 1; / let mut x = 1;let v = vec![1, 2];
// v.push(3); // コンパイルエラー: mutではない
// v = vec![3]; // コンパイルエラー: 再代入もできない
let mut w = vec![1, 2];
w.push(3); // OK: mutを明示した束縛
w = vec![4]; // OK: 再代入
fn add(v: &mut Vec<i32>) { v.push(1); } // 変更可能な参照を渡す束縛は既定で不変で、`mut`を明示したときだけ再代入と変更ができる。`&mut`は同時に1つしか存在できないため、共有と変更が同時に起きない。ただし変更は言語機能として存在し、`mut`・借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。・所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を学ぶ必要がある。melangは変更そのものを設けないので、この仕組みを必要としない。
名前付き引数
名前付き引数はなく、意味を明示したい場合は構造体リテラルを渡す。
fn foo(width: i32, height: i32)fn foo(width: i32, height: i32) {}
foo(10, 20); // 引数名は呼び出し側に現れない
struct FooArgs { width: i32, height: i32 }
fn foo2(args: FooArgs) {}
foo2(FooArgs { width: 10, height: 20 }); // フィールド名で意味を明示言語機能としての名前付き引数はなく、これまでも提案はあるが採用されていない。意味を明示したい場合は、フィールド名を持つ構造体を渡すことが一般的な代替手段になる。
関数の引数
所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を型で管理し、move・借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。・可変借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。を使い分ける。
fn f(s: &String) / fn f(s: &mut String) / fn f(s: String)fn len(s: &String) -> usize { s.len() } // 借用(読み取り専用)
fn push(s: &mut String) { s.push('a'); } // 可変借用
fn take(s: String) {} // 所有権を受け取る
let mut s = String::from("a");
len(&s); // 呼び出し側にも & が付く
push(&mut s); // 可変であることも呼び出し側で分かる
take(s); // moveは呼び出し側に印がない。以降sは使えない引数の渡し方が「借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。するか、所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を移すか、変更を許すか」を型として表す。借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。は呼び出し側にも`&` / `&mut`が付くが、moveは印がなく、変数が消費されることは関数の定義を見ないと分からない。所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。と可変性を安全に扱うための仕組みであり、どちらも扱わないmelangでは採用しない。
if 文/式
`if`は式。`else`がなければ`()`を返すので、手続きと値の`if`が同じ構文になる。
if cond { a } else { b }// 値として使う: elseが必要
let x = if cond { 1 } else { 2 };
// 手続きとして使う: elseは任意
if cond {
process();
}
// let x = if cond { 1 }; // コンパイルエラー: elseがなく()になる`if`は式で、`else`がないと型は`()`になる。そのため値として使う場合は`else`が事実上必須になるが、構文は手続きの`if`と同じである。`else`の使い方に制限はなく、`if let ... else`や`let ... else`(早期脱出用)も用意されている。
I/O の明示
同期的なI/Oは、ただの関数呼び出しと区別されない。
fn read_file(path: &str) -> std::io::Result<String>fn read_file(path: &str) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
fn double(x: i32) -> i32 {
let _ = read_file("x"); // 呼べてしまう。コンパイラは止めない
x * 2
}`io::Result`はI/Oが失敗しうることを示す命名規約であり、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が強制するeffect注釈ではない。同期的なI/O呼び出しは通常の関数呼び出しと構文上区別されない。`async fn`は呼び出し元にも`async`か`.await`を要求し伝播する点で`io`に近いが、表しているのは「非同期に実行されうるか」であって、I/Oの有無そのものではない。`unsafe`が示すのはメモリ安全性のeffectで、I/Oとは別軸。
言語名
言語名とCLIコマンド名(`rustc`)が異なる。表記揺れはほぼない。
言語名は`Rust`だが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。のコマンド名は`rustc`、パッケージマネージャは`cargo`と、場面ごとに別の名前を持つ。言語名自体は一般名詞から離れた固有の綴りのため、検索時に他の意味と衝突しにくく、大文字・小文字の表記揺れも起きにくい。