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

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

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しない例外処理をどう実装していくのかと、実際に実装した結果どのようなメリット/デメリットが得られたのかを説明しています。

tanden

May 29, 2021
Tweet

More Decks by tanden

Other Decks in Programming

Transcript

  1. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    2021/05/29 PHP カンファレンス沖縄 2021
    炭田高輝(@tac_tanden)
    PHP で throw しない
    例外ハンドリング

    View full-size slide

  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

    View full-size slide

  3. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    本日のアジェンダ



    PHPでの例外処理
    例外を使わない言語たち
    PHPでthrowしない例外処理
    3

    View full-size slide

  4. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    例外とはなにか?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    「例外」とはなにか
    やはり難しい
    1

    View full-size slide

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

    View full-size slide

  12. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    例外ハンドリングとは?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    「例外」と例外
    以降のページでは概念としての例外を例外と書き
    プログラミング言語の機能としての例外を「例外」と書きます
    15

    View full-size slide

  16. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    本日のアジェンダ



    PHPでの「例外」処理
    「例外」を使わない言語たち
    PHPでthrowしない「例外」処理
    16

    View full-size slide

  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 {
    // 必ず実行したい処理を行う
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  21. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    本日のアジェンダ



    PHPでの「例外」処理
    「例外」を使わない言語たち
    PHPでthrowしない「例外」処理
    21

    View full-size slide

  22. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    「例外」を使わない言語たち
    「例外」を使わずに
    例外を処理する
    言語がある
    2

    View full-size slide

  23. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    「例外」を使わない言語たち
    今回取り上げる3つの言語
    Rust
    - すべて自学自習による知識で、残念ながら有識者のレビューを受けたものではありません
    - 間違っていたらフィードバックいただけると嬉しいです
    Scala
    Go
    2

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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の値に対して処理を行う

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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型に用意されているメソッドを使って
    成功時の値、エラーの値の処理を行う

    View full-size slide

  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

    View full-size slide

  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であれば成功
    呼び出し元が必ず例外処理を行うようにする

    View full-size slide

  36. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    本日のアジェンダ



    PHPでの「例外」処理
    「例外」を使わない言語たち
    PHPでthrowしない「例外」処理
    36

    View full-size slide

  37. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    PHPでthrowしない例外ハンドリング
    - アイディアとしては2つ
    - Either, Resultのようなクラスを作り、メソッドの返り値として使う
    - 多値返却
    37

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  47. © 2012-2019 BASE, Inc.
    © 2012-2021 BASE, Inc.
    ご清聴ありがとうございました

    View full-size slide