let x = x;
という書き方が許されているOption<T>
またはResult<T, E>
を使用する?
演算子を使うことで簡潔に記述できるunwrap
やexpect
は今後修正すべき箇所のマーカーの役割を持つRc<T>
やRefCell<T>
を理解したいときは有向グラフで考えるlet
文はパターンマッチであるref
やref mut
を使う今ならremove_first
関数をもっと綺麗に書ける。成長しているみたいで嬉しい。
インデントは2にしている。C++を書いているときの感覚と同じにしたいから。
関数の戻り値としてString
や構造体が普通に使える理由について考える。
これらの実体はヒープに格納されているため、関数から抜けても確保し続けることができる。呼び出し元にはその領域を指すポインタを返すと考えれば良さそう。そしてそれは、所有権を呼び出し元に移すことを意味する。
指すヒープ領域が関数内で作成した構造体ではなく引数として渡された構造体であるとき、ライフタイムを考える必要がある。
パターンの中で&
や&mut
を使いたくなったときにref
やref mut
を使う。
パターンに一致したときに所有権の移動が起こるため。
Defer
トレイトの項を読み終えた。
Defer
トレイトを実装している型の変数に対しては*
をつけることができ、これをつけることで参照を外すことができる。イメージ的にはポインタから値を得る感じ。
Box
型はDefer
トレイトの実装であるため、*
によって参照を外すことができる。そして、もしBox<String>
であるなら、String
もまたDefer
トレイトの実装であるため*
が使える。よって、let x: Box<String>;
は**x
といった記述が可能。
…だと思うけど、今実験すると**x
は&str
型ではなくstr
型になった。String
の参照を外すとstr
になるっぽい?
所有権を剥奪する仕組みの有難みが並行プログラミングを学んでいるとわかる。
std::mpsc::channel()
により作成したチャンネルのtx.send()
を使って変数val
のデータを送信すると、その送信後にval
が使えなくなる。これは、受信側にval
の所有権が移ったから。意味的には、「送信したデータをまだ扱えるのなら、送信側が変更できたりしてしまっておかしくなるよね」となる。
Rustのルールと並行プログラミングの考え方がマッチしていて面白い。
rx
がイテレータとして扱えるのが好き。
エラー処理の項を読み終えた。
Result
を使うかpanic!
を使うかの判断基準が明確に定められていて面白かった。
あと、データの正当性チェックを外部に追い出すためにenum
とimpl
を使うというのはなるほどと思った。
ライフタイムについてある程度理解した。
ライフタイム注釈を行ったからといって、ライフタイムを長くしたり短くしたりすることはできない。
そして、同じライフタイム注釈だからといってライフタイムの長さを同じにしないといけないというわけではない。このあたりはジェネリクスと異なる。
チュートリアルを再開してからまだ一度もトレイトを使っていないので、これからトレイトを使ってみる。
new
がResult<T, U>
を返してもいいとは思わなかった。
それはしてはいけないことだと思っていた。
所有権を奪うような実装は避けるべきだと思っていたけど、実はそうではないみたい。リファクタリングにより、Config::new()
の引数を借用ではなく所有権を奪う形に修正している。こうすることで値のクローンが発生せずに効率的とも書かれている。
「再帰的な型を定義するためにBox
を使用する」というのは、TypeScriptの型システムを理解するときの手助けになりそうな概念。コンパイラが列挙型のサイズを決定するとき、列挙されているデータのうちで最も大きいサイズのものにする。ここで、たとえばenum List { Cons(i32, List), Nil }
のように定義してしまうと、Cons(i32, List)
のサイズを決めるためにList
の定義を見に行くけど、そこでもまたCons(i32, List)
が存在して…というように再帰的になっており、サイズを決めることができなくなる。そこでCons(i32, Box<List>)
とすることで、Cons(i32, Box<List>)
のサイズはi32
と、ヒープに存在するList
型への参照の大きさを合わせたサイズだとわかる。
Rustのチュートリアルを進めている。
先ほど「8.3. ハッシュマップ」を終えた。そして演習問題を解いた。
解くことはできたけど、もっと簡潔な方法が存在する気がする。
知らないプログラミング言語を使うといつもこの感覚を味わう。
C++を学びたての頃も、文字列の各文字にアクセスする方法が添え字アクセス以外にも存在し、そちらのほうが良いと思っていた。
しかし実際は、確かに文字列をイテレータのように扱う方法は存在するが、それよりも添え字アクセスのほうが自然であり高速である。
このように、最初に学んだ方法が悪い方法ではないこともある。
特に今回はRustの公式のチュートリアルを進めているので、学ぶ方法が悪い方法でないことは確かだ。もっと効率の良い方法は存在するかもしれないが、基礎さえ見についていればおそらくすぐに理解できる。
次のような問題を考える。
文字列を受け取ると、先頭文字を取り除いた新たな文字列を返す関数を作成してください。もし元の文字列が空文字である場合はNoneを返してください。
この問題を次のように実装した。
次のような不安がある。
s.chars().enumerate()
という記述が長い。もっといい書き方がありそうret
という変数を新たに用意する必要はあるか(もっと関数型っぽく書けないか)remove_first
の引数の型は本当に&str
でいいかOption<String>
で最適かどうか面白い文章を見つけた。
公式の文章に、ある関数の戻り値の型を調べる方法として、APIのドキュメントを確認するほかにもコンパイラに確認することもできると書かれていた。わざとコンパイルエラーを発生させて型を確認するという方法が公式で認められているとは思わなかった。
Rustのチュートリアルを進めていると、仕組みに感動したり常識が覆されたりする。
Result<T, E>
とOption<T>
がたまにごっちゃになる。
let f = f;
みたいな書き方が許されているのが面白い。他の言語では、このような書き方をすると「既に変数f
が宣言されています」というエラーが発生するmut
がないベクタ配列は変更不可なのが安心する。JavaScriptでは、const arr = [];
としても配列に要素を追加できるので安心できない?
演算子を使うことで冗長なパターンマッチのコードを簡潔にできる?
演算子は、Result
を返す関数でしか使用できない」とあるが、実際にはOption
を返す関数でも使用できるunwrap
やexpect
が「エラーの処理方法を決定する前のプロトタイプの段階で便利。プログラムをより良くするときのマーカーの役割も持つ」というように表現されていて面白い。Rustは、いきなり洗練されたコードを書くことを要求しない。その場凌ぎのコードを書くことも許容する。ただ、それはマーカーとしてコード内に残る。やっぱりRust好き