Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rust で関数型ドメインモデリング

Rust で関数型ドメインモデリング

Rust、何もわからない… #15 での発表資料です

Avatar for ONDA, Takashi

ONDA, Takashi

October 14, 2026

More Decks by ONDA, Takashi

Other Decks in Technology

Transcript

  1. 一休レストランと Rust 5 億 約 1/10 一休全体の 1 日のリクエスト 1

    を Python から Rust に移したあとの リソース API 代目 Classic ASP、2 代目 Python、3 代目 Rust の 3 世代が並んで動いている 3
  2. 今日の話 第 1 部 Functional Domain Modeling 何がプログラムを複雑にするか 2. 3

    つの問題への対処 1. 第 2 部 Rust での実践 一休レストランでの実践 4. Rust での落としどころ 3. 4
  3. mutability: 書き換わる前提の値 行 入力をそのまま入れる ' 237 : tmp_strPointType = InVal

    行 条件によって、途中で書き換える ' 1192 : IF NOT tmpBlnPointRewardEnabled THEN tmp_strPointType = Null 行 書き換え済みのつもりで読む ' 1260 : IF ... OR tmp_strPointType = GIVE_POINTTYPE_PAYPAY THEN 読む側は、途中で正しい値に書き換わっている前提で読む ところが経路によっては、書き換わらないまま読んでいた 6 1. 何がプログラムを複雑にするか
  4. 条件分岐: 入れ子のフラグ 1. 何がプログラムを複雑にするか IF tmpBlnPointRewardEnabled THEN IF tmp_instantUsePointFlag THEN

    tmpPaymentAmount = cmnInitNum(tmp_intTPrice) _ - cmnInitNum(tmp_objPointReward.TotalInstantUsePoints) ELSE tmpPaymentAmount = cmnInitNum(tmp_intTPrice) END IF ELSE tmpPaymentAmount = cmnInitNum(tmp_intTPrice) END IF 7
  5. 使う場面で毎回判定する 判定 ポイントを即時利用するか PayPay グルメ経由か 会員か 1. 何がプログラムを複雑にするか ファイル(3,358 行)での回数

    10 回 10 回 32 回 1 一度判定した結果が値に残らないので、使う場面で毎回判定し直す 判定を忘れると、そのままミスになる 8
  6. 判断と I/O の混在 1. IF tmp_strPointType <> GIVE_POINTTYPE_PAYPAY THEN tmpStrErr

    = Replace(E_RS_C30002, ...) END IF ' 判定 IF isErr(tmpStrErr) = False AND isCardPayment() THEN ' tmpStrErr = .pre_reservation_proc(InObjConn) ' DB END IF IF isErr(tmpStrErr) = False THEN IF tmp_objRps.cancelGuard THEN ' 何がプログラムを複雑にするか 判定 に書く また判定 接続 InObjConn が 1 ファイルに 100 回。判定と DB への書き込みが交互に並ぶ 判定だけを確かめたくても DB が要るので、テストがとても大変 DB 9
  7. 今日の話 第 1 部 Functional Domain Modeling 何がプログラムを複雑にするか 2. 3

    つの問題への対処 1. 第 2 部 Rust での実践 一休レストランでの実践 4. Rust での落としどころ 3. 10
  8. 3 つの問題と、その対処 偶有的な複雑さの源 mutability 条件分岐 判断と I/O の混在 2. 3

    対処 言語に守ってもらう(不変がデフォルト) 型でモデリングする pure function に寄せる 11 つの問題への対処
  9. 2. 3 つの問題への対処 Make Illegal States Unrepresentable before after struct

    RewardUsage { use_now: bool, used_points: Option<u32>, saved_points: Option<u32>, } // use_now: true // saved_points: Some(..) なのに enum RewardUsage { UseNow { points: u32 }, // Save { points: u32 }, // } // // ありえない組み合わせは 書けない も書ける 13 使う 貯める
  10. 2. 3 Parse, Don’t Validate 判定しても は のまま。 型を見ても、検証済みかどうか分からない //

    u32 u32 // fn check_party_size(n: u32) -> bool 判定結果を型で表す // fn parse_party_size(n: u32) -> Result<PartySize, InvalidPartySize> 14 つの問題への対処
  11. 2. 3 Modeling Workflows as Pipelines 各ステップの結果の型が、次のステップの入力になる UnvalidatedOrder 検証 →

    ValidatedOrder 価格付け → let priced_order = unvalidated_order .validate(&context)? // -> ValidatedOrder .price(&catalog)?; // -> PricedOrder 15 PricedOrder つの問題への対処
  12. 2. 3 つの問題への対処 Functional Core, Imperative Shell core は値を返すだけ。shell は計算済みの値を書き込むだけで、判断しない

    (I/O) 入力を受け取る shell 入力値 → (pure) 判断と計算 core 16 書き込み命令(値) → (I/O) 書き込むだけ shell
  13. 今日の話 第 1 部 Functional Domain Modeling 何がプログラムを複雑にするか 2. 3

    つの問題への対処 1. 第 2 部 Rust での実践 一休レストランでの実践 4. Rust での落としどころ 3. 17
  14. Rust: 速さと表現力の両立 3. 一休レストランでの実践 関数型ドメインモデリングに必要な道具を、ひととおり持っている 直和型(enum とパターンマッチ) newtype trait 所有権(move

    した値はもう使えない) それでいて、効率はシステムプログラミングができる水準のまま。型を増やしても、 実行時のコストは気にしなくてよい(ゼロコスト抽象化) 18
  15. newtype でポイントの種別を区別する 定義 3. 一休レストランでの実践 受け取る側 enum PointReward { Ikyu(IkyuPointReward),

    Paypay(PaypayPointReward), } trait PaypayApi { async fn grant( &self, reward: &PaypayPointReward, ) -> Result<(), PaypayError>; } struct IkyuPointReward(Amount); struct PaypayPointReward(Amount); api.grant(&ikyu_reward).await // error[E0308]: mismatched types 19
  16. newtype で「検証済み」の段階を表す 3. 検証を通らない金額は、次の計算に渡せない pub struct ValidatedTotalPrice(TotalPrice); // 外からは作れない pub

    fn validate_total_price( total_price: TotalPrice, requested_amount: u32, ) -> Result<ValidatedTotalPrice, PriceChanged> { if total_price.total() != requested_amount { return Err(PriceChanged); } Ok(ValidatedTotalPrice(total_price)) } 20 一休レストランでの実践
  17. 計算のステップを型で表す 3. → TotalPrice apply_coupon → PricingWithCoupon with_points_usage → PricingWithPointsUsage

    with_point_reward Payment Modeling Workflows as Pipelines の実践 22 一休レストランでの実践
  18. 各段階のシグネチャ 3. 一休レストランでの実践 参照でも書けるが、あえて self を消費して「次の段階の計算」として表す impl TotalPrice { fn

    apply_coupon(self, coupon: Option<Coupon>) -> PricingWithCoupon } impl PricingWithCoupon { fn with_points_usage(self, usage: PointsUsage) -> PricingWithPointsUsage } impl PricingWithPointsUsage { fn with_point_reward(self, reward: Option<PointReward>) -> Payment } 23
  19. 呼び出し側 3. let payment = total_price .apply_coupon(coupon) .with_points_usage(points) .with_point_reward(reward); 順序を入れ替えると

    // total_price.with_points_usage(points); // error[E0599]: no method named `with_points_usage` // found for struct `TotalPrice` 24 一休レストランでの実践
  20. core: 書き込み命令を作る 3. pub async fn validate_reserve_input( input: ReserveInput, context:

    &impl ReserveContext, ) -> Result<ReserveCommand, ReserveError> pub struct ReserveCommand { pub commitment: ReservationCommitment, pub inventory_allocation: Option<InventoryAllocation>, pub coupon_usage: Option<CouponUsage>, pub ikyu_point_commitment: Option<IkyuPointCommitment>, pub paypay_commitment: Option<PaypayCommitment>, // ... } 25 一休レストランでの実践
  21. shell: 書き込むだけ 3. 一休レストランでの実践 async fn reserve(&self, command: ReserveCommand) ->

    Result<ReserveResult, ReserveError> { self.inventory.store(&command.inventory_allocation).await?; self.coupon.store(&command.coupon_usage).await?; } if let Some(points) = &command.ikyu_point_commitment { self.ikyu_point.register(points).await?; } if let Some(paypay) = &command.paypay_commitment { self.paypay.commit(paypay).await?; } // ... 26
  22. 今日の話 第 1 部 Functional Domain Modeling 何がプログラムを複雑にするか 2. 3

    つの問題への対処 1. 第 2 部 Rust での実践 一休レストランでの実践 4. Rust での落としどころ 3. 27
  23. での落としどころ 関数型のワークフロー: 依存を部分適用で渡して関数をつなぐ 4. Rust let validateOrder checkProductExists checkAddressExists unvalidatedOrder

    = ... 部分適用で依存を注入し、入力だけを受け取る関数にする // let validateOrder' = validateOrder checkProductExists checkAddressExists let placeOrder = validateOrder' >> priceOrder >> acknowledgeOrder 28
  24. ナイーブに Rust で実装すると 4. Rust での落としどころ fn validate_order<P, PF, A,

    AF>( check_product_exists: P, check_address_exists: A, ) -> impl Fn(UnvalidatedOrder) -> Pin<Box< dyn Future<Output = Result<ValidatedOrder, ValidationError>> + Send>> where P: Fn(ProductCode) -> PF + Clone + Send + Sync + 'static, PF: Future<Output = bool> + Send, A: Fn(UnvalidatedAddress) -> AF + Clone + Send + Sync + 'static, AF: Future<Output = Result<CheckedAddress, AddressError>> + Send, 29
  25. 依存を trait で渡す OO 4. Rust の依存性逆転の原則(SOLID の DIP)と同じ形 pub

    async fn validate_and_calculate_payment<C>( input: CalculatePaymentInput, context: &C, ) -> Result<CalculatePaymentResult, MultiCalculatePaymentError> where C: VisitingPlanExists + DateInventoryExists + OptionMenus + ChildMenus + Coupons + ReservationHistory + PlanExists + PointRewardSettingsExist + MemberPoint + PaypayBalanceExists + RestaurantExists + Sync, 30 での落としどころ
  26. まとめ Rust の表現力で、偶有的な複雑さを最小にする 偶有的な複雑さの源 mutability 条件分岐 判断と I/O の混在 対処

    言語に守ってもらう(不変がデフォルト) 型でモデリングする pure function に寄せる 得られたもの 型が AI に対するガードレールになる コードを見れば、何をしているかすぐ分かる 並行稼働(Parallel Run)で、旧実装と突き合わせられた 31