この記事は Wano Group Advent Calendar 2023 の4日目の記事となります。
Wano グループ TuneCore Japan で Engineering Manager / Frontend テックリードをしている 吉田 翔吾郎(@shogoroy)です。
昨年のWano グループ アドベントカレンダーでは TuneCore JapanでのPJ管理・タスク管理に関する記事(10周年の音楽プロダクトで、PJ管理/タスク管理をJIRAからNotionへ完全移行した話)を書きましたが、今回はWanoグループとして取り組んでいる勉強会の取り組みとそこで学んだRustについて書いていきます。
前置き
この記事で書くこと
Rustってこういう部分が面白い!勉強になる!良いな!をTypeScriptと比較しながら部分的に知れる
この記事で書かないこと
- CodeCrafters の詳細(および対象講座であるRedisの内容)
- TypeScript の基礎・応用
- Rustの基礎文法、etc.
CodeCrafters とは
CodeCraftersは、Redis・Git・Dockerなど開発に欠かせないツールやインフラがどの様に構築されているかを実践的に&ステップバイステップで学べるサービスになっています。
Wanoではこういった学習ツール・勉強会・Conference参加へ補助が出るというエンジニア向け福利厚生があり、今年は有志でCodeCraftersを通じた個人学習と勉強会を定期的に行ってきました。
この記事ではCodeCraftersの初歩的な講座であるRedisライクなサービス構築を行うにあたってRustについて学んだことをTypeScriptユーザー目線で数点紹介します。
■immutable と 破壊的関数 に対するアプローチ
immutable過激派にとって嬉しい言語 Rust
近年のTypeScript(JavaScript)のコードを書く場合、イミュータブルなコードを書く設計が多いかと思います。
普段TypeScript・Reactを使っていて気にする点として、イミュータブルなコードになっているか≒再レンダリング対象になっているかという点があるかと思います。(破壊的な関数の危険性やReactのmemo化についての説明は割愛)
config や lint で防げることもありますが、設定がゆるい場合はTypeScriptは動的型付けなJavaScriptライクに書けてしまいますし、配列の特定要素だけを直接代入してしまったりOpjectのPropertyのみ直接代入してしまったり雑な実装ではついついミュータブルなコードになってバグの原因になりがちです。
// NG:配列の特定要素更新
handleListUpdate = () => {
setList(prev => {
// prev is ["id-1", "id-2"]
prev[0] = "id-a"
return prev; // これじゃ再レンダリングされない
})
}
// NG:Objectの特定要素更新
handleObjUpdate = () => {
setObj(prev => {
// prev is { id: 123, name: "nanashi"}
prev.name = "gombei"
return prev; // これじゃ再レンダリングされない
})
}
上記の例だけであればシンプルなのですが、これにもう一つ関数処理が加わり、もしそれが破壊的関数だった場合にすぐにバグになってしまいます。
// 破壊的関数
const getFilteredObj = (obj: Array<string>, key: string) => {
// return list.filter(item => item.xxx) // これなら問題ない
for (const key of Object.keys(obj)) { // 関数型言語的じゃないのもNGだが、あくまでも例
delete obj[key]
}
return obj
}
handleObjUpdate = () => {
setObj(prev => {
return getFilteredObj(obj, key)
})
}
上記のように関数内での破壊的変更に弱いことでバグが起きやすいのですが、Rustでは関数呼び出しが起きた時点で変数に対する所有権の移動が起き、そのまま変数への破壊的変更を加えられないよう制御が入るため非常に安全なコードになります。
▼Rust公式ドキュメントより
Rustの所有権(Ownership)の概念と詳細についての説明は割愛します。
公式ドキュメントを御覧ください。
https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
■モジュール管理
Rustのモジュール管理はGoっぽさもありつつ、少し独特な印象。Perlとかにも似てるかも?
Rustでのモジュール管理は凄く独特だなと感じました。
TypeScriptでは単にファイルへの相対パス(またはalias を用いた絶対パス)で自前定義のモジュール(ファイル)から関数やinterface など何でも import 可能ですが、
Rustではpackage名の概念とファイルパスの概念を組み合わせたモジュール管理となっています。
//
// TypeScript
//
// src/artst/getter.ts
export const getArtistName = (artist: Artist) => { return artist.name }
// src/index.ts
import { getArtistName } from "./artist/getter";
const fn = () => {
// const artist = fetchArtist(id);
return getArtistName(artist);
}
Rustの場合、以下のようにディレクトリ名と同じ名称のファイル名でmoduleの再export のような処理が必要となります。
一見冗長に見えますが、ファイル単位をモジュール単位として扱いやすくした上で、Rustではファイルレベルとモジュールとを明確に分けられるメリットがあると理解しています。
TypeScriptでは「ファイルの内か外か」レベルでのexport になってしまうため、「可読性のためファイル分割はしたいがこのモジュール(ディレクトリ)より外には出したくない」という制限ができない問題が解決されているように感じます。
//
// Rust
//
// src/artist/getter.rs
pub fn get_artist_name(artist: &Artist) -> String {
return artist.name;
}
// src/artist.rs
pub mod artist;
// src/main.rs
use artist;
■struct と impl
📝 めっちゃGoっぽい!
Frontend・TypeScript(React)では(少なくともTuneCore JapanのFEでは)イミュータブルなコード実現するために、オブジェクト指向との相性が悪く非推奨としてされています。
Rustでのオブジェクト指向プログラミングではstructとimplを用いて実装され、Golangに近い実装だと感じました。
(TypeScriptとの比較は詳細は割愛します)
■ エラー管理 "Result" と Nullable管理の"Option"型
📝 独特だが、めっちゃクセになる書き心地!
個人的に、Rustを書いてみて最も学びになったと感じたのがこのResult型とOption型です。
これらがどんなものかというと、
- Result: 結果として成功か失敗かに分かれる処理。TypeScriptにおけるPromiseに近い。
- Option: 値が入っていないかもしれない値。TypeScriptにおけるObjectのoptinal propertyに近い。あるいは
string | undefined
のようなundefinedが入りうる型に近い。
というふうに私は解釈しています。
さらに、これをRustの match
句で処理することで非常に簡潔に処理を条件分岐させることができます。
例えば、関数の処理として「正常に処理できたらValue型が返るが、値を返せないかもしれない」という処理(Nullableチェック / Option型の処理)の場合以下のようになります。
//
// TypeScript
//
const getArtistFirstReleaseType = (artist: Artist): RelaseType | null => {
if(validateArtistHasNoRelease(artist)) {
return null;
}
return artist.firstRelase.type; // "single" | "album" | "ringtone"
}
const firstSongType = getArtistFirstSongType(artist);
let xxx; // 値更新が必要な場合は switch case だと mutable にせざるをえない、三項演算子の羅列にすると長ったらしくて可読性が低い
switch (firstSongtype) {
case: "album":
// 処理
xxx = aaa;
return;
case: "single":
// 処理
xxx = sss;
return;
case: "ringtone"
// 処理
xxx = rrr;
return
default:
// 値がないときの処理
}
//
// Rust
//
fn get_first_release_type(artist: Artist) -> Option<Song>{
if validate_artist_has_no_release(artist) {
return None;
};
Some(artist.first_release.type)
}
let price = match get_artist_first_song(artist) { // めちゃシンプルに書ける!!
"album" => aaa,
"sigle" => sss,
"ringtone" => rrr,
_ => // 値がないときの処理
};
match 句は三項演算子が多項な場合の処理(switch-caseのような処理)としても使え、非常に汎用性・出現率が高い処理となっている印象です。
個人的にはRust の match句での処理の簡潔さと書き心地が非常にクセになるなと感じました。
▼Rust公式ドキュメントより
あとがき
近年のフロントエンド技術では、ブラウザ標準のJavaScriptの派生言語であるTypeScriptが主流となっていますが、そのビルドツールなどはメモリ効率・実行速度などの理由からRustへの移行が非常に進んでいます。
フロントエンド技術者にとってTypeScriptの習熟が必須となってきたように、近い将来Rustがフロントエンドエンジニアにとっての必須言語(あるいは高レベルなエンジニアになるために必須な言語)となると私は想定しています。
プロダクトレベルでの採用はまだ少ないかと思いますが、Rustの動向は引き続き伺っていきたいと思います。
現在、Wanoグループ / TuneCore Japan では人材募集をしています。興味のある方は下記を参照してください。
Wano | Wano Group JOBS
TuneCore Japan | TuneCore Japan JOBS