※Githubリポジトリはこちら

最近Rustで遊んでいます。

C/C++相当でハードウェア操作ができ、高速でありつつ、GC(ガーベジコレクション)を使わずにメモリ管理を行ってくれるという夢のような言語、それがRustです。

ただRust、そういった高い目標を掲げて作られた言語であるためか、学習難易度は高め。

最初はメモリ管理周りが難しく、またオブジェクト指向言語経験者には驚きのクラスが存在しない。

そんな言語なんですが非常に興味がわきまして少しずつ勉強しているところです。

で、言語を覚えようと思ったら使ってみなけりゃわからない。そこでコマンドライン版のオセロを作ってみました。

ただし「オセロ」は商標のため、アプリ中ではリバーシとしています。

※Githubリポジトリはこちら

実現したこと

2人用オセロゲーム(AIは作っていない)

ゲーム状態の保存、読込(セーブ・ロード)

スクリーンショット

作ってみての感想

APIに違和感

私はJavaの経験が最も多いですが、組み込みC言語開発を経験しています。またScalaでの関数型開発について調査した経験があります。
これらの経験を踏まえてRustに取り組んだわけですが、Rustはいろいろな言語の要素が含まれていて、
操作感としては統一感がないような感じがしました。

例えばファイル等から行入力する場合、Java等のオブジェクト指向言語であれば

BufferedReader br = new BufferedReader(…);
String line = br.readLine();

のように読み取った結果が返却されてくるというAPI設計になっていますが、Rustでは

let mut user_input = String::new();
io::stdin().read_line(&mut user_input).unwrap();

というように、バッファを引数として渡すAPIになっており、C言語っぽいなー、と思いました。

パッケージ分割

Javaであれば、フォルダ=パッケージであり、その中にファイル=クラスを作成するため、分類しやすいです。
Rustの場合、フォルダをモジュールの単位にもできますが、ファイルもモジュールの単位になっており、
この辺で当惑しました。

例えばエラーオブジェクトを作る場合、Javaの場合は

のような階層で作れば

error.MyException

のように参照できますよね。
Rustでは

pub struct MyError {
}

とすると、

error::myerror::MyError

のように一階層増えてしまって、冗長に感じました。

もちろんルートレベルに

error.rs
struct MyError {
:
}

とすればネームスペース的には

error::MyError

となりますが、感覚的にフォルダが作りたくなってしまうので、この辺が「ぐぬぬ」ってなってしまいます。

例外処理

Rustではエラーを表現するオブジェクトを返却することで例外処理を行います。
この辺の設計はさすがRustで、例外をスローすることのオーバーヘッドを回避しており素敵!と感じます。
が、実際使うのが難しいです。
特に、独自例外オブジェクトを作ろうと思ったときに、例えばJavaいうところのIOExceptionをキャッチし、
それを内包した独自例外を作るなら、

public class MyException extends Exception { }

として、

try {
  :
} catch(IOException e) {
  throw new MyException("エラーが発生しました", e); // <- 発生元のeを保持してアプリ固有の例外に変換
}

のように書けますが、これをRustでどうするのかわかりませんでした。

例があるにはありますが(ここ)、あらゆるのエラーに対応する方法がよくわからなかったので実装を見送りました。

ローカル関数

Rustにもローカル関数がありました!
Scalaを使ったときにローカル関数便利だなーって思ったので、これがRustにもあってちょっと嬉しい。

処理を分割するのにprivateメソッド作らなくていいのはすっきりします。

またJavaでのtryブロックを作る代わりにローカル関数作って、そこで出たエラーをアプリ固有のエラーに変換できるので、
多少わかりやすくなりました。

loopにラベルがつけられる

break, continueで処理対象のloopをラベルしてできる。わかりやすい!。がジャンプ命令の多様なのでスパゲッティにもなりやすい。

view::game_view::show() にて怒涛の3重ループ! 見やすいコードではないけれど、ラベルのおかげで実装できました。
(本当はもっと関数分割したほうがいいとは思っている…)

moveって識別子使いたくなる

オセロや将棋、チェスなどで一手を表すとき英語では”move”というそうなので、変数名や構造体名にmoveを使っています。
ただし、moveはRustの予約語なので、r#move としてエスケープする必要があります。

Copyトレイト、Cloneトレイトの違い

最初Copy、Cloneの違いがわかりませんでしたが、Copyはmemcpyで値の複製を実現できるもの、
Cloneはmemcpyだけでは複製の実現ができないものという区別だとわかりスッキリ。
たとえばVecのように内部的にメモリ管理を含む複雑なオブジェクトはCloneが必要ってこと。

この辺は組み込み言語らしさが表れていますね。

頻繁に使われるトレイトにはderiveマクロが用意されている

DebugトレイトとかCloneトレイトとか、自分で実装するのは面倒だし、コピペが大量発生しそうだし、
みたいな場面でderiveマクロが用意されていました。

たとえば

#[derive(Debug, Clone)]
struct MyError {
}

とすればDebugとCloneが自動的に実装されます。こりゃ便利。

最後に

Rust、難しいですが、それぞれの文法や設計に意味があることがひしひしと伝わってきました。

メモリ管理周りについてはさすがですね。
c++でのメモリ管理に苦しんだ過去を思い返すと、Rustでアプリを作るのは非常に安心感があります!

それと関数型の機能がいろいろあって、その辺は使いやすいです。ミュータブル、イミュータブルが標準的に押さえられているし、mapなどのリスト処理があったりで、複雑な処理は簡潔に書けるし。

ただし、エラー処理周りはまだ発展途上ですね。
この辺は便利なモジュールが出てきており、thiserrorとか、anyhowだとか出ているので、これらの知見がそのうち標準に取り込まれるのでしょう。

それから全体的にあまり処理を簡潔に表現できない気がしました。またimplとdynのように、概念的に似ているけど異なる状況で使うキーワードがいろいろあったりで、もう少しまとまりがあるとさらに使いやすくなると感じました。

ただ、GCなしでメモリ管理でき、C++なみの性能を持つRust、今後の発展を期待して止みません!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です