Open4

『Rustで作るプログラミング言語』を読む Part2

ひげひげ

2024/7ごろ読んで挫折して、再チャレンジ。

ひげひげ

3.2

whitespaceidentnumberの3つの関数を作る。これらは単体で使うのではなくて合わせて使う。実際、main関数には、次のようにident(whitespace(number()))と、3つ続けて関数を適用している。

fn main() {
    let source = "123 world";
    println!(
        "source: {}, parsed: {:?}",
        source,
        ident(whitespace(number(source)))
    );
}

コードと構文

fn whitespace(mut input: &str) -> &str {
    while matches!(input.chars().next(), Some(' )) {
        let mut chars = input.chars();
        chars.next();
        input = chars.as_str();
    }
    input
}

引数のinputの先頭にある連続した半角スペースを全て取り除くメソッド。

chars()
文字列スライス&strchar型のイテレーターのChars型に変換する。&str型のメソッド。

next()
イテレーターの次の要素を取得するイテレーターメソッド。Optioin型を返すので、matches!()の比較ではSomeで包んでいる。

matches!(value, pattern)
valuepatternを比較し、マッチしたらtrueを、しなければfalseを返す。Rustの組み込みマクロ。input.chars().next()と比較していることより、先頭が半角空白と一致しているか確認している。matches!boolを返すだけなのでwhile letみたいな構文とは違う。普通にtrueが出るまでループを回しているだけ。

UTF-8 からの文字列の取り出し

whileの条件で確認していることは先頭の文字列を取り出すだけだから、他の言語のようにinput[0]のようにアクセスすればいいと思うが、そうするとエラーが出る。

    let c = "abc";
    println!("{}", c.chars();
error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> Main.rs:6:22
  |
6 |     println!("{}", c[0]);
  |                      ^ string indices are ranges of `usize`
  |
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`
  = note: you can use `.chars().nth()` or `.bytes().nth()`

コンパイラでもchars()を使ってアクセスしろと言っている。
先頭の文字にアクセスするのに、.chars().next()を使うのはプログラミングRust p396にも書かれている由緒正しい書き方。(ゆえにもっと簡単な書き方がないことも分かり残念な気分になる。)

indent関数

fn indent(mut input: &str) -> &str {
    if matches!(
        input.chars().next(),
        Some(_x @ ('a'..='z' | 'A'..='Z'))
    ) {
        while matches!(
            input.chars().next(),
            Some(_x @ ('a'..='z' | 'A'..='Z' | '0'..='9'))
        ) {
            let mut chars = input.chars();
            chars.next();
            input = chars.as_str()
        }
    }
    input
}

パターンのテスト

入力 出力
abc 先頭が英字なのでifブロックに入る。whileでb,cがスキップされて何も残らない。
123abc 123abc 先頭が数字なのでifブロックに入らずそのまま出力
method() () 先頭が英字なのでifブロックに入る。whileでe,t,h,o,dがスキップ。()が残る

x @ pattern
アットパターンはパターンであり値ではないのでそれ単体で使えない。値ではないから型も持たない。match式のマッチアームの=>の左側や、match!マクロの第二引数など、パターンが必要とされる場所に書ける。@パターンは、マッチした値の部品に対してそれぞれ新しい変数を作るのではなく、変数xを一つだけ作って、値全体を移動もしくはコピーする。
今回は_xに移動しているが、_xは使ってないので@パターンは不要。

main関数は本に書いてない

これをコピペ。

fn main() {
    let source = "123 world";
    println!(
        "source: {}, parsed: {:?}",
        source,
        ident(whitespace(number(source)))
    );
}
ひげひげ

3.3

この時点ではインラインのコードしか受け付けていない。

ひげひげ

3.5 全体的に変わってる

全体的に返り値が変わってる

identnumberlparenrparenの返り値が変わった。以前は(&str, Option<Token>)タプルを返していたが、3.5からはOption<(&str, Token)>のようにタプルをOption型に格納している。

3.4の内容
fn token(input: &str) -> (&str, Option<Token>) 
3.5の内容
fn token(input: &str) -> Option<(&str, Token)> {

この節の説明の省略具合はえぐい。

Tokenの変更点

p94を見るとIdent("Hello")とあるようにトークンが値を持っている。なのでToken型を定義するときヴァリアントによって値を持たせる必要がある。

#[derive(Debug, PartialEq)]
enum Token<'src> {
  Ident(&'src str),
  Number(f64),
  LParen,
  RParen,
}

identの変更点

3.4ではinputで受け取った文字列がパターンにマッチしたらトークンを返すだけで、マッチした文字列の情報は保存しなかった。3.5ではではパターンにマッチした文字列をヴァリアントに保存する。そこで、スライスからポップした文字数をカウントし、input[...length]のように文字列を取得している。

https://github.com/msakuta/ruscal/blob/ed869ab38ba0608b75ec63040bcc06eb8a6fc5d7/examples/03-tree.rs#L108

サンプルコードでは1行にしているが読みづらいので、Rustに慣れてないなら一時変数を使って書くと読みやすくなる。

        let ident_length = start.len() - input.len();
        let ident = &start[..ident_length];
        Some((
            input,
            Token::Ident(ident)
        ))

ポップした差分を知りたいから関数の一行目にstart変数にinputを代入している。
https://github.com/msakuta/ruscal/blob/ed869ab38ba0608b75ec63040bcc06eb8a6fc5d7/examples/03-tree.rs#L94

スライスから文字列を取り出す

ident関数でパータンにマッチする英数字の文字列を取り出すとき&start[]と書いている
https://github.com/msakuta/ruscal/blob/ed869ab38ba0608b75ec63040bcc06eb8a6fc5d7/examples/03-tree.rs#L108
number関数でパターンにマッチする数値を取り出すときstart[]と書いている。
https://github.com/msakuta/ruscal/blob/ed869ab38ba0608b75ec63040bcc06eb8a6fc5d7/examples/03-tree.rs#L129
どっちの書き方も等しい。表記ゆれがあるとモヤモヤするので&をつけないstart[]の書き方で書く。

source 関数の変更点