『Rustで作るプログラミング言語』を読む Part2
2024/7ごろ読んで挫折して、再チャレンジ。
3.2
whitespace
、ident
、number
の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()
文字列スライス&str
をchar
型のイテレーターのChars
型に変換する。&str
型のメソッド。
next()
イテレーターの次の要素を取得するイテレーターメソッド。Optioin型を返すので、matches!()
の比較ではSome
で包んでいる。
matches!(value, pattern)
value
をpattern
を比較し、マッチしたら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 全体的に変わってる
全体的に返り値が変わってる
ident
、number
、lparen
、rparen
の返り値が変わった。以前は(&str, Option<Token>)
タプルを返していたが、3.5からはOption<(&str, Token)>
のようにタプルをOption型に格納している。
fn token(input: &str) -> (&str, Option<Token>)
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]
のように文字列を取得している。
サンプルコードでは1行にしているが読みづらいので、Rustに慣れてないなら一時変数を使って書くと読みやすくなる。
let ident_length = start.len() - input.len();
let ident = &start[..ident_length];
Some((
input,
Token::Ident(ident)
))
ポップした差分を知りたいから関数の一行目にstart
変数にinput
を代入している。
スライスから文字列を取り出す
ident関数でパータンにマッチする英数字の文字列を取り出すとき&start[]
と書いている
number関数でパターンにマッチする数値を取り出すときstart[]
と書いている。
どっちの書き方も等しい。表記ゆれがあるとモヤモヤするので&をつけないstart[]
の書き方で書く。