Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

26. Remove Duplicates from Sorted Array

ソート済みの、複数数字が重複する配列の中には一体何個の数字が含まれているかを考える問題。制約として、in-place で解く必要があります。たとえば、[1, 1, 2] という入力がされた場合、返り値は2で [1, 2, _] という順序の配列を返す必要があるということです。

leetcode.com

ただ、いまいち問題の意図がつかめず苦戦しました。単純に国語力の試される問題でもあります。実装は単純でした。

方針としては、

  • i と i+1 (i は配列のインデックスとする) 、要するに隣同士の数字を比較し、右の値が左の値より大きければ入れ替えるという作業を繰り返します。
  • 入れ替える作業が発生した場合、それは実質異なる数値を入れ替えることになるので、カウンターを1つ足します。

という作業を繰り返すだけで解くことができます。問題の意図が理解できれば捻りはとくになく素直な問題ではあります。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        k = 0
        for i in range(len(nums)):
            if nums[k] < nums[i]:
                k += 1
                nums[k] = nums[i]
        return k + 1

時間計算量は O(n) でしょうか。与えられる配列の長さが増えると、その分だけループが多く回ることになります。

空間計算量は in-place な操作をしていることもあり O(1) になります。

20. Valid Parentheses

LeetCodeにあった問題です。()のように一組の括弧がすべての組み合わせに対して正しい順序で成立していれば正解。({[]})のような組み合わせはOKですが、({)}のような組み合わせはダメ、というものです。ちなみに初見では解けませんでした。Easyで結構初見で解けない問題があるのですが、本当にEasyなのかとても気になってくる。

leetcode.com

最初の自分の考察

(という文字列を見つけたとき、隣に)が来ていれば正当(valid)なのではと考えていたのですが、そうすると({})のような組み合わせの際に撃沈します。

解き方

この手の問題ではスタックが利用できます。開始の括弧、つまり({[が来た際にスタックに積んでおき、閉じ括弧が来たらスタックからpopして対応する括弧であればvalidとするという解法です。

たとえば、おそらく一番複雑な部類になるであろう ({[]}) について考察してみると、

  1. ( が来る→スタックに積む。スタックの状態: (
  2. {が来る→スタックに積む。スタックの状態: {(
  3. [が来る→スタックに積む。スタックの状態: [{(
  4. ]が来る→スタックからpop。popした値は[][に対応するので、valid
  5. }が来る→スタックからpop。popした値は{}{に対応するので、valid
  6. )が来る→スタックからpop。popした値は()(に対応するので、valid

といった感じで、綺麗に問題を解けます。最初解法を見たときおもしろいなと思いました。

下記に Python による解法を示しておきます。

# (, {, [ のどれかだった場合("開始"とする)、スタックに積んでおく。
# "開始"でなかった場合、スタックに積んだものを順に上からpopしていき、一致していなかったらFalseを返す。

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        
        for char in s:
            if char in ["(", "{", "["]:
                stack.append(char)
            else:
                if not stack:
                    return False
                
                curr = stack.pop()
                
                if curr == "(":
                    if char != ")":
                        return False
                if curr == "{":
                    if char != "}":
                        return False
                if curr == "[":
                    if char != "]":
                        return False
                
        if stack:
            return False
        
        return True

時間計算量は O(n) です。for char in s というループ一つですべてが完結するためです。s の長さだけループが必要な回数が増えるだけ。

空間計算量は O(n) です。これはスタックを実質外部のメモリとして使用するためでしょう。s の長さだけ積まれるスタックの量が増えるので、この計算量になります。

Goでプロジェクトを始めたい際に楽できるツールを作った

数年ぶりに戻ってきたGoですが、環境が大きく様変わりしていて劇的に使いやすくなっていました。

とくにいいなと思ったのが go mod でした。これは Go の 1.11 に実験で入ったあとから利用できる機能のようです。

ところで、go mod init というコマンドがあります。これは Go プロジェクトを新規に始める際に、go.mod という設定ファイルを作成してくれるものです。Go はプロジェクトの内容をここから色々読み取って機能するようになっています。

このコマンドは、一度ディレクトリを作成してからそのディレクトリに入って叩く必要があります。たとえば、github.com/yuk1ty/startgo というパッケージパスでプロジェクトを始めたい際には、一度 startgo というディレクトリを作成し、cd し、その中で go mod init を叩く必要があります。

mkdir startgo
cd startgo
go mod init github.com/yuk1ty/startgo

さらに、git を使えるようにしたい場合、git init も中で打つ必要があります。加えて .gitignore も用意したいことも多いでしょう。Hello, world のために main.go もファイルを作成してエディタで用意して…といった具合にです。

git init
gibo dump go >> .gitignore
touch main.go
vim main.go
# vim で Go を編集する

最初 go mod init を触った際、Rust の cargo new のように一通りプロジェクトに必要な内容物を一気に生成してくれるといろいろ手間が省けて嬉しいなと感じたのを覚えています。git も gitignore も Hello, world 用の簡単なファイルも用意された状態でプロジェクトが始まり、go run main.go するだけで開発をスタートできると嬉しいはずです。

というわけで cargo にインスパイアされてこのニーズを満たす CLI ツールを作ってみました。

github.com

この手のツールはすでに存在していそうですが、とくに調べずにとりあえず好きに作ってみました。知り合いの Gopher に聞いてみたところとくに心当たりがなさそうでしたので[*1、この手のツールは本当に現時点ではないのかもしれません。心当たりのいる Gopher の方がいたら教えていただきたいです。

使い方と仕様

前提

  • v0.2.0 時点では Windows は動作するか保証できていないです。
  • VCS には git を想定しています。

コマンド

readygo コマンドには次のオプションが用意されています。

  • --module-path (-p): go mod init する際に使用するモジュールパス。
  • --dir-name (-n): 作成するディレクトリに使用する名前。省略可能。省略した場合は、--module-path を参考にディレクトリ名は設定される。
  • --layout (-l): Go Standard Layout か、そうではなく空っぽのディレクトリを作成するかを選べる。defaultstandard を設定可能。省略可能。省略した場合の値は default

たとえば、次のように使用することができます。

readygo --module-path github.com/yuk1ty/startgo --dir-name startgo --layout default

この結果生成されるファイル等は次のようになります。

ls -a --tree --level 1 startgo
startgo
├── .git
├── .gitignore
├── go.mod
└── main.go

もちろん短いコマンドも用意されています。

readygo -p github.com/yuk1ty/startgo -n startgo -s default

また、--dir-name オプションは省略可能です。

readygo --module-path github.com/yuk1ty/startgo

この場合、--dir-name には startgo が自動で挿入されます。これは非常にシンプルなロジックで決定されています。具体的には、スラッシュで一度 split した後に、配列の一番うしろを取るという簡単なロジックです。だいたいのケースはこれで対応できるのではないかと思い採用しています。

最後に --layoutstandard を設定すると、いわゆる Standard Go Project Layout のうち、cmd, pkg, internal の3つのディレクトリを一旦生成します。この Standard Go Project Layout はいろいろと議論の余地があるようですが[*2]OSS をいくつか眺めていると意外と利用のユースケースが見受けられたので、最小限用意するようにしています。

readygo --module-path github.com/yuk1ty/startgo --layout standard

この結果生成されるファイル等は次のようになります。

ls -a --tree --level 1 startgo
startgo
├── .git
├── .gitignore
├── cmd
├── go.mod
├── internal
├── main.go
└── pkg

なお、この --layout には所定のフォーマットの YAML ファイルを読み込む機能を導入しようかと考えています。ご自身のよく作られるパッケージの型に合わせてカスタマイズできるとよいのではないかとは考えています。他にも Go のコミュニティでよく利用されるディレクトリレイアウトなどあればご教授ください。一番多いのはフラットディレクトリではないかと思っているので、基本的には default の生成する空のディレクトリで事足りるのではないかとは思っています。

生成されるファイル

readygo コマンドを実行すると、go.mod 以外にもいくつか開発に必要なファイルを用意します。

コマンド実行後用意されるのは、具体的には次のファイルやディレクトリです。パッケージ作成後すぐに git にコミットしたり、あるいは go run して動作確認できることを目指してこのファイルやディレクトリを選んでいます。

  • .git: git init した結果生成されるディレクトリ。
  • .gitignore: Go パッケージで使用できる .gitignore が生成される。
  • main.go: Hello, world できるコードが記述された Go ファイル。

内部実装

内部実装はだいたい300行前後の比較的簡単なロジックになっています。Go で CLI ツールを作ったのは初めてでしたので、知見を少し残しておきたいと思います。

Cobra

Go で CLI ツールを使う際に使えるライブラリのようです。

github.com

とくに cobra-cli が強力で、この CLI ツールにいろいろと指示を出すと雛形を用意してくれます。この上で開発をすれば好みの CLI ツールを作成可能なので、非常に開発しやすく体験がよかったです。

今回作成した CLI ツールもこの Cobra を使い倒しています。コマンド処理の本体実装は root.go に記述されています。このディレクトリのレイアウトなどは cobra-cli の生成するものに従っています。

github.com

git や go コマンド周りの実装

git については最初 git 専用のライブラリがありそうだったので使用しようかと思いましたが、結局普通にシェルを Go から実行することにしました。これが一般的な方法なのかはよくわかりませんが、やることは git init くらいでその結果をアプリケーション側で利用することはなかったため、この形で間に合ったかなと思っています。git コマンドがお使いの環境にない場合はエラーになりそこで処理が終了します。

   git := exec.Command("git", "init")
    err = git.Run()
    if err != nil {
        return err
    }

github.com

ただ、cargo などの実装を見ていると VCS にはさまざまな種類を選択できるようです。一旦自分が使いたいために git での初期化のみに対応しましたが、今後他の VCS 対応も追加していこうかなと思っています。cargo では git 以外の VCS を使用する場合は追加でオプションをつけることで専用の初期化が走るように作られていますが、readygo もこの方式に習って新しいオプションをつけるようにしようかと考えています。

go xxx コマンドを Go から実行する際に特別なパッケージがあるかもよくわからなかったので、やはり同様に go mod init コマンドを Go から直接コマンドを実行しています。同様にとくにコマンドを実行した結果を使用したかったわけではなく、副作用だけ発生させて結果はそのまま破棄で構わなかったので、この形で間に合ったかなと思います。

   cmd := exec.Command("go", "mod", "init", *pkgName)
    err = cmd.Run()
    if err != nil {
        return err
    }

readygo -p [パッケージパス] ではじめられるので、ぜひ試してみてください。

今後のプラン

*1:というか、たった数コマンドなのでそこまで手間じゃないというのはありそうです。

*2:Russ Cox がコメントしている(https://github.com/golang-standards/project-layout/issues/117)。そもそも Go がオフィシャルに推進しているものではないことや、「多くの Go のエコシステムで使われてきた」という言説自体が誤りであること、また pkg ディレクトリがとくに余計な複雑性を持ち込むことになりよくないなどといった話が書かれている。

GitHub Actions 上の cargo install でインストールされるプラグインをキャッシュしつつ使いたい

GitHub Actions 上で cargo install 経由でインストールされるプラグインを使いたいと思いました。しかし実際に使ってみると、そのプラグインのインストールとビルドに5分程度毎回時間を要することがわかりました。これはビルド時間を伸ばすことにつながり開発の生産性を下げるのので、解決策を探しました。

背景

今回解決したい事象の背景としては次のとおりです。

  • GitHub Actions 上で、SQL クエリビルダー sqlx のクライアント sqlx-cli を実行し、ビルド時とテスト時にデータベースにマイグレーションをかけたい。
    • sqlx はコンパイル時にデータベースに接続し、データベース上にテーブルがあるかどうかまでチェックする機能がある。それに必要。
  • ただ、sqlx-cli はインストールとビルドに時間がかかる。手元の GitHub Actions ではだいたい5分くらいを要していたようだった。
  • キャッシュすれば解決するのでキャッシュしたい。

解決策

結果的に便利な action が見つかったので、それを使用することにしました。ただこの action は、基本的には cargo install 時のキャッシュ専用のディレクトリを作ってそこにキャッシュを行うだけという比較的単純なものなので、自分で同様の action を作ってみてもよいかもしれません。

github.com

今回は sqlx に対して使用したいので、使えるように下記のように定義を書き足しました。

# GitHub Actions の定義ファイルの一部
      - uses: baptiste0928/cargo-install@v1
        name: Install sqlx-cli
        with:
          crate: sqlx-cli
# 続く

最初のビルドではフルインストールとビルドが走りますが、2度目以降のビルドではきちんとキャッシュされていることを確認できます。

f:id:yuk1tyd:20220417163419p:plain
キャッシュが効くと、「Restored crate from cache」と出る

「ちょい使い」に便利なIOクレート ezio

Rust を使っているとどうしても思い出しながらでないと書けないものに標準入力、ファイルの読み書きがあります。というのも、Web アプリケーションを作るソフトウェアエンジニア(私)の場合日常業務でそこまで必要になる操作ではなく、ファイルの読み書きは S3 の SDK を叩くことのほうが多いですし、ターミナルからの入力はほぼ使うことはないためです。

しかし、ちょっとした CLI ツールを作るとなると話は変わってきます。これらはもちろんものによりますが、比較的プリミティブな標準入力操作やファイル読み書きの操作を求められるためです。そういった操作の際、Rust は Python ほどは楽に書けないというか、気をつけなければならないポイントがいくつかあり「手軽」とは呼べないことがあります。

こうした「手軽さ」のなさは面倒なポイントのひとつではありましたが、先日 ezio というクレートを作者の方が Twitter で紹介しているのを見て使ってみたところ、これが本当に使いやすくおすすめしたいと思いました。というわけで、ezio を軽く紹介する記事です。

ezio

Easy IO の略でしょうね。easy(/ˈiːzi/) + io→ez + io→ezioでしょうか。

github.com

docs.rs

作者いわく「エラーハンドリングや速度面はこだわっていないので、本番で使うには少し適さないかもしれない」とのことです。詳しくベンチマークを取ったわけではありませんが、もしかすると本番のアプリケーションのユースケースで使用が適さない場合があることに注意が必要かもしれません。しかし、手元で軽く動かす程度の CLI ツールを作る分にはこれで十分です。

特徴的なのは簡単なインタフェースで、プレリュードを一度読み込んでおくだけで、

use ezio::prelude::*;

たとえば標準入力はたったの1行で済みます。

let _ = stdio::read_line();

それでは実際に「Rust の標準ライブラリで書いたコード」と「ezio で書き直したコード」を比較しながら、どう便利になっていくのかを見てみましょう。

英和辞書ツールを作る

今回は先日発売された『手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた』より題材をお借りしております。実際の書籍の内容より少し改良を加えたものを使用しています。

英単語を入力すると日本語での意味を検索できる簡易的なアプリケーションを作ってみましょう。処理としては、

  1. 標準入力操作を待ち、単語をユーザーに入力させる。
  2. 裏で辞書ファイルを読み、メモリに保持させる。
  3. 1行1行一致検索を行う。

です。

Rust の標準ライブラリで

これを Rust の標準ライブラリでやると結構大変です。もちろん本番のアプリケーションではこのくらい注意深く実装する必要はあります。

標準入力は std::io::stdin().read_line() のように書くと行うことができます。普段 Rust の標準入力を使わない私は今回躓いたのですが、read_line すると末尾に改行コードがついてくるようです。そこで、改行コードを削る実装を追加しています。

ファイルの読み込みは、std::fs::Filestd::io::BufReaderstd::io::BufRead の3つの助けが必要になります。ファイルを読み込んで、それを1行1行読み込むという操作を行っています。BufReader に乗せてバッファリングし、その中身を順繰りにイテレートして一致検索を行います。

use std::fs::File;
use std::io::{stdin, BufRead, BufReader};

fn main() {
    // 1. 標準入力操作
    let mut word = String::new();
    stdin().read_line(&mut word).unwrap();
    if !word.is_empty() {
        // 末尾に改行コードがくっついてるので削る
        word.truncate(word.len() - 1);
    }

    // 2. ファイル読み込み
    let file = File::open("ejdict-hand-utf8.txt").unwrap();
    let reader = BufReader::new(file);

    // 3. 一致検索
    for line in reader.lines() {
        let line = line.unwrap();
        if line.find(&word).is_none() {
            continue;
        }
        println!("{}", line);
    }
}

同じディレクトリ内に下記のようにファイルを用意します。ファイル自体はこのサイトからダウンロードして置いています。

❯ ls --tree .
.
├── Cargo.lock
├── Cargo.toml
├── ejdict-hand-utf8.txt
├── src
...

動かすと単語の和訳の検索ができます。

❯ cargo run -q
homebrew
homebrew        自家醸造ビール

これを ezio で書き直すとどうなるでしょうか。見てみましょう。

ezio で

まず、Cargo.toml に下記を追加しましょう。

[dependencies]
ezio = "0.1.2"

ezio で書いたコードは下記のようになります。

use ezio::prelude::*;

fn main() {
    // 1. 標準入力操作
    let word = stdio::read_line();
    // 2. ファイル読み込み & 3. 一致検索
    for line in file::reader("ejdict-hand-utf8.txt") {
        if line.find(&word).is_none() {
            continue;
        }
        println!("{}", line);
    }
}

嘘みたいに短くなったことがわかります。標準入力の read_line 関数は String を返すので、わざわざ先ほどの標準ライブラリの例のように空のミュータブルな String を用意してそれを渡すといった処理は不要になります。

また、File::open とバッファリングを reader という関数で一挙に行っています。実際、reader 関数は下記のような実装になっています。

pub fn reader(path: impl AsRef<Path>) -> Reader {
    Reader(std::io::BufReader::new(
        std::fs::File::open(path).expect("Couldn't open file"),
    ))
}

このクレートは先ほども説明したとおり細かいエラーハンドリング等は行わず、内部で expect を使って Result 型は実質 unwrap されていることが多いので注意が必要ですが、簡易的な CLI ツールを作る分にはむしろこれで十分ではないかと思います。

今回は紹介しませんでしたがクレートの README に書かれているように、ファイルへの書き出しは file::write 等のシンプルな関数で済ませられます。

まとめ

  • ezio を使うと素早く標準入出力を含む操作ができる。

『コンセプトから理解するRust』

『コンセプトから理解するRust』を一足お先に読みました。Rust に関する日本語書籍の発刊が増えてきており、読むより発売するペースが上がっている気がします。私もだんだん精読するより積むほうが増えてきてしまいました。

今回も例のごとく、全体に目を通した上で感想などを書いていきたいと思います。本自体は2/12発売のようです。

本書はアプリケーションを実装しながら Rust を学んでいくというより、Rust に登場する特有の概念を解説しながら Rust を学んでいくというコンセプトになっています。その過程で他のプログラミング言語の実装と比較しながら、他の言語と Rust で違う点を説明したり、あるいは共通しているポイントを見つけたりしながら Rust 特有の概念を理解していきます。特有の概念というのは、代表的なものは本書の表紙でも紹介されている「所有権、型、トレイト」です。たしかにこれらを押さえれば、一旦 Rust は書き始められるかと思います。

『実践Rustプログラミング入門』をはじめとしたアプリケーションを実装しながらプログラミング言語を解説する本ですと、どうしてもサンプルコードを多く掲載する関係で本が分厚くなりがちです。しかし本書は類書と比較してもポイントをしっかり押さえつつもかなり薄めに仕上がっており、「他のプログラミング言語はそれなりに使用してきた*1が、Rust 特有の話だけサクッと学びたい」という方はこの本がよいのかもしれません。

最初にまず変数にまつわる話題を取り扱ったあと、その際登場する所有権について解説されます。その後に、Rust に登場する代表的な型について紹介されます。その中で Option や Result、Box や Rc をはじめとするスマートポインタも解説されます。スマートポインタには概念図が記載されており、理解しやすく書かれていると感じました。そうした話題を一通り押さえたあとに、Rust での抽象化の話題(トレイトやジェネリクス)が扱われます。その他の話題として、ファイルの入出力や関数型プログラミングの側面を利用した Rust プログラミング、並列処理・並行処理、あるいは非同期処理に関する話題、そして C との FFI が扱われます。

本書の目次はこちらのサイトに掲載されていますので、興味がある章があるようでしたら読んでみるとよいかもしれません。

本書の良さ

  • Rust 特有の概念を丁寧に説明している。
  • とくに、メモリ管理にまつわる部分は図でわかりやすく説明している。
  • 他の言語をそれなりにやってきた方は楽しめるかも。

Rust 特有の概念を丁寧に説明している

「所有権」や「ライフタイム」あるいは「トレイト」など、最初他の言語から Rust に入門すると理解に苦労する概念をとくに丁寧に説明しています。あとがきにもありましたが、もともと著者の方もこうした Rust 特有の概念の理解に苦労したうちの一人で、どのように説明すればわかりやすく伝えられるかを考えて本書を書かれたとのことでした。あるいは、変数の代入と束縛については微妙に違いがある話など、他の入門書では見ない話題が度々書かれていました。

トレイトとジェネリクスの説明に1章丸々割いているのは本書の大きな特徴だと思います。Rust ではこの2つの概念を用いて抽象化をゼロコストで行えるという特徴がありますが、これらを懇切丁寧に解説していると思います。dyn Traitimpl Trait の話題、ならびに動的ディスパッチと静的ディスパッチにまつわる話題が解説されます。

とくに、メモリ管理にまつわる部分は図でわかりやすく説明している

Rust 特有の概念として所有権をはじめとするメモリ管理の概念があります。それを理解するのは最初の関門となるわけですが、本書では図を使って直感的に理解できる説明を心がけているようです。要所要所で図によるメモリ管理や処理の遷移がよく解説されており、ひとまず雰囲気を掴むのにはこれで十分だと感じました。

他の言語をそれなりにやってきた方は楽しめるかも

この本では C/C++Python との比較がまま登場するため、他のプログラミング言語をいくつかよく触ったことがあり、プログラミング言語間の思想や目的の違いを理解されている方にはとくに楽しめる内容になっていそうだと思いました。

ただ本書はあくまで Rust への入門書ですので、「所有権のチェックはそもそもコンパイラ内部ではどのように扱われているか」「トレイトと実際の実装の紐付けがコンパイラ内部ではどのように扱われているか」といったような言語処理系が好きな方が興味を持つであろう話題には触れられてはいません。そうした高度な話題をタイトルから期待されている方には少し向かないかもしれません。

Rust 入門時に必要な知識がコンパクトに得られる一冊ではないかと思います。「作りたいものは決まっていて、すばやくキャッチアップしたいが手頃な資料がないだろうか?」と考えていた方にはおすすめできるかもしれません。

*1:それなり、の定義は難しいですが…

『手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた』を読みました

先日発売になった『手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた』を、一通り目を通していました。感想を記しておきます。なお感想は、例のごとく全体に軽く目を通して、いくつかサンプルプログラムを写経してみた程度の上でのものです。あらかじめご了承ください。

Python から Rust に入門するという切り口

最近はRustの本が多く出版され始めており、読むよりも買うほうがだんだん多くなってきてしまっています。とくに単なる言語の入門にとどまらず、さまざまな切り口から解説する本が増え始めているように思います。本書もそのひとつで、Python から Rust に入門しようという非常に特異な切り口の一冊です。

Python から Rust という切り口は、多少なりとも求められている気がしています。SNS を見ていると、データサイエンティストや機械学習関連の研究者の方が Rust を使い始めたよ、という話を見かけるようになりました。私も仕事上そういった方々と関わりがあるのですが、Rust への注目をそれなりに感じます。

何より私が入門したときは、Java からいきなり Rust に入門してしまったので、「Java で書くこれは Rust ではどう書くのだろうか…?」「トレイトって何」「参照の使いどころがわからない」などと日々調べながら苦労して覚えていった記憶があります。他の言語での書き方を Rust に直すとどうなるかという切り口は、そうした数年前の私のようなユーザーに非常にフィットすると思います。この本をきっかけに、他言語から Rust に入門するユーザーも増えていくのではないでしょうか。

Rust は言語仕様がライトではない関係で解説も重厚になりがちではあるのですが、JavaPython 出身者は最初面食らってしまうかもしれません。私も面食らった一人でした。しかし本書は、全体的にライトな解説の仕上がりになっていて、そうしたユーザーであってもつまづきにくく構成されているなと感じました。

説明が明快でわかりやすい

解説は非常にライトでつまづきポイントを抑えておりわかりやすいと思いました。Rust は、PythonJava のようないわゆるレイヤーの高い言語を触っていると登場してこない概念が平気で登場してきます。たとえば参照は、そうした言語のユーザーを最初に悩ませるポイントになると思います。本書では図や解説文でそうした慣れない概念をよく解説しています。

これは少し語弊があるかもしれませんが、『詳解Rustプログラミング』や『プログラミングRust』は、どちらかというとアカデミック寄りの説明方法をしていると思っていました。一方で、本書はそうしたアカデミックなバックグラウンドのない方であってもわかるように、実学的で平易な説明の仕方をしていると感じました。

豊富なサンプルでテンポよく Rust を学べる

本書では非常に豊富なサンプルでテンポよく Rust の文法と使い方を学んでいくことができます。Rust と Python で共通する概念が登場するアプリケーションでは、まず Python で実装し、そのあと Rust で実装し直してみるという流れを取ります。本書の後半に登場する Rust 特有の概念(トレイトやジェネリクスなど、Python にはない概念)については、Rust コードのみでの解説です。まずは Python から Rust への変換で基本的な書き方に慣れ、Rust の応用的な話に踏み込んでいく、といった構成でしょうか。

題材が個人的に好きで、迷路の自動生成、乱数生成器のフルスクラッチや画像加工、音源の制作&wavファイルの生成、チャットサーバーの実装から簡易的なプログラミング言語の実装まで幅広かったです。いくつか実装してみましたが、個人的にも非常に楽しめました。

機能の解説は網羅的

サンプルアプリケーションを実装し、それを通じて言語の機能を解説する形式の本だと、どうしてもサンプルアプリケーションの選定によって紹介する言語機能が網羅的にならないという問題があるかと思います。たとえばスマートポインタの解説は意外とサンプルアプリケーションの規模だと紹介の流れが難しく、苦心するポイントかもしれません。

本書は応用的な話、とくにスマートポインタまで含めて網羅的によく解説されていると思いました。スマートポインタは私も最初よくわからなかったので、解説は必須だと感じていました。肝心のスマートポインタの解説のサンプルアプリケーションは、いわゆる単方向リストの実装が採用されていました。単方向リストでは、参照と参照外しがまずまずハードに登場しますが、順を追って丁寧に解説していてわかりやすかったです。

スマートポインタ以外にも、マクロやC・Pythonとのつなぎ方、WebAssembly にいたるまで結構網羅的です。マクロの説明の仕方は個人的にも苦労するポイントだと感じていたのですが、本書の説明の仕方は真似したいです。

まとめ

サンプルが豊富で日曜工作にうってつけです。今週末の予定が埋まりました。