背景
週末を利活用したく、新しい言語をゼロから学習して即戦力を備えるまでどのぐらいかかるかを実験してみました。
事前状態
- いくつかのプログラミング言語(C++,Python,JavaScript,Java,Go,Bash,etc.)の経験
- 基礎的なウェブアプリ構築(フロントエンド、バックエンド)の経験
- Rustは未経験。事前に知っていたこと
- コンパイル言語
- 実行速度が速い、C/C++からGoの間ぐらい
- WebAssemblyにコンパイルできる、ブラウザ上で動くゲームやシミュレーターなどを作れる
実績
- 開発環境を備える (30 min, 15:30~16:30)
- 基本コンセプトを把握する (45 min, 16:00~16:30,<休憩>,19:30~19:45)
Hello WorldGame 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の学習ステップを教えてくれたのは、
- Understand the Basics
- Setup Rust Development Environment
- Write Your First Rust Program
- Learn Ownership, Borrowing, and Lifetimes
- Explore Rust’s Standard Library
- Understand Error Handling
- Work on Small Projects
- Read Advanced Topics and Best Practices
- Join the Rust Community
- 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 |
main.rsの内容です。
fn main() { |
コンパイルと実行
$ rustc main.rs |
楽勝ですね!
15:38 add extension
普段VisualStudioCodeを愛用しているため、Rust用のExtensionなにかおすすめある? って聞いたら、次の回答でした
- Rust (rls) by rust-lang
- rust-analyzer by matklad
- CodeLLDB by Vadim Chugunov
- Better TOML by bungcip
- Crates by serayuzgur
- Cargo by serayuzgur
…しかし、中に(deprecated)などメンテを続けなくなるものや、機能重複のものもあったため、詳細を確認しつつ、最終的には以下を利用しました。
- Language support: rust-analyzer
- Debugger: CodeLLDB
- TOML support for
Cargo.toml
: Even Better TOML - 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で怒られました。snakeケースに変えてみましょう。
$ cargo new my_rust_app |
フォルダ構成も確認しましょう。
$ cd my_rust_app |
gitや.gitignoreも生成されています。
$ cat .gitignore |
/target
をignoreされているので、コンパイルした中間ファイルやバイナリなどだろうか、一旦無視します。
$ cat Cargo.toml |
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
が自動生成される
{ |
もう一度 Ctrl+F5、コンパイルと実行の一連が発動し、Hello, world!
メッセージがコンソールに現れて実行成功でした。
もちろん、コンソールでcargo run
を実行してもよいです。
ちなみに、cargoでのビルドは以下です。
* Executing task: CodeLLDB: cargo |
続けて、ChatGPT先生から基本知識を勉強します。
15:57~16:07 basic data types, variables, functions, and control flow constructs
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;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)
}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; |
ただ、変数を含めた計算式はダメでした。
let x = 100; |
以下のコンパイラエラーになった
error[E0435]: attempt to use a non-constant value in a constant |
16:12~16:23 Ownership, Borrowing, and Lifetimes
Ownership、Borrowing、そしてLifetimesの概念は、Rustが安全性とパフォーマンスを両立させるための基本的な仕組みとして重要です。
- Ownership(所有権):
- Rustの中心的な概念であり、メモリリソースの管理を行う
- 各値には所有者が存在し、所有者はその値を所有する
- 所有者がスコープを抜けると、その値は解放される(dropされる)
- Borrowing(借用):
- 所有者が値を所有する間、他のコードに値の一時的な参照を許可する
- 借用は不変(
&
)または可変(&mut
)の2つの形態がある - 不変の借用は同時に複数の読み取りアクセスを可能にするが、可変の借用は1つの書き込みアクセスしか許可しない
- Lifetimes(寿命):
- 借用の有効範囲を明示的に示すための仕組み
- 値の参照が有効である期間を示す
- コンパイラは、借用が所有者より長く続かないように監視し、データ競合や無効なメモリアクセスを防ぐ
- 所有権ルール:
- Rustの値は常にただ1つの所有者を持つ
- 所有者がスコープから外れるとき、値は解放される
- 所有者は値を必要とするだけ持つ
- 値を参照する際には、所有権を借用できるが、所有者が存在しなければならない
- メリット:
- 所有権、借用、および寿命のシステムは、ランタイムコストなしでメモリ安全性を保証する
- データ競合やセグメンテーション違反などの一般的なランタイムエラーを排除する
- コンパイラがコードの正当性を静的に確認することで、バグを早期に発見しやすくする
16:23~16:30 Standard Library
std::collections
- ベクタ、ハッシュマップ、セットなど、Rust のコレクション型が含まれている
Vec
、HashMap
、HashSet
、LinkedList
、BinaryHeap
などの型がある
std::io
- 入出力操作を行うためのモジュールです
- ファイル I/O、標準入出力、バッファリング、エラーハンドリングなどをサポートしている
Read
、Write
、BufReader
、BufWriter
などの型や関数がある
std::fs
- ファイルシステム関連の機能が提供されている
- ファイルやディレクトリの操作、メタデータの取得、パスの処理などが可能です
File
、DirEntry
、Metadata
、create_dir
、read_dir
などの型や関数が含まれている
std::thread
- スレッドの作成、スレッド間通信、同期処理などを行うためのモジュールである
thread::spawn
、thread::sleep
、Mutex
、Arc
、JoinHandle
などの型や関数がある
std::sync
- 同期プリミティブが提供されている
- ミューテックス、アトミックなデータ型、チャネルなどが含まれている
Mutex
、RwLock
、AtomicBool
、mpsc::channel
などの型や関数がある
std::net
- ネットワークプログラミング関連の機能が提供されている
- ソケットの作成、TCP/UDP 通信、アドレス解決などが可能
TcpStream
、UdpSocket
、Ipv4Addr
、SocketAddr
などの型や関数がある
std::time
- 時刻や時間に関連する操作が提供されている
- システム時刻の取得、時間の計測、スリープなどが可能
SystemTime
、Duration
、Instant
などの型や関数がある
std::path
- ファイルパスに関連する機能が提供されている
- パスの結合、解析、正規化などが可能
Path
、PathBuf
、join
、canonicalize
などの型や関数がある
std::env
- 実行環境とのやり取りに関連する機能が提供されている
- コマンドライン引数の取得、環境変数の操作、ディレクトリの取得などが可能
args
、var
、current_dir
などの型や関数がある
std::error
- エラー処理に関連する機能が提供されている
- エラー型、エラーハンドリング機構などが含まれている
Error
、Result
、From
、Display
などの型やトレイトがある
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>
.
Result<T, E>:
Result<T, E>
is a type that represents either success with a value of typeT
or failure with an error of typeE
- The
Ok(T)
variant signifies success and contains the value of typeT
, while theErr(E)
variant represents failure and contains the error of typeE
- 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),
}
}Option
: Option<T>
is a type that represents either some value of typeT
or none (null-like value)- The
Some(T)
variant contains the value of typeT
, while theNone
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"),
}
}Error Propagation:
- Rust provides the
?
operator to propagate errors from functions that returnResult
orOption
- When used within a function that returns
Result
orOption
, 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)
}- Rust provides the
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");
}
}- Panics represent unrecoverable errors in Rust and are triggered by calling the
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回答:
- 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
- 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, andcargo publish
for publishing crates to the official package registry, crates.io
- rustup:
rustup
is the Rust toolchain installer and manager. It allows developers to install and manage multiple versions of the Rust toolchain on their systemrustup
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 |
ChatGPTに教えられた骨格を利用します(ここだけコピー&ペースト)
// Your main.rs file |
まずは、main()から改修します。
ちょっとリテラルのパラメータを切り出し、デバグしやすいようにloopを一旦コメントアウトします。
fn main() { |
次に、struct GameOfLife
に状態データを保持する場所だろうかと思い、2d配列をこの前教えられたstd::collectionsのVecを使うぐらいの感覚だが、Vecと2次元のVecの使い方を具体的にまた聞きました。
20:00~20:30 2d Vecを利用し、struct GameOfLife
とそのimplのnew
の実装
struct GameOfLife { |
20:30~20:34 render
の実装
fn render(&self) { |
初期状態の出力ができました!
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . |
20:34~20:37 ランダム値の生成方法を習得
Cargo.tomlのdependenciesにrandというcratesとそのバージョンを記載します。
[dependencies] |
プログラミング先頭にuse文を入れて、ランダム使うところで、random()
をこの前のダミーのfalse
を入れ替える。型は推論してくれるらしいです(ここはbool。i32やf64でも同じ書き方で、自動的に推論できて素晴らしい)。
use rand::prelude::random; |
ランダムの初期状態の出力ができた!
. . * * * * * * . . * . . . . . * * * * * . * . * * . * . . . . * . * . * . . . . . . . * . . * . . |
20:38~22:00 apply the rule
ここからは、だいぶ効率悪くなってきて、苦戦しながらやっていきます。
ライフゲームのルールは以下です。
In Conway's Game of Life, each cell interacts with its eight neighbors. The rules are: |
2次元のVecの初期化や、別fnの切り方、cloneがdeep copyなのか、shallow copyなのか、色々詰まったりしたが、count_neighbors
を切り出して少し後回しして、update
だけは先に完成させました。
&
やmut
はどこにつけるべきかがよく迷ったが、コンパイラが親切に怒られてくれるから、なんとかできました。
impl GameOfLife { |
次に、count_neighbors
も完成した。分岐が多すぎて全然美しくないですが…。
三項演算子cond?a:b
はないらしいが、if cond {a} else {b}
は該当する使い方がグーグルして分かりました。
調べると、ブロックの最後が ;
のない式であれば、戻り値として使用されるからですね!(関数型言語の特徴かな?)
fn count_neighbors(&self, r: usize, c: usize) -> usize { |
22:00 Completed
最終的に次のコードが完成形です。
use rand::prelude::random; |
cargo run
の実行結果です。
Next Step
- 現在はランダムな開始状態でスタートするしか対応していないが、RLEフォーマットなどのパターンファイルを読むようにしたい
- 現在は固定なステージサイズになっているが、動的に拡大するなり、Rustの威力を発揮できる並行計算を試したい
- Rust+WebAssemblyを使ってブラウザ上でUIを作りたい
感想
- Rustが古くはないが、割とC/C++のようにローレベルで、型やメモリの形態などをケアする言語であり、コーディングゼロ経験の人にPythonやJavaScriptほど優しくない
- 型推論が限界までやってくれてすごく優秀で、好きになる理由の1つ
- コンパイラが厳しくて、コーディングのあるべきをちゃんと教えてくれるので、とても勉強になる
他の良い学習リソース
Rust Documentation
https://doc.rust-lang.org/beta/
Official Learn Rust
https://www.rust-lang.org/learn