← melang

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`と、場面ごとに別の名前を持つ。言語名自体は一般名詞から離れた固有の綴りのため、検索時に他の意味と衝突しにくく、大文字・小文字の表記揺れも起きにくい。