Rust とは?から始まって、基本的な文法をカバーします
自分なりの grep コマンドを実装します
自分のペースで、できるところまでやりましょう
システムプログラミング用の言語としてスタートしました
信頼性が高く、パフォーマンスの出るプログラムを書けるように設計されています
組み込みからWeb まで、さまざまな場面で利用されています
テキストファイルから、パターンに一致する行を抜き出すコマンド
使い方:grep オプション パターン ファイル名
次の例では、Cargo.toml
の中から、2018
という文字列が含まれる行を抜き出しています:
edition = "2018"
FizzBuzz を作ろう
テキストファイルを表示するプログラムを作ろう
2 を改造して、grep コマンドを作ろう
1 から 100 までの数値を出力します
ただし、次の場合は数字の代わりに指定された文字列を出力します
数字が 3 の倍数の場合、Fizz を出力します
数字が 5 の倍数の場合、Buzz を出力します
数字が 3 の倍数で、しかも 5 の倍数でもある場合には、FizzBuzz を出力します
Rust のプロジェクトの操作には、cargo コマンドを使います
cargo new
を実行すると、プロジェクトが作成されます
% cargo new fizzbuzz
Created binary (application) `fizzbuzz` package
fizzbuzz
├── Cargo.toml
└── src
└── main.rs
cargo build
でビルドします
cargo run
で、ビルドしたプログラムを実行します
ビルドされていない場合は、ビルドも行います
標準ではデバッグビルドを実行します
% cd fizzbuzz
% cargo run
Compiling fizzbuzz v0.1.0 (/Users/chikoski/fizzbuzz)
Finished dev [unoptimized + debuginfo] target(s) in 0.77s
Running `target/debug/fizzbuzz`
Hello, world!
main 関数が定義されています
この関数(main)がエントリーポイントです
println! は文字列を出力し、最後に改行するマクロです
fn main ( ) {
println ! ( "Hello, world!" ) ;
}
fn main ( ) {
let mut counter = 0 ;
while counter < 10 {
println ! ( "Hello, world!" ) ;
counter += 1 ;
}
}
0..10
は 0 以上 10 未満の範囲を表すオブジェクト を定義します
このオブジェクトは、イテレーター としての性質を持っています
そのため for 文 で範囲に含まれる整数を列挙できます
下記では、列挙された値を使わないので、_
を利用しています
fn main ( ) {
for _ in 0 ..10 {
println ! ( "Hello, world!" ) ;
}
}
Rust での if は式です。つまり評価値を持ちます
実行したブロックで最後に評価した式の値が、if 式の評価値となります
ブロックの最後の式に ;
がついていないことがポイントです
fn main ( ) {
for n in 0 ..10 {
let output = if n % 15 == 0 {
"FizzBuzz"
} else {
format ! ( "{}" , n)
} ;
println ! ( "{}" , output) ;
}
}
コンパイル時には型チェックが行われます
次の例では、if 節の評価値が str
型なのに対し、else の評価値が String
型であることが原因でコンパイルエラーとなっています
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:6:9
|
3 | let output = if n % 15 == 0{
| ____________________-
4 | | "FizzBuzz"
| | ---------- expected because of this
5 | | }else{
6 | | format!("{}", n)
| | ^^^^^^^^^^^^^^^^ expected `&str`, found struct `String`
7 | | };
| |_______- `if` and `else` have incompatible types
|
String
は文字列を表すオブジェクトです
str
は文字列のスライスを表す値です
to_string
メソッドで、String
オブジェクトへ変換できます
let name = "World" ;
let message = format ! ( "Hello, {}!" , name) ;
println ! ( "{}" , message) ;
let slice_of_message = & message[ 0 ..5 ] ;
println ! ( "{}" , slice_of_message) ;
let another_string = slice_of_message. to_string ( ) ;
println ! ( "{}" , another_string) ;
if 節の評価値の型が String となるように修正します
to_string
メソッドを呼ぶことで、String 型の "FizzBuzz"
を作成できます
fn main ( ) {
for n in 0 ..10 {
let output = if n % 15 == 0 {
"FizzBuzz" . to_string ( )
} else {
format ! ( "{}" , n)
} ;
println ! ( "{}" , output) ;
}
}
rustc に --explain オプションをつけて実行すると、より詳しい解説を読めます
次の例では、E0308 のエラーについて、解説を読みます
同じ解説を Web でも読めます
% rustc --explain E0308
Expected type did not match the received type.
Erroneous code examples:
以下は、手続き的に書いた FizzBuzz の例です
これを少しずつ改変し、Rust の柔軟性をみていきます
fn main ( ) {
for n in 1 ..20 {
let output = if n % 15 == 0 {
"FizzBuzz" . to_string ( )
} else if n % 5 == 0 {
"Buzz" . to_string ( )
} else if n % 3 == 0 {
"Fizz" . to_string ( )
} else {
format ! ( "{}" , n)
} ;
println ! ( "{}" , output) ;
}
}
fn
キーワード で関数を定義できます
()
内に引数のリストを、->
の後に返り値の型を書きます
下記は、FizzBuzz の数値から文字列への変換を、関数に切り出した例です
u32
を String への変換として実装しています
fn fizzbuzz ( n : u32 ) -> String {
if n % 15 == 0 {
"FizzBuzz" . to_string ( )
} else if n % 5 == 0 {
"Buzz" . to_string ( )
} else if n % 3 == 0 {
"Fizz" . to_string ( )
} else {
format ! ( "{}" , n)
}
}
実引き数をを与えて関数を呼びます
n
や output
に対して、型を明記していないのは、コンパイラーが型推論を行うためです
fn main ( ) {
for n in 1 ..20 {
let output = fizzbuzz ( n) ;
println ! ( "{}" , output) ;
}
}
#[test]
とアノテーションされた関数は、テスト用の関数として処理されます
次の例では、fizzbuzz
の振る舞いをテストしています
cargo test
でテストを実行できます
#[ test]
fn test_fizzbuzz_returns_fizzbuzz ( ) {
let expected = "FizzBuzz" . to_string ( ) ;
let actual = fizzbuzz ( 15 ) ;
assert_eq ! ( expected, actual) ;
}
fn fizzbuzz ( n : u32 ) -> String {
if n % 15 == 0 {
"FizzBuzz" . to_string ( )
} else if n % 5 == 0 {
"Buzz" . to_string ( )
} else if n % 3 == 0 {
"Fizz" . to_string ( )
} else {
format ! ( "{}" , n)
}
}
fn main ( ) {
for n in 1 ..20 {
let output = fizzbuzz ( n) ;
println ! ( "{}" , output) ;
}
}
FizzBuzz:関数プログラミング的なアプローチ
FizzBuzz はデータ変換を行う関数として捉えることもできます
例:数値の範囲 -> 文字列の配列
一つ一つの数値を、文字列に変換する関数は fizzbuzz として用意されています
これを上手に使って、関数プログラミング的なアプローチで FizzBuzz を書き直します。
コレクション中の要素一つ一つに関数を適用して、別のコレクションを作る map と呼ばれる操作は、関数プログラミングで良く利用されます
Rust のイテレーターにも map
メソッドは用意されています
このメソッドは、各要素に、引数に与えた関数を適用した結果を持つイテレーターを返します
fn main ( ) {
for output in ( 1 ..20 ) . map ( fizzbuzz) {
println ! ( "{}" , output) ;
}
}
クロージャーは関数の一種で、定義された時にアクセス可能な変数であれば関数本体内で利用できる、という点が特徴です
無名関数やラムダ、といった名前でクロージャーを提供している言語もあります
Rust でもクロージャーは利用できます。下記の例では、 fold
メソッドの第 2 引数でクロージャーを定義しています
仮引数リストは |
と |
の間に記述します
fn main ( ) {
let output = ( 1 ..20 ) . map ( fizzbuzz) . fold ( "" . to_string ( ) , |accum, line|{
format ! ( "{}\n {}" , accum, line)
} ) ;
println ! ( "{}" , output) ;
}
fn fizzbuzz ( n : u32 ) -> String {
if n % 15 == 0 {
"FizzBuzz" . to_string ( )
} else if n % 5 == 0 {
"Buzz" . to_string ( )
} else if n % 3 == 0 {
"Fizz" . to_string ( )
} else {
format ! ( "{}" , n)
}
}
fn main ( ) {
let output = ( 1 ..20 ) . map ( fizzbuzz) . fold ( "" . to_string ( ) , |accum, line|{
format ! ( "{}\n {}" , accum, line)
} ) ;
println ! ( "{}" , output) ;
}
基本的な文法を確認しました
手続き的にも、関数型的にも書けます
コンパイラーを良いレビュワーとして付き合っていけると良いと思います
新しいプロジェクトを作ります
決まったファイルの中身を出力するプログラムを作ります
コマンドライン引数で、読むファイルを指定できるように変更します
mygrep
という名前のプロジェクトを作ります
次の例では、ホームディレクトリにプロジェクトを作成しています
fn main ( ) {
let path = "./src/main.rs" ;
match std:: fs:: read_to_string ( path) {
Ok ( content) => print ! ( "{}" , content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
Rust では、ある操作の成否を Result
を使って表現します
Result
は成功を表す Ok
と エラーを表す Err
から成る enum です
is_ok
メソッドで、Ok
か Err
かを判別できます
fn main ( ) {
let result: Result < u32 , String > = Ok ( 1 ) ;
let message = if result. is_ok ( ) {
"Success"
} else {
"Fail"
} ;
println ! ( "{}" , message) ;
}
Ok
は成果物を値として持てます。* 同様に Err
も、エラーの理由を値として持てます
成果物を表すデータ型や、エラーの理由を表すデータ型は、プログラムによって異なります
そのため Result
を扱う際には、成果物のデータ型とエラーの理由を表すデータ型もあわせて指定します
次の例では、成果物の型に u32
を指定し、エラーの理由は String
で与えられるとしています
let result: Result<u32, String> = Ok(1);
unwrap
メソッドを使って、成果物(もしくはエラーの理由)を取り出せます
次の例では、成功した場合に unwrap
メソッドを使って成果物を取り出しています
fn main ( ) {
let result: Result < u32 , String > = Ok ( 1 ) ;
let message = if result. is_ok ( ) {
format ! ( "Result = {}" , result.unwrap( ) )
} else {
"Fail" . to_string ( )
} ;
println ! ( "{}" , message) ;
}
条件分岐と unwrap
との組み合わせる方法は、パターンマッチを使って簡略化できます
パターンマッチは match 式 として記述できます
パターンの一部分を、変数に束縛することで、Ok
/ Err
から値を取り出せます
fn main ( ) {
let result: Result < u32 , String > = Ok ( 1 ) ;
let message = match result {
Ok ( value) => format ! ( "Result = {}" , value) ,
Err ( _) => "Fail" . to_string ( )
} ;
println ! ( "{}" , message) ;
}
Result のエイリアス:std::io::Result
エラーの理由を表す型が決まっている、といった理由で Result のエイリアスが作られることは良くあります
代表例は、std::io::Result です
次のように、エラーを std::io::Error で表すと定めています
type Result < T > = Result < T , std:: io:: Error > ;
出力するファイルを指定できるようにするための準備として、内容を出力する部分を関数に切り出します
ファイルのパスを String
で受け取り、()
を返す関数として定義しました
これにあわせて path
の型が String
になっている点に注意してください
fn run ( path : String ) {
match std:: fs:: read_to_string ( path) {
Ok ( content) => print ! ( "{}" , content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let path = "./src/main.rs" . to_string ( ) ;
run ( path) ;
}
出力するファイルを、コマンドライン引数として指定することとします
実行のイメージは次のようになります
この例では src/main.rs
を出力します
std::env::args()
は、コマンドライン引数を String として保持するイテレーターを返します
nth
メソッドで、n 番目の要素を取得できます
nth
メソッドは Option
という値を返します
fn main ( ) {
let arguments = std:: env:: args ( ) ;
match arguments. nth ( 1 ) {
Some ( path) => run ( path) ,
None => println ! ( "No path is specified" ) ,
}
}
Option はオプション、つまり存在するかもしれないし、しないかもしれないといった値を表現します
下記の例では、1 番目のコマンドライン引数を取得しています
この値が存在するかどうかは、ユーザーの入力に依存します
fn main ( ) {
let arguments = std:: env:: args ( ) ;
match arguments. nth ( 1 ) {
Some ( path) => run ( path) ,
None => println ! ( "No path is specified" ) ,
}
}
fn main ( ) {
let arguments = std:: env:: args ( ) ;
match arguments. nth ( 1 ) {
Some ( path) => run ( path) ,
None => println ! ( "No path is specified" ) ,
}
}
Some は、実際の値を内部に保持しています
Option は Result と同様に、unwrap
メソッドがあります。これを利用して実際の値を取得できます
また下記のようにパターンマッチを利用しても、保持されている値を取得できます
fn main ( ) {
let arguments = std:: env:: args ( ) ;
match arguments. nth ( 1 ) {
Some ( path) => run ( path) ,
None => println ! ( "No path is specified" ) ,
}
}
fn run ( path : String ) {
match std:: fs:: read_to_string ( path) {
Ok ( content) => print ! ( "{}" , content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let arguments = std:: env:: args ( ) ;
match arguments. nth ( 1 ) {
Some ( path) => run ( path) ,
None => println ! ( "No path is specified" ) ,
}
}
ここまでで、指定したファイルの中身を文字列として出力するプログラムができました。これを拡張して grep コマンドを実装します
grep には 2 つのコマンドライン引数があります。1 つ目がパターン、2 つめがファイルパスです
次のれいでは、version がパターンで、Cargo.toml がファイルパスとなります
% grep version Cargo.toml
version = "0.1.0"
fn grep ( content : String , pattern : String ) {
for line in content. lines ( ) {
if line. contains ( pattern. as_str ( ) ) {
println ! ( "{}" , line) ;
}
}
}
fn run ( path : String , pattern : String ) {
match std:: fs:: read_to_string ( path) {
Ok ( content) => grep ( content, pattern) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
std::env::args
の返り値はイテレーターとしての性質を持っています
nth
メソッドを呼ぶたびに、内部の状態が変わります
状態変化による面倒を避けるため、都度 std::env::args
を呼んでいます
fn main ( ) {
let pattern = std:: env:: args ( ) . nth ( 1 ) ;
let path = std:: env:: args ( ) . nth ( 2 ) ;
if pattern. is_some ( ) && path. is_some ( ) {
run ( path, pattern) ;
}
}
今後の発展のために、path
と pattern
をまとめたデータ構造を作ります
データ構造は struct
キーワード を使って定義できます
struct MyGrep {
path : String ,
pattern : String ,
}
impl
キーワード を使うと、データ構造に振る舞いを定義できます
例えば、データ構造を作成する関数 new は次のように定義できます
struct MyGrep {
path : String ,
pattern : String ,
}
impl MyGrep {
fn new ( path : String , pattern : String ) -> MyGrep {
MyGrep {
path,
pattern,
}
}
}
定義した MyGrep 型を使うようにコードを書き換えます
書き換えるのは、main と run の 2 関数です
変数名とフィールド名を .
でつなぐとフィールドの値を参照できます
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( mygrep. path ) {
Ok ( content) => grep ( content, mygrep. pattern ) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let pattern = std:: env:: args ( ) . nth ( 1 ) ;
let path = std:: env:: args ( ) . nth ( 2 ) ;
if pattern. is_some ( ) && path. is_some ( ) {
run ( MyGrep :: new ( path. unwrap ( ) , pattern. unwrap ( ) ) )
}
}
struct MyGrep {
path : String ,
pattern : String ,
}
impl MyGrep {
fn new ( path : String , pattern : String ) -> MyGrep {
MyGrep {
path,
pattern,
}
}
}
fn grep ( content : String , pattern : String ) {
for line in content. lines ( ) {
if line. contains ( pattern. as_str ( ) ) {
println ! ( "{}" , line) ;
}
}
}
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( mygrep. path ) {
Ok ( content) => grep ( content, mygrep. pattern ) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let pattern = std:: env:: args ( ) . nth ( 1 ) ;
let path = std:: env:: args ( ) . nth ( 2 ) ;
if pattern. is_some ( ) && path. is_some ( ) {
run ( MyGrep :: new ( path. unwrap ( ) , pattern. unwrap ( ) ) )
}
}
grep は -n
オプションをつけると、行番号をつけて結果を出力します。この機能を実装します
オプションの解析には、ライブラリを利用することします
ライブラリのことを、Rust では crate(クレート)と呼びます
今回は structopt という crate を使います
Rust のライブラリは、crates.io というレポジトリにまとめられています
Cargo.toml の dependencies に、使用する crate を追記することで利用できるようになります
[dependencies ]
structopt = " 0.3.21"
structopt を利用したコマンドラインオプションの解析
structopt を使うと、コマンドラインオプションの解析を宣言的に記述できます
定義したデータ構造の各フィールドにアトリビュートを追加することで、コマンドラインオプションとの対応関係を記述します
以下は、MyGrep 型にアトリビュートを追加した例です
use structopt:: StructOpt ;
#[ derive( StructOpt ) ]
#[ structopt( name="mygrep" ) ]
struct MyGrep {
#[ structopt( name = "PATTERN" ) ]
pattern : String ,
#[ structopt( name = "FILE" ) ]
path : String ,
}
structopt を利用したコマンドラインオプションの解析(つづき)
MyGrep::from_args
は structopt によって追加されました
この関数が、コマンドラインオプションの解析と、MyGrep オブジェクトを作成します
fn main ( ) {
let mygrep = MyGrep :: from_args ( ) ;
run ( mygrep) ;
}
[package ]
name = " mygrep"
version = " 0.1.0"
authors = [" 自分のなまえ" ]
edition = " 2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies ]
structopt = " 0.3.21"
MyGrep::new
は使わなくなったので、削除しました
use structopt:: StructOpt ;
#[ derive( StructOpt ) ]
#[ structopt( name="mygrep" ) ]
struct MyGrep {
#[ structopt( name = "PATTERN" ) ]
pattern : String ,
#[ structopt( name = "FILE" ) ]
path : String ,
}
fn grep ( content : String , pattern : String ) {
for line in content. lines ( ) {
if line. contains ( pattern. as_str ( ) ) {
println ! ( "{}" , line) ;
}
}
}
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( mygrep. path ) {
Ok ( content) => grep ( content, mygrep. pattern ) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let mygrep = MyGrep :: from_args ( ) ;
run ( mygrep) ;
}
Derive アトリビュート:コードの自動的な追加
line_number
という bool 型のフィールドを追加します
追加したフィールドにも、structopt アトリビュートを追加します
short
はショートオプション、long
はロングオプションを受け付けるための記述です
struct MyGrep {
#[ structopt( name = "PATTERN" ) ]
pattern : String ,
#[ structopt( name = "FILE" ) ]
path : String ,
#[ structopt( short = "-n" , long) ]
line_number : bool ,
}
次のように --help
オプションをつけて、cargo コマンドを実行し、-n
オプションが存在することを確認します
--help
や --version
は、structopt を Derive した際に追加されています
% cargo run -- --help
(中略)
mygrep 0.1.0
USAGE:
mygrep [FLAGS] <PATTERN> <FILE>
FLAGS:
-h, --help Prints help information
-n, --line-number
-V, --version Prints version information
ARGS:
<PATTERN>
<FILE>
grep の第 1 引数を、MyGrep オブジェクトに変更します
fn grep ( mygrep : MyGrep , content : String ) {
let mut line_number = 1 ;
for line in content. lines ( ) {
if line. contains ( mygrep. pattern . as_str ( ) ) {
if mygrep. line_number {
println ! ( "{}: {}" , line_number, line) ;
} else {
println ! ( "{}" , line) ;
}
}
line_number += 1 ;
}
}
grep のシグネチャ変更にあわせて、run も変更します
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( mygrep. path ) {
Ok ( content) => grep ( mygrep, content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
mygrep.path
の値は read_to_string
の引数に所有権 が移動しています
所有権が移動したフィールドをもつオブジェクトを利用しているので、コンパイルエラーがおきます
所有権は代入や、関数呼び出しによって移動します
error[E0382]: use of partially moved value: `mygrep`
--> src/main.rs:68:25
|
67 | match std::fs::read_to_string(mygrep.path) {
| ----------- value partially moved here
68 | Ok(content) => grep(mygrep, content),
| ^^^^^^ value used here after partial move
|
= note: partial move occurs because `mygrep.path` has type `String`, which does not implement the `Copy` trait
error: aborting due to previous error
For more information about this error
所有権を渡す代わりに、一時的に値を貸し出す、ということもできます
貸し出す場合は、値は参照 という形で渡します
変数名の前に &
をつけることで、値への参照を取得します
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( & mygrep. path ) {
Ok ( content) => grep ( mygrep, content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
use structopt:: StructOpt ;
#[ derive( StructOpt ) ]
#[ structopt( name="mygrep" ) ]
struct MyGrep {
#[ structopt( name = "PATTERN" ) ]
pattern : String ,
#[ structopt( name = "FILE" ) ]
path : String ,
#[ structopt( short = "-n" , long) ]
line_number : bool ,
}
fn grep ( mygrep : MyGrep , content : String ) {
let mut line_number = 1 ;
for line in content. lines ( ) {
if line. contains ( mygrep. pattern . as_str ( ) ) {
if mygrep. line_number {
println ! ( "{}: {}" , line_number, line) ;
} else {
println ! ( "{}" , line) ;
}
}
line_number += 1 ;
}
}
fn run ( mygrep : MyGrep ) {
match std:: fs:: read_to_string ( & mygrep. path ) {
Ok ( content) => grep ( mygrep, content) ,
Err ( reason) => println ! ( "{}" , reason)
}
}
fn main ( ) {
let mygrep = MyGrep :: from_args ( ) ;
run ( mygrep) ;
}
-r
オプションは、指定されたパターンを正規表現として解釈するオプションです
正規表現は regex クレートで実現されています
まず Cargo.toml
を編集して regex クレートを読み込んだ上で、main.rs
に機能を実装します