読者です 読者をやめる 読者になる 読者になる

Rust で 言語処理100本ノック 第1章 前半

Rust

2016/12/16 追記:この記事のコメントで私の煩雑コードにアドバイスしてくださった方々、本当にありとうございました!多くの学びを得られましたことをここで御礼申し上げます(稚拙ながらコメントの返信をしました)。



これは Rust Advent Calendar 2016 の 14日目の記事です。

初心者枠なので、濃密でためになる情報はありません。ごめんなさい。


今年の秋頃から趣味でのろのろとRustについて調べ始め、最近勉強がてらあの有名な言語処理100本ノック をやってみています。

言語処理100本ノックはPythonを想定して問題作成されている事もあってか、Rustで書こうとした時に色々苦労したので、
今回はそのことについてつらつら書いていきます。


長くなるので、今回は第一章の前半だけ。


00. 文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ

    let text = "stressed";
    println!("{}",
        text.chars()  // ["s", "t", .. "d"]
        .rev()  // iterableをreverseする
        .collect::<String>()  // iterable to String
    );
    // https://doc.rust-lang.org/std/iter/trait.Iterator.html#examples-17

    // rsplitを使った方法も出来た
    let text_2: Vec<&str> = "stressed".rsplit("").collect();
    println!("{}", text_2.join(""));

ふわっとした感じで言うと、Pythonで言うところの.split("")がchars() にあたるらしいです(細かいことをいうとリスト型ではなく、charのイテレータ)。
まずchars()でiterableにして、rev()でreverse、Stringに変換みたいな感じでやっています。

してるのは、これを入れないとコンパイラが型アノテーションできずにエラー吐くからです。

このあたり最初わからずにだいぶ苦労しました。

ちなみに rsplit() というsplitした上で逆順にリストに格納するメソッドがあったのでそちらでも書いてみました。どっちの書き方が早いのかな...。

01.「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

    let text: Vec<_> =  "パタトクカシーー".split("").collect();
    // .split("")すると0番目と-1番目は""(空文字)
    // mapでもいけそう
    let mut result = "".to_string();
    for (idx, x) in text.iter().enumerate() {
        if idx % 2 == 0 {
            result.push_str(x);
        }
    }
    println!("{}", result);
    // 文字列を先にlistにできたら text.split(|x| x%2 == 0); みたいにできそう
    // https://doc.rust-lang.org/beta/std/vec/struct.Vec.html#examples-50

to_string() で&str->Stringに変換したあと偶数配列だけcharを取得してます。pushは破壊的なのでmutな変数にpush_str()する、という流れです。
そういえば同じようなことが出来るメソッドで to_owned() というのがあるんですが、いまいち違いがわかってません。


02. 「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

    // 01と同じ方法で
    let text_1: Vec<_> = "パトカー".split("").collect();
    let text_2: Vec<_> = "タクシー".split("").collect();

    let mut result = "".to_string();
    // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.zip
    let iter = text_1.iter().zip(text_2.iter());
    for x in iter {
        result.push_str(x.0);
        result.push_str(x.1);
        // println!("{}{}", x.0, x.1);  // tupleのaccessはdot
    }
    println!("{}", result);

01と方法はほぼ同じです。
違う点はzip()を使って2つループさせてるところくらいです。Pythonにも同様のものがあります。一旦変数に入れないと行けないところがPythonと違ってちょっと面倒ですが。
zipで回した中身がtupleになっていて、tupleの要素へのアクセスは x.0, x.1という書き方でやるようです。

03. "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

    let text: Vec<_> = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.".split(" ").collect();
    let mut result = "".to_string();
    for x in text {
        result.push_str(x.len().to_string().as_str());
    }
    println!("{}", result);

01, 02と方法変わってない。
そういえばlistの中身を確認するときってドキュメントだとassert使って証明の流れが多いですね。
log的な感じで標準出力してくれたりしないのかな。

04. 元素記号

use std::collections::HashMap;

     :

    let text: Vec<_> = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.".split(" ").collect();
    let mut result = HashMap::<String, usize>::new();

    // 2文字を結合したものを一時的に入れる
    let mut two_chars = "".to_string();
    for (idx, x) in text.iter().enumerate() {
        let word: Vec<_> = x.split("").collect();
        // matchは使えないので愚直にif
        if idx == 0 || idx == 4 || idx == 5 || idx == 6 || idx == 7 || idx == 8 || idx == 14 || idx == 15 || idx == 18 {
            result.insert(word[1].to_string(), idx);  // idx => usize で usizeをstrにcastする方法を見つけられない
        } else {
            two_chars.push_str(word[1]);
            two_chars.push_str(word[2]);
            //result.insert(tmp.to_string().as_str(), idx.to_string().as_str());  // error: expected a literal
            result.insert(two_chars.to_string(), idx);
        }
        // Char_atはunstable https://doc.rust-lang.org/1.1.0/syntax/str/fn.char_at.html
    }

最初、1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語だけ抜き出すというのをmatchでやったのですが、
matchの結果にinsert()は使えないようで、expected type `std::option::Option<&str>というエラーが出てしまったので愚直にfor-ifで判定してます。PythonのIN演算子を探したのですが参照周りの制約で難しいのでしょうか、見当たりませんでした。

ちなみに match ではこういう風に使いたかった。"|" で区切ると or条件になり、それ以外の場合を"_"に書く、という感じの構文で表現できるようです。

match {
    0 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 18 => result.insert( ... ,
    _  => result.insert(... ,
}

この問題のゴールは、条件に沿ってtextをスライスした結果を連想配列(辞書型もしくはマップ型)にすることなので、Rustにも連想配列がないか調べたところありました
mutな変数にinsert()で要素を追加していくという感じです。

> HashMap::::new();
usizeをstrにどうやってcastするのかイマイチ不明だったので、HashMap::::new()で逃げてます。
まぁ本来は後に文字列出力することも考えて<&str, &str>が良いのでしょうね。

> let mut two_chars = "".to_string();
文字列から先頭2つを抜きだすのは良いんですが、それを結合させる良い方法が思いつかず、一時的にtwo_chars(こういうのはやらないほうが良いんですが...)に突っ込んでます。
"A" + "B" まで簡素に書きたいとは言わないので、今より楽な方法はないかなーと模索してます。



こんな感じで、ドキュメントを読み漁りつつなんとなく動くものができたという感じです。慣れるまでなかなかつらいですね。
Pythonやっぱり楽だなぁというのと、型にこんなに気を使うと型のない言語が不安になりますね。

もっと良い書き方を覚えたら追記していきます。

おわり。