フューチャー技術ブログ

爆速習得、初心者からRustの即戦力を備えるまで

背景

週末を利活用したく、新しい言語をゼロから学習して即戦力を備えるまでどのぐらいかかるかを実験してみました。

事前状態

  • いくつかのプログラミング言語(C++,Python,JavaScript,Java,Go,Bash,etc.)の経験
  • 基礎的なウェブアプリ構築(フロントエンド、バックエンド)の経験
  • Rustは未経験。事前に知っていたこと
    • コンパイル言語
    • 実行速度が速い、C/C++からGoの間ぐらい
    • WebAssemblyにコンパイルできる、ブラウザ上で動くゲームやシミュレーターなどを作れる

実績

  1. 開発環境を備える (30 min, 15:30~16:30)
  2. 基本コンセプトを把握する (45 min, 16:00~16:30,<休憩>,19:30~19:45)
  3. Hello World Game of Lifeを実装してみる (135 min, 19:45~22:00)
  • スタートから1個目のシンプルなデモプログラミング(Conway’s Game of Lifeのコンソール版)の作成まで、環境整備+概念学習+コーディング+デバグ時間実績は210 minでした。(途中で飲んでいたので後半は効率悪かった)
  • 爆速学習の流れは、ChatGPT先生に聞く+少しグーグル検索(Rustの本を3冊買いましたが、最初は読みませんでした。理由は効率重視のため、アウトプット駆動でやりたいと考えたためです)
  • また、自分の手で作りたいので、基本はウェブからソースをコピーしないことをルールとしました

ChatGPTさんとのやり取りログはこちら
(全部英語でやりました、質問の仕方に興味ある方ご参考を)

学習ログ

15:30 Start

ChatGPT先生にRustの学習ステップを教えてくれたのは、

  1. Understand the Basics
  2. Setup Rust Development Environment
  3. Write Your First Rust Program
  4. Learn Ownership, Borrowing, and Lifetimes
  5. Explore Rust’s Standard Library
  6. Understand Error Handling
  7. Work on Small Projects
  8. Read Advanced Topics and Best Practices
  9. Join the Rust Community
  10. Continuous Learning and Practice

1番目を飛ばし、直接2番の環境構築を行います。

15:30~15:33 Installation

推奨された一番楽の方法でcurl|shのワンライナーでrustupをインストールします。
(rustupってなに?というのは後にします)

https://www.rust-lang.org/tools/install

15:33~15:37 Hello World

インストール成功の証にもなるため、Hello Worldの書き方を聞きました。

$ mkdir my_rust_project
$ cd my_rust_project
$ touch main.rs

main.rsの内容です。

fn main() {
println!("Hello, World!");
}

コンパイルと実行

$ rustc main.rs
$ ./main
Hello, World!

楽勝ですね!

15:38 add extension

普段VisualStudioCodeを愛用しているため、Rust用のExtensionなにかおすすめある?って聞いたら、次の回答でした

  1. Rust (rls) by rust-lang
  2. rust-analyzer by matklad
  3. CodeLLDB by Vadim Chugunov
  4. Better TOML by bungcip
  5. Crates by serayuzgur
  6. Cargo by serayuzgur

…しかし、中に(deprecated)などメンテを続けなくなるものや、機能重複のものもあったため、詳細を確認しつつ、最終的には以下を利用しました。

  1. Language support: rust-analyzer
  2. Debugger: CodeLLDB
  3. TOML support for Cargo.toml: Even Better TOML
  4. Dependency manager: crates

15:44 データタイプ、変数、関数、コントロールフロー(if文、ループ文)などのお勉強

15:47~15:57 VSCodeで動かす

やっぱりChatGPT先生だけに聞くのがなかなか不安であるため、Googleでvscode run for rustを検索しました。

https://code.visualstudio.com/docs/languages/rust

やはり cargo を使うことにしました。

$ cargo new MyRustApp
warning: the name `MyRustApp` is not snake_case or kebab-case which is recommended for package names, consider `myrustapp`
Created binary (application) `MyRustApp` package

命名制約違反ということでwarningで怒られました。snakeケースに変えてみましょう。

$ cargo new my_rust_app
Created binary (application) `my_rust_app` package

フォルダ構成も確認しましょう。

$ cd my_rust_app  
$ tree -a -L 2
.
├── .git
│   ├── FETCH_HEAD
│   ├── HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   ├── info
│   ├── objects
│   └── refs
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
├── .rustc_info.json
├── CACHEDIR.TAG
└── debug

9 directories, 10 files

gitや.gitignoreも生成されています。

$ cat .gitignore
/target

/targetをignoreされているので、コンパイルした中間ファイルやバイナリなどだろうか、一旦無視します。

$ cat Cargo.toml 
[package]
name = "my_rust_app"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

$ cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "my_rust_app"
version = "0.1.0"

Cargo.tomlはmeta情報やライブラリ導入用だろう(dependenciesが追加する箇所みたい)
Cargo.lockは自動生成のようで、編集不可とあります。

VSCodeでmy_rust_appをopenし、main.rsを見てみると、この前のHello Worldそのままでした。

とにかく、Ctrl+F5 (Run Without Debugging)でmain.rsを実行してみます。

OKとYesを押すと.vscode/launch.jsonが自動生成される

{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'my_rust_app'",
"cargo": {
"args": [
"build",
"--bin=my_rust_app",
"--package=my_rust_app"
],
"filter": {
"name": "my_rust_app",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'my_rust_app'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=my_rust_app",
"--package=my_rust_app"
],
"filter": {
"name": "my_rust_app",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

もう一度 Ctrl+F5、コンパイルと実行の一連が発動し、Hello, world!メッセージがコンソールに現れて実行成功でした。
もちろん、コンソールでcargo runを実行してもよいです。

ちなみに、cargoでのビルドは以下です。

 *  Executing task: CodeLLDB: cargo 

Running `cargo build --bin=my_rust_app --package=my_rust_app --message-format=json`...
Compiling my_rust_app v0.1.0 (/Users/wsysuper/my_rust_app)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
* Terminal will be reused by tasks, press any key to close it.

続けて、ChatGPT先生から基本知識を勉強します。

15:57~16:07 basic data types, variables, functions, and control flow constructs

  1. Basic Data Types and Variables:
    // Declare variables with explicit types
    let number: i32 = 42; // Signed 32-bit integer
    let floating_point: f64 = 3.14; // Double-precision floating-point number

    // Rust allows type inference
    let another_number = 123; // Compiler infers the type as i32

    // Mutable variables using 'mut' keyword
    let mut mutable_variable = 10;
    mutable_variable = 20; // Can be changed later

    // Constants using 'const' keyword (must be annotated with a type)
    const PI: f64 = 3.14159;
  2. Functions:
    // Function to add two numbers
    fn add(a: i32, b: i32) -> i32 {
    return a + b; // Explicit return
    }

    // Function without explicit return
    fn multiply(a: i32, b: i32) -> i32 {
    a * b // Implicit return
    }

    // Function with no parameters and no return value
    fn greet() {
    println!("Hello, World!");
    }

    // Function with multiple return values (using tuple)
    fn calculate(a: i32, b: i32) -> (i32, i32) {
    (a + b, a * b)
    }
  3. Control Flow Constructs:
    // If statement
    let number = 42;
    if number > 0 {
    println!("Positive");
    } else if number < 0 {
    println!("Negative");
    } else {
    println!("Zero");
    }

    // While loop
    let mut count = 0;
    while count < 5 {
    println!("Count: {}", count);
    count += 1;
    }

    // For loop (range-based)
    for i in 0..5 {
    println!("Value: {}", i);
    }

    // For loop (iterating over elements)
    let numbers = [1, 2, 3, 4, 5];
    for num in numbers.iter() {
    println!("Value: {}", num);
    }

    // Match expression
    let number = 42;
    match number {
    0 => println!("Zero"),
    1..=100 => println!("Between 1 and 100"),
    _ => println!("Something else"),
    }

16:07~16:12 変数(mutable/immutable)そして定数(const)について教わる

  • let:

    • 不変の変数を宣言するために使用される
    • let a = 1;のように値を割り当てた後は変更できない
    • デフォルトでは不変であり、値の変更を意図していない場合に使用する
  • let mut:

    • 可変の変数を宣言するために使用される
    • let mut a = 1;のように、mut修飾子を付けることで変更可能な変数を宣言できる
  • const:

    • 定数を宣言するために使用される
    • const A: i32 = 1;のように、型を明示的に注釈付けし、値はコンパイル時に既知でなければならない
    • 常に不変であり、プログラムの実行中に変更することはできない

さらに、試験して定数の評価式があってもうまくいきました(コンパイラが計算の結果を定数化してくれるね)。

const A: i32 = 1 + 1;
const B: i32 = A + 1;

ただ、変数を含めた計算式はダメでした。

let x = 100;
const C: i32 = x + 1;

以下のコンパイラエラーになった

error[E0435]: attempt to use a non-constant value in a constant
--> src/main.rs:6:20
|
6 | const C: i32 = x + 1;
| ------- ^ non-constant value
| |
| help: consider using `let` instead of `const`: `let C`

16:12~16:23 Ownership, Borrowing, and Lifetimes

Ownership、Borrowing、そしてLifetimesの概念は、Rustが安全性とパフォーマンスを両立させるための基本的な仕組みとして重要です。

  • Ownership(所有権):
    • Rustの中心的な概念であり、メモリリソースの管理を行う
    • 各値には所有者が存在し、所有者はその値を所有する
    • 所有者がスコープを抜けると、その値は解放される(dropされる)
  • Borrowing(借用):
    • 所有者が値を所有する間、他のコードに値の一時的な参照を許可する
    • 借用は不変(&)または可変(&mut)の2つの形態がある
    • 不変の借用は同時に複数の読み取りアクセスを可能にするが、可変の借用は一つの書き込みアクセスしか許可しない
  • Lifetimes(寿命):
    • 借用の有効範囲を明示的に示すための仕組み
    • 値の参照が有効である期間を示す
    • コンパイラは、借用が所有者より長く続かないように監視し、データ競合や無効なメモリアクセスを防ぐ
  • 所有権ルール:
    • Rustの値は常にただ1つの所有者を持つ
    • 所有者がスコープから外れるとき、値は解放される
    • 所有者は値を必要とするだけ持つ
    • 値を参照する際には、所有権を借用することができるが、所有者が存在しなければならない
  • メリット:
    • 所有権、借用、および寿命のシステムは、ランタイムコストなしでメモリ安全性を保証する
    • データ競合やセグメンテーション違反などの一般的なランタイムエラーを排除する
    • コンパイラがコードの正当性を静的に確認することで、バグを早期に発見しやすくする

16:23~16:30 Standard Library

  1. std::collections
    • ベクタ、ハッシュマップ、セットなど、Rust のコレクション型が含まれている
    • VecHashMapHashSetLinkedListBinaryHeapなどの型がある
  2. std::io
    • 入出力操作を行うためのモジュールです
    • ファイル I/O、標準入出力、バッファリング、エラーハンドリングなどをサポートしている
    • ReadWriteBufReaderBufWriterなどの型や関数がある
  3. std::fs
    • ファイルシステム関連の機能が提供されている
    • ファイルやディレクトリの操作、メタデータの取得、パスの処理などが可能です
    • FileDirEntryMetadatacreate_dirread_dirなどの型や関数が含まれている
  4. std::thread
    • スレッドの作成、スレッド間通信、同期処理などを行うためのモジュールである
    • thread::spawnthread::sleepMutexArcJoinHandleなどの型や関数がある
  5. std::sync
    • 同期プリミティブが提供されている
    • ミューテックス、アトミックなデータ型、チャネルなどが含まれている
    • MutexRwLockAtomicBoolmpsc::channelなどの型や関数がある
  6. std::net
    • ネットワークプログラミング関連の機能が提供されている
    • ソケットの作成、TCP/UDP 通信、アドレス解決などが可能
    • TcpStreamUdpSocketIpv4AddrSocketAddrなどの型や関数がある
  7. std::time
    • 時刻や時間に関連する操作が提供されている
    • システム時刻の取得、時間の計測、スリープなどが可能
    • SystemTimeDurationInstantなどの型や関数がある
  8. std::path
    • ファイルパスに関連する機能が提供されている
    • パスの結合、解析、正規化などが可能
    • PathPathBufjoincanonicalizeなどの型や関数がある
  9. std::env
    • 実行環境とのやり取りに関連する機能が提供されている
    • コマンドライン引数の取得、環境変数の操作、ディレクトリの取得などが可能
    • argsvarcurrent_dirなどの型や関数がある
  10. std::error
    • エラー処理に関連する機能が提供されている
    • エラー型、エラーハンドリング機構などが含まれている
    • ErrorResultFromDisplayなどの型やトレイトがある

16:30 休憩に入る

<3時間後再開>

19:30~19:40 Understand Error Handling

Error handling in Rust is a fundamental aspect of writing robust and reliable code. Rust provides mechanisms to handle errors in a way that ensures safety and encourages explicit handling of error conditions. The two main types used for error handling in Rust are Result<T, E> and Option<T>.

  1. Result<T, E>:
    • Result<T, E> is a type that represents either success with a value of type T or failure with an error of type E
    • The Ok(T) variant signifies success and contains the value of type T, while the Err(E) variant represents failure and contains the error of type E
    • Result types are commonly used for functions that may fail or encounter errors during execution
      use std::fs::File;
      use std::io::Read;

      fn read_file_contents(file_path: &str) -> Result<String, std::io::Error> {
      let mut file = File::open(file_path)?;
      let mut contents = String::new();
      file.read_to_string(&mut contents)?;
      Ok(contents)
      }

      fn main() {
      match read_file_contents("example.txt") {
      Ok(contents) => println!("File contents: {}", contents),
      Err(err) => eprintln!("Error reading file: {}", err),
      }
      }
  2. Option:
    • Option<T> is a type that represents either some value of type T or none (null-like value)
    • The Some(T) variant contains the value of type T, while the None variant represents the absence of a value
    • Option types are commonly used for functions that may or may not return a value
      fn divide(a: f64, b: f64) -> Option<f64> {
      if b != 0.0 {
      Some(a / b)
      } else {
      None
      }
      }

      fn main() {
      let result = divide(10.0, 2.0);
      match result {
      Some(value) => println!("Result: {}", value),
      None => println!("Cannot divide by zero"),
      }
      }
  3. Error Propagation:
    • Rust provides the ? operator to propagate errors from functions that return Result or Option
    • When used within a function that returns Result or Option, the ? operator will return early with the error if an error occurs, simplifying error handling code
      fn read_file_contents(file_path: &str) -> Result<String, std::io::Error> {
      let mut file = File::open(file_path)?;
      let mut contents = String::new();
      file.read_to_string(&mut contents)?;
      Ok(contents)
      }
  4. Panics:
    • Panics represent unrecoverable errors in Rust and are triggered by calling the panic! macro
    • Panics unwind the stack and terminate the program, printing an error message if provided
    • Panics are typically used for programming errors or unexpected conditions that cannot be handled gracefully
      fn divide(a: f64, b: f64) -> f64 {
      if b != 0.0 {
      a / b
      } else {
      panic!("Division by zero");
      }
      }

Error handling in Rust encourages developers to handle errors explicitly, promoting code safety and reliability. By using Result<T, E> and Option<T> types effectively, Rust ensures that error conditions are appropriately managed, improving code readability and maintainability.

19:40~19:45 他の不明点について質問

私: What are rustc, cargo, and rustup used for?

ChatGPT回答:

  1. rustc:
    • rustc is the Rust compiler, responsible for compiling Rust source code into executable programs or libraries
    • It takes Rust source files (.rs) as input and generates machine code for the target platform
    • Developers can invoke rustc directly from the command line to compile Rust programs without using any build system
  2. cargo:
    • cargo is Rust’s build system and package manager. It automates the process of compiling Rust code, managing dependencies, and building projects
    • With cargo, developers can create, build, test, and publish Rust packages effortlessly
    • It provides commands like cargo new for creating new projects, cargo build for compiling code, cargo run for executing programs, cargo test for running tests, and cargo publish for publishing crates to the official package registry, crates.io
  3. rustup:
    • rustup is the Rust toolchain installer and manager. It allows developers to install and manage multiple versions of the Rust toolchain on their system
    • rustup makes it easy to switch between different versions of the Rust compiler, including stable, beta, and nightly releases
    • It also manages associated tools like cargo, rustc, and documentation
    • rustup simplifies the process of setting up a Rust development environment and ensures compatibility with Rust projects across different environments

In summary, rustc is the Rust compiler used for compiling Rust code, cargo is the build system and package manager for Rust projects, and rustup is the toolchain installer and manager for managing Rust installations and associated tools. Together, these tools form the foundation of the Rust development ecosystem, enabling efficient and reliable development of Rust applications.

19:45 Starting with a classic project like Conway’s Game of Life

いよいよ本気に初めてのプロジェクトを開始します。

cargo new game_of_life
code game_of_life

ChatGPTに教えられた骨格を利用します。(ここだけコピペ)

// Your main.rs file

struct GameOfLife {
// Define your game board here
// ...
}

impl GameOfLife {
fn new(width: usize, height: usize) -> Self {
// Initialize the game board
// ...
GameOfLife {
// Initialize your fields here
// ...
}
}

fn update(&mut self) {
// Update the game board based on the rules of the game
// ...
}

fn render(&self) {
// Render the game board to the console
// ...
}
}

fn main() {
let mut game = GameOfLife::new(50, 30); // Example size for the board

loop {
game.update();
game.render();
std::thread::sleep(std::time::Duration::from_millis(100)); // Adjust the speed of the simulation
}
}

まずは、main()から改修します。
ちょっとリテラルのパラメータを切り出し、デバグしやすいようにloopを一旦コメントアウトします。

fn main() {
let width = 50;
let height = 30;
let sleep_time = 100;

let mut game = GameOfLife::new(width, height);
// loop {
game.update();
game.render();
std::thread::sleep(std::time::Duration::from_millis(sleep_time));
// }
}

次に、struct GameOfLifeに状態データを保持する場所だろうかと思い、2d配列をこの前教えられたstd::collectionsのVecを使うぐらいの感覚だが、Vecと2次元のVecの使い方を具体的にまた聞きました。

20:00~20:30 2d Vecを利用し、struct GameOfLifeとそのimplのnewの実装

struct GameOfLife {
width: usize,
height: usize,
cells: Vec<Vec<bool>>,
}

impl GameOfLife {
fn new(width: usize, height: usize) -> Self {
let mut gol = GameOfLife {
width,
height,
cells: Vec::with_capacity(height),
};
for _ in 0..height {
let mut row = Vec::with_capacity(width);
for _ in 0..width {
row.push(false); // ここはrandomのtrue/falseする方法が分からない、一旦全部falseに
}
gol.cells.push(row);
}
return gol;
}
// ...
}

20:30~20:34 renderの実装

fn render(&self) {
for row in &self.cells {
for cell in row {
if *cell {
print!("* ");
} else {
print!(". ");
}
}
print!("\n");
}
print!("\n");
}

初期状態の出力ができました!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

20:34~20:37 ランダム値の生成方法を習得

Cargo.tomlのdependenciesにrandというcratesとそのバージョンを記載します。

[dependencies]
rand = "0.8.5"

プログラミング先頭にuse文を入れて、ランダム使うところで、random()をこの前のダミーのfalseを入れ替える。型は推論してくれるらしいです(ここはbool。i32やf64でも同じ書き方で、自動的に推論できて素晴らしい)。

use rand::prelude::random;
// ...
// row.push(false);
row.push(random());

ランダムの初期状態の出力ができた!

. . * * * * * * . . * . . . . . * * * * * . * . * * . * . . . . * . * . * . . . . . . . * . . * . . 
* . * * * * * . . . . . * * . * . . * . * . * . . . . . . * * . . . . * * * . . . * * . . * . . . *
* . . . . . . . * . * * . * . * * * . * * . * . * . . * . * . . . * . * . * * * * . * * . . * . * *
* . . * * * . * . . . * * . * . * * . * . . * * * * . . * . * * . * * * . * . * . . * . . * . . * .
. . . . * * * * . . . * . * . * * . * * * * . . . . . * . * . * * * * . * * . * . * * * . * * . * .
* * . * * . * . * . . * . * * . * * . * . * . . . . * . * . * * . * . * . . . . * . . * * . . * * *
* . . * * . * * * * * * * * . * . . . . . . * * . * * . * * * . * . . * . * * * . * . . . . * * * *
. . * . * * . * * . * . . * . . * . . * * . * . . . . * * * * * . * * . . . * . * * * . * . . . * .
. . * * * . . . . * . . * . . . . . * . . * . * * . * . . * * . . . . . * . . * . * * . * . * * * .
* . . * * . * . . . . . * * * * . . . . * * * . * * * . * . . . * * . * . . * * . * . . * * . * . *
. * . * . . . . * . . . * . . * * . . . . . . . * . * * * * . * . . . . . * . * * * * . . . . . * *
* * * . . * * . * * * * * * * . * . * * . . . * . . . * * . * * . . * . * . * . * . * * . . * . . *
. * * . * * . * . . . . * * . * . . * . . . * . * . * * . * * . * * . . * * . . . . . . * . . * * .
* . . . . . . . . . . . . * * * . . * * * * . . * . . . . * * * * . * * * * * * * * . . * * . . * .
* * . * . * . . * * * * . . . . . . . . * * . . . * * . . . . . . . * * . . . . . * . * . * . * . *
. . * . * . . * * . * . . * . * . * . . . . * . . * * * . . * . * * * . . . . . * . * * . . * . * .
* * . . . . * . . * . * * . * . * . . * . * . * . * * * . * * * . . . * * . . * . . * . * * . . . *
. * . . . . * * * * * * * * * . . * . . . * . . . . . * * * . . * * . . * * * . . * * * . * . . * .
* . . * . . * * * * * . . . . . * . * . . . . * * * . * * * * * . * . . * . . * . . . . * . . * * .
* . * . * * . * . * . . . * * . . . . * . . . . * * * * * . . * . . * * * * . * * * . * * . * * . .
. . * * * . * . * . * * * . * * . * * . . . * * * * . . * * . . * . . . . * . * . * . * * * * * * .
* . . . * . * * . * * * . * * * . . * * . . . . * * * . . * . * * * . * * . * . * . * . * * * * * .
* * . . . * . * * * . * . . * * . . * . . . * . . . * * . . . . . . * . * * * . . . . * . . . * * .
* . . * . . * . . * . * * * . . . . . . * . * * * . . * . * . * * . * * . . * . * * * * * * * . . .
. . * * * * . * * * * . * * * * * * * . * . . . * . . * * * * . . . . * . . * * . * . * . . . * * *
* . . * * * . . . . * * * * . * . * . . * * * . * . * . * . * * . . . . . . . . . . . . * * * * * *
* . . * * . . . . * * * * * . * . . * . * . . * . . . * * . * . * * . * * . . * . * . . * * . . * .
. * . * . * * . * * . * * . * . * * . . . . . . * . . * * . * . * * * . * * . * . * * * . * . . . *
. . . . . . . . * * * * . . . * . * . * . * . . * . . * * * * * * . . * * * * . * . * . * . . * . *
* . * . * . * . * . . * . . * . . . . * . * . . . * . . * . * . * * . . . * . . . * * * * . . . * .

20:38~22:00 apply the rule

ここからは、だいぶ効率悪くなってきて、苦戦しながらやっていきます。

ライフゲームのルールは以下です。

In Conway's Game of Life, each cell interacts with its eight neighbors. The rules are:
Any live cell with fewer than two live neighbors dies (underpopulation).
Any live cell with two or three live neighbors survives to the next generation.
Any live cell with more than three live neighbors dies (overpopulation).
Any dead cell with exactly three live neighbors becomes alive (reproduction).

2次元のVecの初期化や、別fnの切り方、cloneがdeep copyなのか、shallow copyなのか、色々詰まったりしたが、count_neighborsを切り出して少し後回しして、updateだけは先に完成させました。

&mutはどこにつけるべきかがよく迷ったが、コンパイラが親切に怒られてくれるから、なんとかできました。

impl GameOfLife {
// ...
fn count_neighbors(&self, r: usize, c: usize) -> usize {
// TODO
return 2;
}

fn update(&mut self) {
let mut new_cells = Vec::with_capacity(self.height);

for _ in 0..self.height {
new_cells.push(vec![false; self.width]);
}

for r in 0..self.height {
for c in 0..self.width {
let neighbors = self.count_neighbors(r, c);
match neighbors {
0..=1 => new_cells[r][c] = false,
2 => new_cells[r][c] = self.cells[r][c],
3 => new_cells[r][c] = true,
_ => new_cells[r][c] = false,
}
}
}
self.cells = new_cells;
}
// ...
}

次に、count_neighborsも完成した。分岐が多すぎて全然美しくないですが…。

三項演算子cond?a:bはないらしいが、if cond {a} else {b}は該当する使い方がグーグルして分かりました。
調べると、ブロックの最後が ; のない式であれば、戻り値として使用されるからですね!(関数型言語の特徴かな?)

fn count_neighbors(&self, r: usize, c: usize) -> usize {
let mut cnt = 0;
if r > 0 {
if c > 0 {
cnt += if self.cells[r - 1][c - 1] { 1 } else { 0 };
}
cnt += if self.cells[r - 1][c] { 1 } else { 0 };
if c < self.width - 1 {
cnt += if self.cells[r - 1][c + 1] { 1 } else { 0 };
}
}
if c > 0 {
cnt += if self.cells[r][c - 1] { 1 } else { 0 };
}
if c < self.width - 1 {
cnt += if self.cells[r][c + 1] { 1 } else { 0 };
}
if r < self.height - 1 {
if c > 0 {
cnt += if self.cells[r + 1][c - 1] { 1 } else { 0 };
}
cnt += if self.cells[r + 1][c] { 1 } else { 0 };
if c < self.width - 1 {
cnt += if self.cells[r + 1][c + 1] { 1 } else { 0 };
}
}
return cnt;
}

22:00 Completed

最終的に次のコードが完成形です。

use rand::prelude::random;

struct GameOfLife {
width: usize,
height: usize,
cells: Vec<Vec<bool>>,
}

impl GameOfLife {
fn new(width: usize, height: usize) -> Self {
let mut gol = GameOfLife {
width,
height,
cells: Vec::with_capacity(height),
};
for _ in 0..height {
let mut row = Vec::with_capacity(width);
for _ in 0..width {
row.push(random());
}
gol.cells.push(row);
}
return gol;
}

fn count_neighbors(&self, r: usize, c: usize) -> usize {
let mut cnt = 0;
if r > 0 {
if c > 0 {
cnt += if self.cells[r - 1][c - 1] { 1 } else { 0 };
}
cnt += if self.cells[r - 1][c] { 1 } else { 0 };
if c < self.width - 1 {
cnt += if self.cells[r - 1][c + 1] { 1 } else { 0 };
}
}
if c > 0 {
cnt += if self.cells[r][c - 1] { 1 } else { 0 };
}
if c < self.width - 1 {
cnt += if self.cells[r][c + 1] { 1 } else { 0 };
}
if r < self.height - 1 {
if c > 0 {
cnt += if self.cells[r + 1][c - 1] { 1 } else { 0 };
}
cnt += if self.cells[r + 1][c] { 1 } else { 0 };
if c < self.width - 1 {
cnt += if self.cells[r + 1][c + 1] { 1 } else { 0 };
}
}
return cnt;
}

fn update(&mut self) {
let mut new_cells = Vec::with_capacity(self.height);

for _ in 0..self.height {
new_cells.push(vec![false; self.width]);
}

for r in 0..self.height {
for c in 0..self.width {
let neighbors = self.count_neighbors(r, c);
match neighbors {
0..=1 => new_cells[r][c] = false,
2 => new_cells[r][c] = self.cells[r][c],
3 => new_cells[r][c] = true,
_ => new_cells[r][c] = false,
}
}
}
self.cells = new_cells;
}

fn render(&self) {
for row in &self.cells {
for cell in row {
if *cell {
print!("* ");
} else {
print!(". ");
}
}
print!("\n");
}
print!("\n");
}
}

fn main() {
let width = 50;
let height = 30;
let sleep_time = 100;

let mut game = GameOfLife::new(width, height);
loop {
game.update();
game.render();
std::thread::sleep(std::time::Duration::from_millis(sleep_time));
}
}

cargo run の実行結果です。

Screen_Recording_2024-03-18_at_0.28.59.gif

Next Step

  • 現在はランダムな開始状態でスタートするしか対応していないが、RLEフォーマットなどのパターンファイルを読むようにしたい
  • 現在は固定なステージサイズになっているが、動的に拡大するなり、Rustの威力を発揮できる並行計算を試したい
  • Rust+WebAssemblyを使ってブラウザ上でUIを作りたい

感想

  • Rustが古くはないが、割とC/C++のようにローレベルで、型やメモリの形態などをケアする言語であり、コーディングゼロ経験の人にPythonやJavaScriptほど優しくない
  • 型推論が限界までやってくれてすごく優秀で、好きになる理由の一つ
  • コンパイラが厳しくて、コーディングのあるべきをちゃんと教えてくれるので、とても勉強になる

他の良い学習リソース

Rust Documentation
https://doc.rust-lang.org/beta/

Official Learn Rust
https://www.rust-lang.org/learn