無駄と文化

実用的ブログ

毎年3日間だけ花粉症に罹っている【定点観測】

ここ数年、春先に鼻炎になる。「花粉症か」「自分にもついに来たか」と思って薬を飲むのだが3日ほどすると治っている。そんなことが続くので冗談めかして「毎年3日間だけ花粉症になるんです」などと言っていた。

 

2024年、今年も花粉症の季節がやってきた。

そろそろ実態を解き明かしたい気持ちがあるので、定点観測1年目という気持ちでブログに書き残そうと思う。

 

◾️2024-02-25 sun

朝からやたらと目が痒かった。数週間前に結膜炎に罹っていたので、それが再発したか?と思いながら薬局で薬用の目薬を買って挿した。13時頃。

 

車で30分ほど走って実家に顔を出した。この時点でくしゃみが連続で出るようになっていた。ムズムズが止まらない。ただこの時点では風邪の初期症状のような気もしたし、実家でひさしぶりににペットの犬と触れ合ったことによるアレルギーのような気もした。

 

夜、入浴したら少しだけ落ち着いた。とはいえくしゃみは出る、鼻水も出る。鼻詰まりで顔がポッと火照る感じがする。何らかの鼻炎だなと思い薬局で鼻炎薬を買って飲んだ。21時頃。

 

深夜、喉が痛みだした。唾を飲み込むときに喉にゴロゴロとした違和感と痛みがあり、よく眠れなかった。

 

◾️2024-02-26 mon

朝から調子が悪い。目の痒み、鼻水、喉の痛みが相変わらずで、これは花粉症というやつでは?とかなり確信を得た。もともとハウスダストのアレルギー持ちでくしゃみが止まらなくなることがあったが、目や喉まで同時にやられるのは初めての経験だった。さらに言うと頭がボーっとして集中できない、深い思考が要求されるようなタスクができないなど、かなり苦痛だし仕事にも支障が出ていた。

再び薬局に行きアレルギ製の鼻炎薬を買って飲んだ。15時頃。

 

夜、服薬のおかげで喉の痛みは和らいだが、頭がふらつく感覚があった。昨晩は喉の痛みで寝不足だったのでそのせいかと思いながら就寝。

 

◾️2024-02-27 tue

朝からだいぶ弱っていた。元気が出ない、気力が湧かない、鼻が詰まる、頭がふらつく。ああこんな状態が数週間続くのかと軽く絶望しながら仕事を開始した。

相変わらず思考を要するタスクは思うように進まない。だんだんと眩暈がしてきて生唾が出るようになってきた。12時頃。

(私にとって生唾が出るのは吐き気を伴う体調不良の前兆だった)

 

昼休みを長めにもらってベッドに横になった。何となく寒気がする気がして羽毛布団に毛布を重ねて潜り込んだ。しっかりと暖かいセットアップのはずなのに横になっていて寒さを感じる。悪寒がするというやつで、花粉症ではなく風邪かもっとひどいウイルス性の何かを疑い始めた。15時頃。

 

横になって少しは症状が和らいだ気がしたので、起き上がって本日中に最低限終わらせておきたいタスクだけ消化することにした。終わったのは18時頃。

 

早めに退勤して夕食を済ませて、私用の Web 会議に参加して、風呂に入った。不思議なことに体調がかなり回復しているのを感じた。目の痒みはおさまり、鼻は通っている、喉の痛みもほぼない。ふらつきや吐き気もほぼ意識しなくてよくなっていた。22時頃。

もしかしてアレルギー性の鼻炎薬が体調に合わずにフラフラになっていたのか?と疑って、その日の晩の薬は飲まずに寝た。

 

◾️2025-02-28

朝起きる。完全回復という感じではないが、昨日に比べるとだいぶ体調が良いのを実感する。何よりも頭が働く。ちゃんとした思考ができる。13時頃。

 

結局、薬を飲まないままくしゃみも喉の痛みも治ってしまった。2024年度の私の花粉症さ治った。

 

 

いったいこれは何なんだ?経過を見て判断しようと思う。

 

型無しラムダ計算学習用ステップ評価器 skiMogul を作っている話

みなさま ラムダ計算 をご存知でしょうか。
ラムダ計算はある種の関数型プログラミング言語の体系で、変数と関数, そして関数適用というミニマルな構成要素だけでチューリング完全な表現力を持っています。

この記事では、ラムダ計算の中でも特に単純な 型無しラムダ計算 に着想を得てラムダ計算のステップ評価器を作っている話について書きます。

 

既存のモデルへの不満

型無しラムダ計算の処理系は昔からいくつも実装されていて、有名なところで UnlambdaLazy_K などがあります。
どちらも SKI コンビネーター理論 に基づいていて、 s, k, i という たった3つの組み込み関数でプログラムを記述することが可能 1 です。

かく言う私も Unlambda から型無しラムダ計算に入門したクチです。しかし、従来の処理系には長らく不満がありました。それは関数の計算過程が印字不可で、計算の過程が非常に見えづらいことです。
早い話 Unlambda や Lazy_K は print デバッグできないのがツラい という事なのですが。私の見立てではこの print デバッグで試行錯誤できない感が ラムダ計算の世界から入門者を遠ざけている要因になっている ように思います。

 

ラムダ計算の計算過程を可視化する

そこで、『無いものは作ればいい精神』です。型無しラムダ計算学習用ステップ評価器 skiMogul を作りました。
実際の動作を見てもらう方がイメージしやすいと思うので GIF を、

型無しラムダ計算学習用ステップ評価器 skiMogul

mogul-lang.mudatobunka.org

ラムダ式をステップ評価する様が見て取れると思います。
今のところ

  • 論理演算 (NOT, AND, OR, XOR)
  • 整数(チャーチ数)の操作 (後者関数 SUCC, 前者関数 PRED)
  • 整数(チャーチ数)の演算 (和 ADD, 差 SUB, 積 MUL, 商と剰余 DIV)
  • 整数(チャーチ数)の同士の比較 (ゼロ比較 IS_ZERO, 等値比較 EQ, 不等比較 GT, LT)
  • 連結リスト操作 (CONS, CAR, CDR)
  • 無名再帰 (Y コンビネータ)
  • 自作関数の定義

などが出来ます。

 

シンタックス

skiMogul の記法について簡単に解説してみます。

関数適用 (関数実行)

まず、 i という組み込みの関数があります。 Identity の略で、引数をそのまま返す関数です。

> i
i

このように、i 単体を評価しても何も起きません。

では、関数 i に引数 x を与えて実行してみましょう。JavaScript 風にパーレン () で関数呼び出しを書きます。

> i(x)
i(x)
⇒ x

skiMogulで実行する

はい、実行結果は x です。
このように関数に引数を与えて実行することを 関数適用 と云います。この場合 ix を適用しました。

 

他にも組み込み関数を使ってみましょう。定数関数 k は2つの引数を取り、2つめの引数を捨てて1つめの引数をそのまま返します。
k は Constant (Konstant) の略ですね、たぶん。

k(x, y)
⇒ x

skiMogulで実行する

「2つの引数を取る」と書きましたが、これは正確な表現ではありません。厳密には Mogul の世界には多変数関数が存在せず、1引数関数しか扱えないからです。
k(x, y)k(x)(y) と同じ意味に解釈されます。これは kx を適用した結果に y を適用していると読めます。このように1変数関数を用いて多変数の関数適用を表現する方法を「関数のカリー化」と言います 🍛

 

もう一つ欠かせない関数 s を使ってみましょう。
sSubstitution の略で、3引数 x, y, z を与えると x(z, y(z)) を返します。

> s(x, y, z)
s(x, y, z)
⇒ x(z, y(z))

skiMogulで実行する

関数合成引数の入れ替え を実現するために重要な関数です。

関数抽象と関数定義

ラムダ計算に欠かせないのが 関数抽象 です。
モダンなプログラミング言語に親しんだ方であれば 無名関数 または クロージャー (closure) の事だと云えば伝わりやすいでしょうか。

例として、引数 x に対して x(x) を返す関数抽象を以下のように書きます。

x => x(x)

関数抽象に引数を与えて評価することができます。 a を適用してみましょう。

> (x => x(x))(a)
(x => x(x))(a)
⇒ a(a)

skiMogulで実行する

いいですね。

関数定義

関数抽象は 別の変数に代入する こともできます。

> FOO = x => x(x)
> FOO(a)
FOO(a)
⇒ (x => x(x))(a)
⇒ a(a)

skiMogulで実行する

このようにして自作関数 FOO を定義することができました。これが 関数定義 です。

多変数関数

多変数関数は関数抽象をネストさせることで実現します。

> x => y => y(x)
x => y => y(x)

> (x => y => y(x))(a)(b)
(x => y => y(x))(a)(b)
⇒ (y => y(a))(b)
⇒ b(a)

skiMogulで実行する

上手く動いていますね。
このように1引数の関数抽象をネストさせて多変数関数を表現する方法を カリー化 と云います。

ちなみに、skiMogul では多変数風の関数抽象の糖衣構文を用意しているので下記のように書くこともできます。

> (x, y) => y(x)
(x, y) => y(x)

> ((x, y) => y(x))(a, b)
((x, y) => y(x))(a, b)
⇒ (y => y(a))(b)
⇒ b(a)

関数の定義を参照する

ここまでに見てきたように、 Mogul では関数適用を変数に代入することで自作の関数を定義することが出来ます。
逆に定義済みの関数の定義を参照するには ? コマンド を使います。

> ? s
s(x, y, z) = x(z, y(z))

skiMogulで実行する

Mogul には他にも多くの組み込み関数が定義されています。 Context パネルで定義済みの関数を一覧できます。
また ? コマンドを単体で実行することでも同じように見ることができます。

> ?
i(x) = x
k(x, y) = x
s(x, y, z) = x(z, y(z))
Y(f) = (x => f(x(x)))(x => f(x(x)))
...
XOR(x, y) = x(NOT(y), y)
ι(f) = f((x, y, z) => x(z, y(z)), (x, y) => x)

skiMogulで実行する

skiMogul で計算っぽいことをやってみる

遊んでみましょう。

論理演算

論理演算のために必要な一通りの関数が用意されています。
TRUE, FALSE, NOT, AND, OR, XOR などです。

> AND(TRUE, FALSE)
AND(TRUE, FALSE)
⇒ TRUE(FALSE, FALSE)
⇒ ((x, y) => x)(FALSE)(FALSE)
⇒ (y => FALSE)(FALSE)
⇒ FALSE

> AND(TRUE, NOT(FALSE))
AND(TRUE, NOT(FALSE))
⇒ TRUE(NOT(FALSE), FALSE)
⇒ ((x, y) => x)(NOT(FALSE))(FALSE)
⇒ (y => NOT(FALSE))(FALSE)
⇒ NOT(FALSE)
⇒ FALSE(FALSE, TRUE)
⇒ ((x, y) => y)(FALSE)(TRUE)
⇒ (y => y)(TRUE)
⇒ TRUE

> AND(OR(FALSE, TRUE), NOT(FALSE))
AND(OR(FALSE, TRUE), NOT(FALSE))
⇒ OR(FALSE, TRUE)(NOT(FALSE), FALSE)
⇒ FALSE(TRUE, TRUE)(NOT(FALSE), FALSE)
⇒ ((x, y) => y)(TRUE)(TRUE, NOT(FALSE), FALSE)
⇒ (y => y)(TRUE)(NOT(FALSE), FALSE)
⇒ TRUE(NOT(FALSE), FALSE)
⇒ ((x, y) => x)(NOT(FALSE))(FALSE)
⇒ (y => NOT(FALSE))(FALSE)
⇒ NOT(FALSE)
⇒ FALSE(FALSE, TRUE)
⇒ ((x, y) => y)(FALSE)(TRUE)
⇒ (y => y)(TRUE)
⇒ TRUE

skiMogulで実行する

IF を使うと if式 っぽいものを書くこともできます。

> IF(FALSE)(x, y)
IF(FALSE, x, y)
⇒ FALSE(x, y)
⇒ ((x, y) => y)(x)(y)
⇒ (y => y)(y)
⇒ y

整数演算

020 までの整数 2 は定義済みです。
ADD, 差 SUB, 積 MUL, 商と剰余 DIV を計算することもできます。

> ADD(2, 3)
ADD(2, 3)
⇒ (f, x) => 2(f, 3(f, x))

この評価結果はラムダ計算的に正しいのですが、最終結果を見ても釈然としないかも知れませんね。
等価比較 EQ を使って ADD(2, 3)5 に等しいことを確かめてみましょう。

> EQ(ADD(2, 3), 5)
EQ(ADD(2, 3), 5)
⇒ AND(GTE(ADD(2, 3), 5), LTE(ADD(2, 3), 5))
⇒ GTE(ADD(2, 3), 5)(LTE(ADD(2, 3), 5), FALSE)
⇒ IS_ZERO(SUB(5, ADD(2, 3)))(LTE(ADD(2, 3), 5), FALSE)
...
⇒ (u => TRUE)(_ => FALSE)
⇒ TRUE

skiMogulで実行する

結果は TRUE、つまり 2+3 は 5 に等しいということですね。

 

実装

skiMogul は Rust で実装されています。
Rust コードを wasm にコンパイルして GitHub Pages にデプロイしています。計算のためにサーバーと通信することはありません。すべての計算はフロントエンドで行われます。つまり全てがブラウザ上で実行されているということです。

全てのソースコード は GitHub に置いています。

 

今後の展望

最初にも書いたように、私が skiMogul を書き始めた動機は ラムダ計算の入門者のハードルを下げること です。だいたい6~10歳くらいの小学生がシンプルに自然にラムダ計算を学び始められる世界を夢見ています。
skiMogul の機能について要望・意見があれば教えてもらえると嬉しいです。

 

 

私からは以上です。


  1. Unlambda には3つの主要な関数以外に、評価順を制御する d 関数や印字用の関数群なども多数組み込まれていますが
  2. ただし、チャーチ数と云われる関数として表現された数です

Rust はコストのかかるコードを書くのが苦痛になるようにデザインされている(?)

Rust は「コストのかかるコードは書くのが苦痛であるべき」という思想のもとデザインされている気がする。
というのも Rust を書いていると「意図的にショートカットが用意されていない」と思える場面があるからだ。

いくつか例を挙げてみようと思う。

 

文字列の連結

String 型の変数 s を所有しているときに、s の 後ろに 文字列リテラルを連結するのは簡単だ。+ 演算子で文字列の連結ができる。

let mut s = String::from("hello");
s = s + ", world";
println!("{}", s);  // => hello, world

一方で s の 前に 連結するには一手間必要になる。

let mut s = String::from("world");
s = "hello, ".to_string() + &s;
println!("{}", s);  // => hello, world

+ 演算子の左オペランドは String でなければいけないので "hello, " をそのまま置くことはできない。.to_string() メソッドで String 型に変換している。1
右オペランドは &str 型でなければいけないので &s として &str に変換している。

 

文字列の連結がなぜこんなにも左右非対称なのか、実装を追ってみよう。

+ 演算子の挙動は Add trait を実装することで定義される。

doc.rust-jp.rs

Rust では Add<&str> for String が定義済みなので + 演算子を使って String 型と &str 型が連結できる。
他方で Add for &str は未定義なので、&str 型と String 型の連結はできない。2

これによって String 型の変数 s を後ろに伸ばしていくのは簡単で、前に伸ばしていくためには冗長な記述が必要という非対称性が生まれている。

 

このような状況はおそらく意図的で。&str + String がシンプルに書けないのは &str + String を実行するのにコストが大きいからだ。

Rust の String 型は内部的に Vec として実装されている。Vec は「8ビット非負整数の可変長配列」だ。

Vec はデータを前から詰めて保持するために、後ろに伸ばすのは容易で前に伸ばすのにはコストがかかる。String 内部的には Vec なので同じ制約下にあるはずだ。

ちなみにここで言う「前に伸ばすのにはコストがかかる」というのは、連結後の文字列が収まるメモリを別の場所に確保してから連結後の文字列を配置し直す必要があるということだ。
String + &str はあらかじめ確保していたメモリに連結後の文字列が収まらなかったときだけ再配置すればいい一方で、&str + String は 毎回かならず メモリの再配置が必要になる。

そういう都合で String + &str は簡単に書けて、&str + String には意図的にショートカットが用意されていないんじゃないだろうか。

 

ユーザー定義型は (可能であっても) 自動的には Copy にならない

2つめの例は値の複製について。

Rust には言わずと知れた所有権システムがある。*1
Rust の基本戦略は値を所有している変数を一つだけに制限して、できるだけ借用 (参照) で間に合わせるというものだ。

そのため Rust の代入は基本的に所有権を移動 (ムーブ) させる。

let x = vec![1, 2, 3];
let y = x;  // ここでムーブが発生し x は所有権を失う
println!("{:?}", x);
//               ^ エラー: ムーブ済みの値を借用しようとしている

この制限を回避する方法はいくつかある。

  1. 変数を .clone() する
  2. 型を Copy にしておく
  3. Rc などの型で包む

いちばんよく使われるのは 1. だろう。
Rust の多くの型は .clone() メソッドを実装していて、値を複製して別の変数に渡すことができる。

let x = vec![1, 2, 3];
let y = x.clone();  // x を複製して値の所有権を y に渡す
println!("{:?}", x);  // => [1, 2, 3] 
//              ^ OK: x の所有権は失われていない

.clone() はお手軽だが、所有権がムーブする箇所で一々 .clone() をタイプするのは面倒でもある。もし型が Copy trait を実装していれば .clone() を省略できる。
Copy trait を実装した型は所有権がムーブするタイミングで自動的に値が複製される。元の変数が所有権を失うことはない。

let x = 42;  // 42: u8 は Copy trait を実装している
let y = x;   // .clone() を書かなくても自動的に複製される
println!("{:?}", x);  => 42
//              ^ OK: x の所有権は失われていない

char, bool, u8 などが Copy になっている。
さらに Copy な型を組み合わせたタプルもまた Copy になる。例えば (char, bool, u8) は Copy だ。

let x = ('🦀', true, 42);  // (char, bool, u8) は Copy になる
let y = x;                 // .clone() を書かなくても自動的に複製される
println!("{:?}", x); // => ('🦀', true, 42)
//              ^ OK: x の所有権は失われていない

 

Copy な型はいわば .clone() を免除されているわけだ。
これは「Copy な型が占有するメモリ」が「それの参照が占有するメモリ」に比べて充分に小さいからだ。

Rust において char は4バイト, bool は1バイト, u8 は1バイトだ。一方それらの参照である Box, Box, Bool のサイズはどれも1バイトだ。
u8 な値を二つの変数に持たせるのに、値そのものを持たせるのも片方に参照を持たせるのもメモリサイズは変わらない。
参照を持たせることで節約にはならないし、暗黙に .clone() したとしても無駄遣いにはならない。

というわけで Copy な型は手動で .clone() する必要がなく、代入時に値が自動的に複製される。

 

さて「Copy のタプルも Copy になる」ということを踏まえてユーザー定義型について見てみよう。
Copy な型だけを含む struct や enum は Copy に なれる#[derive(Clone, Copy)] で修飾すればいい。

#[derive(Clone, Copy)]
struct Foo { val: u8 }

let foo = Foo { val: 42 };
let bar = foo;
println!("{}", foo.val); // => 42

なんとも簡単だが 自動的に Copy になる訳ではない。

ユーザー定義型が (可能であっても) 自動的に Copy にならない理由は「将来的に拡張されて Copy でない型を含むようになったときに困るから」と説明されている。
Copy でない型を含む型はどうがんばっても Copy になれない。将来的にも Copy な型しか持たせないことを心に決めたうえで、実装者が責任を持って #[derive(Clone, Copy)] をタイプする必要がある。

 

というわけで実はユーザー定義型を安易に Copy にするべきではないのだ。現実的なユースケースではムーブを回避したいときには一々 .clone() することになる。

そういう意味で Rust は 一々 .clone() をタイプさせるように デザインされていると言える。

プログラマが .clone() をタイプするときデータが複製されている様を意識せずにはいられない。複製元の値と同じだけメモリが消費されてる様を意識するというストレスがかかるわけだ。
そういう少しのストレスによって Rust はプログラマに極力 .clone() せずに済むコードを書くように仕向ける。

一々 .clone() をタイプするのは苦痛で。ストレスが無いのは一つの値を一つの変数だけが所有しているコードだ。
苦痛から逃れるコードを書こうとすると、自然に「値の複製」も「ガベージコレクション」も必要のないコードになっていく。

所有権システムの制約も Copy な型の制約も、理想的なコードを書かせるための意図したデザインと思えてくる。

 

Vec が map() メソッドを持たない

Rust の Vec は便利だ。色々なメソッドがあり、ほとんどが安全だ。
が、とある不思議なことに気づく。Vec には .map() メソッドが無い。

.map() メソッドは Iterator に定義されている。
Vec の中の値を変換しようとしたら、

  1. まず Vec を Iterator に変換し
  2. .map() して
  3. 結果の Iterator を再び Vec に変換する

という手順を踏む必要がある。

とはいえこれらの手順はどれも簡単だ。
Vec から Iterator を作るには .into_iter() メソッドを呼び出すだけでいい。
Iterator から Vec の変換は .collect() メソッドがやってくれる。

let vec = vec![1, 2, 3];

let iter = vec.into_iter();
let iter = iter.map(|n| n * n);
let vec: Vec<u8> = iter.collect();

println!("{:?}", vec);  // => [1, 4, 9]

簡単ではあるが、いかにも冗長だ。

いちいち変数に格納せずともメソッドチェーンで書くこともできる。そうするといくらかスッキリする。

let vec = vec![1, 2, 3];

let vec: Vec<u8> = vec.into_iter().map(|n| n * n).collect();

println!("{:?}", vec);  // => [1, 4, 9]

が、やはり冗長じゃないか?

Vec が .map() を持たない背景には遅延評価的な思想がある気がしている。
ここでいったん脇道に逸れて Iterator の .map() メソッドについて見てみよう。

 

Iterator の .map() は Iterator が持つ値を変換するメソッド ではない

.map() は std::iter::Map を返すように実装されている。std::iter::Map は別の Iterator と「値を変換する関数 f」を内包した構造体だ。
こんな感じの実装になっている。

pub struct Map<I, F> {
    pub(crate) iter: I,
    f: F,
}

impl<B, I: Iterator, F> Iterator for Map<I, F>
where
    F: FnMut(I::Item) -> B,
{
    type Item = B;
    fn next(&mut self) -> Option<B> {
        match self.iter.next() {
            Some(v) => Some(self.f(v)),
            None => None,
        }
    }
}

std::iter::Map は値を変換可能な状態を保持しているだけで、値が必要になるまで変換は行わないのだ。
「値が必要になったとき」とは iter.next() が呼ばれたときだ。

 

std::iter::Map の存在を知れば .map() の見方も変わってくる。Iterator の .map() は変換可能な状態を作っているだけ。

必要になるまで変換を行わないのは、全ての値に対して変換を行わなくてもいい場合があるからだ。
例えば .skip() でいくつかの要素を読み飛ばしたり .find() で特定の要素だけをピックアップしたときには、捨てられる要素に対しての変換は省略できる。

Rust はコレクションの中に使われない値があるかもと想定しているので「必要になったときに初めて値を用意する」という処理を抽象化して用意している。それこそが Iterator だ。

 

というわけで Vec を .map() するときに Iterator を経由するのは使われない値があるときに備えてのことだと言える。
Vec に .map() を直接実装してしまうと、そのような効率化のための意図は消し飛んでしまう。タイプ数が減る代わりに非効率さが覆い隠される。

プログラマが vec.into_iter().map().collect() をタイプするときに思うことは、どうにかして .collect() を省けないか?ということだ。
.collect() を呼んだ時点で内部的に .next() が呼ばれて、遅延されていた std::iter::Map の処理は即座に実行されていまう。もし必要ない要素があるなら、Vec に変換することなく処理を進めるのが効率がいい。

Vec を .map() するための冗長さは裏側で起こっていることをプログラマに思い出させるためにある。

 

Rust はコストのかかる箇所にマークをつける

Rust を学んでいて面白いと思ったのは、Rust のデータ型はコストのかかる処理を覆い隠したりしないことだ。
むしろコストのかかる箇所が見えるように個別の型やトレイトが用意されている。

この記事では、

  • 後ろ側に連結が容易な String 型
  • 自動的な複製を許す Copy トレイト
  • Lazy な Iterator トレイトと正格な Vec 型

を例に挙げた。

これらの型やトレイトはプログラマが見て裏側の処理の性質を知るのにも役立つし。もっと直接的に、とあるトレイトの有無を条件に制約を課すガードとしても使われている。*2

 

コストのかかる処理をマークするための型があることで、マークするためにわずかにタイプ数が増える。そうやってわずかにストレスを感じさせて、コストのかかる処理を避けるよう誘導しているんじゃないだろうか。

もちろんコストのかかる処理をカジュアルに書ける記法を用意することも技術的に可能だ。でも Rust はそのようにデザインされていないと感じる。むしろコストのかかる箇所が明確に見えるように、コストのかかる処理を書いていて苦痛に感じるようにデザインされている気がする。

 

まとめ

ここで紹介した冗長さはどれも意図的だと私は思っている。
Rust を書いていて苦痛なときは「もっと効率のいい書き方がある」というメッセージを受信しているときだ。

Rust は苦痛を型で表現しているのが面白い。
文字列連結の例では String と &str が、値の複製の例では Copy が、.map() の例では Iterator や std::iter::Map が。これらの型はコストを覆い隠すのではなく、そこにコストがあることを表明している。

この記事を読んで「いや Rust でもコストが覆い隠されているデザインはあるぞ」とか「他の言語ではこういう工夫がされているよ」とかの意見や知見があればぜひ教えてほしい。
私もこの意見に確信があるわけではなく、まだまだ考え始めたところだ。

 

 

私からは以上です。

 

おまけ

X でアンケートを取ってみたら見事に真っ二つに割れた図


  1. Rust では文字列リテラル "some string" は &str 型に解決される
  2. 連結不可能というわけではなく、左オペランドを String 型に右オペランドを &str 型に変換しておく必要があるということ

*1:そしてガベージコレクションが無い

*2:いわゆる「トレイト境界」