Save 37% off PRO during our Black Friday Sale! »

PHPでthrowしない例外ハンドリング

444706893bf3e53d6d96b4509d2d05d5?s=47 tanden
May 29, 2021

 PHPでthrowしない例外ハンドリング

PHPカンファレンス沖縄2021で発表した資料です。

サマリー
PHPでは、例外をthrowとtry-catch-finallyを使って処理する実装をすることが多いと思います。
対して、GoやScala、Rustなどthrow -> try-catch-finallyでの例外ハンドリングを実装せず、多値返却やEither、Resultなど結果とエラーを表すデータ型を使って例外処理を行う言語も存在します。
この資料では、PHPでGoやScala、Rustのようにthrowしない例外処理をどう実装していくのかと、実際に実装した結果どのようなメリット/デメリットが得られたのかを説明しています。

444706893bf3e53d6d96b4509d2d05d5?s=128

tanden

May 29, 2021
Tweet

Transcript

  1. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 2021/05/29 PHP

    カンファレンス沖縄 2021 炭田高輝(@tac_tanden) PHP で throw しない 例外ハンドリング
  2. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 自己紹介 BASE株式会社

    Owners Experience Backend Group Engineering Manager 2016.09 - 新卒でWebゲーム開発 2020.01 - BASE株式会社で『BASE』の開発 カンファレンスの登壇は初めてです よろしくお願いします!     炭田高輝(tanden) Back-End Web Developer 2
  3. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 本日のアジェンダ 1

    2 3 PHPでの例外処理 例外を使わない言語たち PHPでthrowしない例外処理 3
  4. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか?

  5. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか プログラミングにおける

    例外とは何か? 5
  6. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか 考えれば考えるほど難しかった

    6
  7. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか 『「例外」は、エラーや例外イベントを呼び出し

    元のコードに渡すことができる特別な手段であ る。<中略> エラー状況に対処できないコードはエラーを解釈 してそれをうまく処理する機能を持っていると期 待して、システムの他の部分に制御を渡すことが できる。』(p.242) メソッド内で処理できない(しない)状態に なったことを外部に伝える手段のこと コードコンプリート第2版 第8章 防御的プログラミング 7
  8. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか 『「例外とは予期せぬ事態に備えるためのもので

    あり、プログラムの通常の流れの一部には組み込 むべきでない」』(p.129) 『ヒント34:例外は例外的な問題のみに使用す ること』(p.130) 成功でも失敗でもない、ルーチン(メソッド) が処理することを想定していない状態になった ときにのみ、例外は使われるべき 達人プログラマー 24 いつ例外を使用するのか 8
  9. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか 契約による設計における成功と失敗

    『ルーチンが契約を満たす状態で実行を終えた場 合、そのルーチンコールは成功である。成功しなけ れば失敗である。』(p.528) 『例外とはルーチンコールの失敗を引き起こす可能 性のある実行時イベントである。』(p.528) 契約による設計にはこの場では深入りしません。 しかし、契約による設計を導入することで例外を 明確に定義できるようになります。 オブジェクト指向入門 第2版 原則・コンセプト 9
  10. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 「例外」とはなにか やはり難しい

    1
  11. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外とはなにか 概念としての例外

    - どのように処理すべきか織り込まれていないイベントのこと - 事前条件違反 - 事後条件違反 - その他の処理を失敗させるイベント プログラミング言語の機能としての例外 - どのように処理すべきか織り込まれていない状態になったことを外部に伝える手段 11
  12. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外ハンドリングとは?

  13. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外ハンドリング 『ルーチンの実行中に起こった例外を処理する

    には、正しいやり方が2つある。 R1 リトライ:例外となる状態を変更、ルーチン を最初から実行し直そうとする。 R2 失敗(組織的パニック):環境をきれいに し、実行を終了して呼び出し側に失敗を報告す る。』(p.534) 例外を捕捉して、プログラム上でリトライもし くは失敗として扱うこと オブジェクト指向入門 第2版 原則・コンセプト 13
  14. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外ハンドリングとはなにか 概念としての例外ハンドリング

    - 捕捉した例外をもとに、プログラムの目的のために何らかの処理を実行すること - リトライ - ユーザへ例外が発生したことをフィードバックする - ログに吐いて処理を終了する プログラミング言語の機能としての例外ハンドリング - プログラミング言語に備わっている仕組みを使って、プログラムの目的を達成するために 例外に対して何らかの処理を実装すること 14
  15. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 「例外」と例外 以降のページでは概念としての例外を例外と書き

    プログラミング言語の機能としての例外を「例外」と書きます 15
  16. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 本日のアジェンダ 1

    2 3 PHPでの「例外」処理 「例外」を使わない言語たち PHPでthrowしない「例外」処理 16
  17. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. PHPの「例外」ハンドリング -

    呼び出されるメソッド側で例外クラスを throwする - 呼び出し側で例外クラスを捕捉する - 捕捉するために呼び出すメソッドを try-catchで囲む - try-catchブロックの後で「常に実行したい 処理」を行いたい場合 finallyブロックを追 加 - finally内で実装された処理は例外が throw されなくても実行される PHPの例外処理 17 function division(float $x, float $y) float { if ($y === 0) { throw new Exception('0 division'); } return $x/y; } try { echo division(0); } catch (Exception $e) { // 例外を捕捉して何らかの処理を行う } finally { // 必ず実行したい処理を行う }
  18. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. throw と

    try-catch-finallyの難しさ 仮説 throw / try-catch-finallyは 開発者にとって扱うのが難しいのではないか 1
  19. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. throw と

    try-catch-finallyの難しさ - returnではない何か...? - throwというものがあるらしい - throwすると処理はどうなるの...? - どうやら処理は中断している様子 - throwしたら次はどこに処理が移るの? - ステップ実行するしかないか... 新卒でまだエンジニアになりたての頃の遠い記憶 19
  20. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. throw と

    try-catch-finallyの難しさ - throwしてcatchする箇所まで処理がジャンプする - goto文のような使い方ができてしまう - 呼び出し元のメソッドは、呼び出し先のメソッドのどこでどんな例外がthrowされるのか 知っておく必要がある - 依存関係が生まれる - throw / try-catch-finallyの複雑さに対して、アプリケーションにとって例外ハンドリン グはどこまで必要なのかのトレードオフを設計する必要がある - トレードオフの設計は難しい 例外をthrow / try-catch-finallyで「適切に」処理するのはかなり難しいのではないか 20
  21. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 本日のアジェンダ 1

    2 3 PHPでの「例外」処理 「例外」を使わない言語たち PHPでthrowしない「例外」処理 21
  22. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 「例外」を使わない言語たち 「例外」を使わずに

    例外を処理する 言語がある 2
  23. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 「例外」を使わない言語たち 今回取り上げる3つの言語

    Rust - すべて自学自習による知識で、残念ながら有識者のレビューを受けたものではありません - 間違っていたらフィードバックいただけると嬉しいです Scala Go 2
  24. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Scalaの例外処理 -

    Scalaではtry-catch-finally節も用意されている - Javaのコードを取り込むことも可能なため - しかしScalaでは、関数型言語の性質上、副作用をさけるために例外は好まれない 「例外は副作用」とは - プログラミング上の副作用 - 関数が値を受け取り値を返すことを主作用とすると、それ以外のプログラムの状態を変化させる作 用を副作用 - 「例外」は主作用以外でプログラムの状態を変更してしまうことから副作用に分類できる 24
  25. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Scalaの例外処理 Either

    either = (2者のうち)どちらか一方 2 エラー処理の文脈で 成功 or 失敗のどちらかを表すことに適した 抽象クラス
  26. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Scalaの例外処理 Either

    Eitherを継承したLeft, Rightを利用する 2 Left: 失敗時の値 Right: 成功時の値 を入れることが多い 英語の”right”(正しい)にかけているらしい
  27. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Scalaの例外処理 Eitherを返す関数

    27 def division(x: Float, y: Float): Either[String, Float] = if(y == 0.0) Left("0 division") else Right(x/y) - 上記のような関数からのEither型の返り値に対して、match式やmap、flatMapによって 関数を適用してRightやLeftの値に対して処理を行う
  28. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Rustの例外処理 -

    『Rustには例外が存在しません』(Rust公式ガイド p.107) - 代わりに2種類のエラーを使う - 回復不能なエラー - 回復可能なエラー 28
  29. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Rustの例外処理 -

    回復不能なエラー - 処理の実行を中止すべきエラー - panic!を呼び出すことで意図的にクラッシュさせる仕組みがある - 回復可能なエラー - ログを残したり、ユーザにフィードバックしてリトライなど - クラッシュさせるようなエラーは少ないので、エラー処理のメインはこちら - Result<T,E>を使う 29
  30. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 例外ハンドリング(再掲) 『ルーチンの実行中に起こった例外を処理する

    には、正しいやり方が2つある。 R1 リトライ:例外となる状態を変更、ルーチン を最初から実行し直そうとする。 R2 失敗(組織的パニック):環境をきれいに し、実行を終了して呼び出し側に失敗を報告す る。』(p.534) 例外を捕捉して、プログラム上でリトライもし くは失敗として扱うこと オブジェクト指向入門 第2版 原則・コンセプト 30
  31. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Rustの例外処理(再掲) -

    回復不能なエラー - 処理の実行を中止すべきエラー - panic!を呼び出すことで意図的にクラッシュさせる仕組みがある - 回復可能なエラー - ログを残したり、ユーザにフィードバックしてリトライなど - クラッシュさせるようなエラーは少ないので、エラー処理のメインはこちら - Result<T,E>を使う バートランド・メイヤーの例外ハンドリングの定義そのもの しかし、言語機能としての「例外」はない 31
  32. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Rustの例外処理 Result

    enum Result<T, E> { Ok(T), Err(E), } 3 列挙子にOkとErrをもったenum型のクラス
  33. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Rustの例外処理 Result

    fn division(x: f64, y: f64) -> Result(f64, ZeroDivisionError) { if y == 0.0 { Err(ZeroDivisionError) } else { Ok(x/y) } } 3 match式やResult型に用意されているメソッドを使って 成功時の値、エラーの値の処理を行う
  34. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Goの例外処理 -

    Goにも「例外」が存在しません - “We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. “ (Go document Frequently Asked Questions (FAQ)) - convoluted = 入り組んだ - 例外処理をどうしているのか - 多値返却 34
  35. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Goの例外処理 多値返却

    func Division(x float64, y float64) (float64, error) { if y == 0.0 { return nil, err } else { return x/y, nil } } 3 呼び出しもとで、返却されたerrorをチェックしnilでなければ失敗、nilであれば成功 呼び出し元が必ず例外処理を行うようにする
  36. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 本日のアジェンダ 1

    2 3 PHPでの「例外」処理 「例外」を使わない言語たち PHPでthrowしない「例外」処理 36
  37. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. PHPでthrowしない例外ハンドリング -

    アイディアとしては2つ - Either, Resultのようなクラスを作り、メソッドの返り値として使う - 多値返却 37
  38. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. Either, Resultのようなクラスを作れるか

    - php-fp/php-fp-eitherというライブラリがある - PHPでEitherのようなオブジェクトにmapで関数を当てられるようにしたライブラリ - 4年前で更新が止まっている - 型付けができない - PHPにはジェネリクスがない - Left / Right もしくは Ok, Errの中身の値の型を柔軟に変更できない - そもそもenum型もない なのでこちらの線で進めるのは難しそう 38
  39. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 多値返却 -

    PHP7: タプルで返却 - 成功:[$result, null] - 失敗:[null, new RuntimeException()] - 上記のような形で返してあげることは可能 - しかし、メソッドの返り値が array にしかできない - PHP8: Union型が使えそう 多値返却の線はありかもしれない 39
  40. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 実際に行ったPHP7での多値返却 -

    タプルでの多値返却 - メリット - throwとtry-catchを書かずにすべて return で実装することができた - コードがシンプルになったのと、ユニットテストが書きやすかった - デメリット - メソッドの返り値が array にしかできない - PHPStanでの静的解析はarrayの中身までは見ることができなさそう?(未検証) 40
  41. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 実際に行ったPHP7での多値返却 -

    成功/失敗の値をもったDTO - タプルでは返り値の型定義をarrayにしかできないことを解決すべく、DTOに成功/失敗の 値を持たせることを試みた - 成功と失敗の値はコンストラクタから渡してインスタンス化する - 成功:(null, $result) - 失敗:(new RuntimeException(), null) - $dto->getResult(); - $dto->getError(); 41
  42. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 実際に行ったPHP7での多値返却 -

    成功/失敗の値をもったDTO - タプルでは返り値の型定義をarrayにしかできないことを解決すべく、DTOに成功/失敗の 値を持たせることを試みた - メリット - 返り値の型付けができた - デメリット - 実装が面倒で複雑になった(ジェネリクスがないためいちいちresultの型に合わせてク ラスを作る必要がある) - PHPStanで静的解析ができなさそう(要検証) 42
  43. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. PHP7での多値返却 -

    やるならタプルでの多値返却 - 返り値はarrayのみしか型付けできないデメリット - 追記:PHPdocにarray shapes記法で組み込みではないが型付けは可能 - 実装自体はすごくシンプルになる部分のトレードオフ - 型を付けようとDTOのような、結果とエラーの値をもったオブジェクトを使うのは失敗 43
  44. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. PHP8での多値返却 -

    Union型が使えるのではないか - float | ZeroDivisionException、など - Union型であれば、PHPStanでの静的解析も適用できるのではないか - 本当は使い勝手の検証も含めて、実際に手元で何か開発しようと思っていたのですが時間 がなくできませんでした - 今後の検証課題です 44
  45. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. PHPでのthrowしない例外ハンドリング -

    PHP7:タプルによる多値返却 - PHP8:Union型による多値返却(厳密には多値返却ではない。使い勝手含めて要検証) - Union型の検証次第だが、総合して考えるとPHPで throw / try-catch-finally による例 外ハンドリングは避けられなさそう - 仮にジェネリクスとenum型がPHPに実装されたらまた検討してみてもいいかもしれません 45
  46. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. 参考文献 -

    Steve McConnell,クイープ訳,CODE COMPLETE 第2版 上 完全なプログラミングを目指して, 日経BP - アンドリュー・ハント,デービッド・トーマス,村上 雅章訳,達人プログラマー,初版,ピアソン・ エデュケーション - バートランド・メイヤー,酒匂 寛訳,オブジェクト指向入門,第2版,原則・コンセプト,翔泳社 - Jim Blandy,JasonOrendorff,中田 秀基訳,プログラミングRust,初版,オライリー - Steve Klabnik,Carol Nichols, 尾崎 亮太訳,プログラミング言語Rust公式ガイド,初 版,KADOKAWA - 瀬良 和弘,水島 宏太,河内 崇,麻植 泰輔,青山 直紀,実践Scala入門,初版,技術評論社 46
  47. © 2012-2019 BASE, Inc. © 2012-2021 BASE, Inc. ご清聴ありがとうございました