Skip to content

純粋 vs 副作用

公開日:

東京都大田区蒲田大田区産業プラザ PiOで開催された『PHPカンファレンス2025』でレギュラートーク(25分)として発表しました。

Download PDF

スライドテキスト

Page 1

純粋 vs 副作用

PHPはなぜ難しいのか?

Pure vs. Side Effects: Navigating PHP's Complexities

pixiv Inc.

USAMI Kenta

2025-06-28 #phpcon PHPConference Japan 2025

Page 2

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE / にゃんだーすわん
  • ピクシブ株式会社 Platform Div > WebTechnology Team PHPer
  • 2012年末から現職、APIとかCIとかいろいろなところを見つめてきました
  • 最近チームが再編されてインフラっぽい仕事もしてます
  • Emacs PHP Modeを開発しています (2017年-)
  • プログラミング言語にちょっとこだわりのある素人 (spcamp2010)

Page 3

Page 4

Page 5

Page 6

今回のお題

Page 7

Page 8

純粋関数 pure function

副作用 side effect

Page 9

きいたことありますか?

Page 10

数学的な関数?

f(x) sin(x)

Page 11

関数fのxに2を代入する

f(x) = 2x

f(2) = 4

Page 12

関数fのxに2を代入する

2に関数fを適用する

f(x) = 2x

f(2) = 4

Page 13

f(x) = 2x

f(x)f(0)f(1)f(2)f(3)

2x

Page 14

f(x)f(0)f(1)f(2)f(3)

2x

Page 15

Page 16

いつどこで計算しても結果は同じ!

数学的な関数

f(x) sin(x)

Page 17

プログラミングにおける関数

Page 18

今日は特に区別しません!

Page 19

f(x) = 2x

$f = function($x) {return 2 * $x;};

Page 20

f(x) = 2x

$f = fn($x) => 2 * $x;

Page 21

Page 22

0〜3までの数に $f 関数を適用する

f(x) = 2x

f(x)f(0)f(1)f(2)f(3)

2x

Page 23

0〜3までの数に $f 関数を適用する

$f = fn($x) => 2 * $x;

array_map($f, [0, 1, 2, 3]);

f(x)f(0)f(1)f(2)f(3)

2x

Page 24

Page 25

Page 26

Page 27

PHP関数

=数学の関数

Page 28

PHP関数

≠数学の関数

Page 29

関数の結果を画面に表示する

$f = fn($x) => 2 * $x;

echo "f(2) = ", $f(2);echo "sin(0) = ", sin(0);

Page 30

関数の結果を画面に表示する

われわれはvar_dump() も関数と呼んでいる

$f = fn($x) => 2 * $x;

var_dump(["f(2)" => $f(2)]);var_dump(["sin(0)" => sin(0));

Page 31

関数の結果を画面に表示… しない

計算した結果をどこにも使わない

$f = fn($x) => 2 * $x;

計算資源の無駄!

$f(2);sin(0);

SDGsに反する!

Page 32

なぜ同じような関数なのに違いが…

関数はひとつ!じゃない!!

画面に文字を表示する

文字列を生成して

printf("Hello, world!");sprintf("Hello, world!");

捨てている!

Page 33

画面に文字を表示する(or HTTP出力)

作った文字列は受け取らないと意味ない

Page 34

なぜ差がついたのか…

慢心・環境の違い

Page 35

状況を整理しましょう

Page 36

PHPの「関数」には種類がある

  • 数学っぽい関数
  • 同じ引数を渡したとき、関数の呼び出し結果(戻り値)は必ず同じになる
  • 戻り値は受け取らないと無駄になる

Page 37

PHPの「関数」には種類がある

  • 数学っぽい関数
  • 同じ引数を渡したとき、関数の呼び出し結果(戻り値)は必ず同じになる
  • 戻り値は受け取らないと無駄になる
  • 数学っぽくない関数
  • 関数の呼び出し結果(戻り値)は別に受け取らなくてもいい

何かってなんだよ…

  • どこかに何かの影響を及ぼす

Page 38

休憩

Page 39

話は変わって

Page 40

ユニットテストは好きですか?

Page 41

好きですか?

Page 42

好きかはともかくお得なことはいっぱい

Page 43

ユニットテストの基本スタイル

テスト対象の関数

function twice(int $n): int {return $n * 2;}

Page 44

ユニットテストの基本スタイル

期待値と結果を比較してチェック

function twice(int $n): int {return $n * 2;}

class TwiceFuncTest extends TestCase {function test(): void {$this->assertEquals(4, twice(2));}}

Page 45

ね、簡単でしょ?

Page 46

簡単に済まないのが

現実

Page 47

純粋 vs 副作用

PHPはなぜ難しいのか?

Pure vs. Side Effects: Navigating PHP's Complexities

pixiv Inc.

USAMI Kenta

2025-06-28 #phpcon PHPConference Japan 2025

Page 48

PHPの「関数」には種類がある

  • 数学っぽい関数
  • 同じ引数を渡したとき、関数の呼び出し結果(戻り値)は必ず同じになる
  • 戻り値は受け取らないと無駄になる
  • 数学っぽくない関数
  • 関数の呼び出し結果(戻り値)は別に受け取らなくてもいい

何かってなんだよ…

  • どこかに何かの影響を及ぼす

Page 49

いつの間にかどこかの何かに影響を及ぼす

Page 50

あるいは勝手にどこかの何かの影響を受ける

Page 51

何かってなんだよ…

Page 52

PHPの「関数」には罠がいっぱい!

  • 呼び出すごとに結果が変わる(かもしれない)
  • 日時関数: time(), date(), strtotime(), …etc.
  • ランダム関数: rand(), random_bytes(), …etc
  • 呼び出すごとにどこかに影響を及ぼす(かもしれない)
  • 出力関数: var_dump(), header(), ob_start(), setcookie() …etc.
  • 両方! DB・ファイルシステム関数・外部通信

Page 53

罠 = 副作用

Page 54

「テストしにくい」の正体

  • テスト対象を実行した結果が予想しにくい
  • 外部の状態の影響を受ける・影響を受ける対象が多すぎて把握できない
  • 現在時刻・DB/ファイルの内容・外部APIの実行結果・ネットワーク… etc.
  • テスト対象を実行した結果に何が起こるか制御できない
  • ファイルに書き込まれる、DBが更新される、不必要な外部リクエスト… etc.

Page 55

不規則に副作用を起こすコードは容易に制御不能になる

Page 56

秩序のないコードが人間に牙を剥く

Page 57

閑話休題

Page 58

ドラえもんご存じですか?

Page 59

侵略者が送り込んだ未知の技術

の電子頭脳に対して…

工学的アプローチによって

戦力化を提案する野比のび太

『大長編ドラえもん(7) のび太と鉄人兵団』著: 藤子・F・不二雄/藤子プロ1987年2月25日初版第1刷、2011年第86刷Kindle版 p97, p102より引用

Page 60

みたことのある回路になおしゃいいんだよ!

Page 61

のび太は技術者の鑑

Page 62

制御できないものは制御できる部分にブレークダウンする

Page 63

ヘキサゴナルアーキテクチャ(Hexagonal architecture翻訳)Alistair Cockburn著、tai2訳2015年10月9日公開、2025年6月28日閲覧より引用

Page 64

クリーンアーキテクチャ(The Clean Architecture翻訳)Robert Martin著、tai2訳2015年10月9日公開、2025年6月28日閲覧より引用

Page 65

副作用がある部分を切り離す

  • 任意の処理を純粋関数で書くということは「できない」
  • 現在時刻・DB/ファイルの内容・外部APIの実行結果・ネットワーク… etc.
  • それらに依存する部分を抽象化して切り離すことで、

テスト時にシンプルな偽物に差し替えてテストすることができる

  • DIは純粋関数型になれないオブジェクト指向言語に許された武器
  • 時刻への依存はPSR-20、乱数への依存は組込みのRandomizerで分離

Page 66

Page 67

Page 68

Page 69

制御できないものは制御できる部分にブレークダウンする

Page 70

「なんたらアーキテクチャ」は構造をわかりやすく図示してくれるが、必須ではない

Page 71

プログラム上で制御しやすい基本単位

#とは

Page 72

そうです

Page 73

純粋関数

Page 74

純粋の反対語は不純(impure)

純粋関数とは…

  • 参照透過性 (referential transparency)
  • 雑に言うと「同じ入力に常に同じ結果を返す」という性質
  • 副作用を持たない (No side effects)
  • 与えられたパラメータだけに基いて戻り値を計算して返す以外に、

外部から観測できる影響を与えない

  • 「純粋関数型」と呼ばれる言語(Haskellなど)は、不純な性質が混ざらない

基本的に…

Page 75

こんな関数は純粋じゃない!

  • 実装の中で不純な関数を使ったり、グローバル変数を書き換える
  • time(), file(), rand(), $_GET, …etc.
  • 明示的にecho, printなどで出力している
  • 変数参照(&$var)を使っている
  • クロージャで & を使わない外部変数参照(use)はOK

Page 76

関数の結果を画面に表示… されない

計算した結果をどこにも使わない

$f = fn($x) => 2 * $x;

計算資源の無駄!

$f(2);sin(0);

SDGsに反する!

Page 77

Page 78

やるじゃん

Page 79

全関数を純粋と不純に分けたい…

Page 80

PHPStanの純粋判定を支える仕組み

  • functionMetadata.hasSideEffects
  • PHPの標準関数が副作用を持つかどうかを管理するメタデータ(配列)
  • いまのところ、副作用がないものだけhasSideEffects=>falseと記録
  • JetBrains/phpstorm-stubsをインポート+個別補正している
  • ImpurePoints
  • 型と同じように、コード内で副作用が発生する箇所を管理している

Page 81

PHPDoc @pure タグ

  • DocCommentに@pureと書かれた関数は純粋として扱われる
  • その関数を呼び出した結果が無視されていると、もちろん警告
  • @pureがついた関数の実装に副作用があると、きちんと警告してくれる
  • ただし、それでも@pureの呼び出し側が無効化されるわけではない

Page 82

resources/functionMetadata.php

Page 83

JetBrains/phpstorm-stubs

  • PhpStormで使用するための関数メタデータを管理しているパッケージ
  • #[JetBrains\PhpStorm\Pure]というアトリビュートで純粋性を定義
  • 問題点
  • #[Pure]とマークされている関数の基準が曖昧
  • #[Pure(mayDependOnGlobalScope: true)] ?????

「不純な純粋」というTig-HugなOxymoron

Page 84

Page 85

Page 86

Page 87

純粋じゃなくても戻り値が大事な関数はある

Page 88

Page 89

CurlHandler/リソースは外部から観測できない内部状態に依存するので純粋と言いがたい

Page 90

PHP 8.5に導入される新機能

#[NoDiscard]

  • PHP RFC: Marking return values as important (#[\NoDiscard])
  • このアトリビュートがついた関数の戻り値を無視すると、

PHPランタイムがE_WARNINGを発生させてくれる

  • #[NoDiscard]が導入される8.5ではビルトイン関数に対しては

flock()とDateTimeImmutable::set*()のみ追加

  • 現状はちゃんと確認しないと事故るやつのみ、保守的にマークされてそう

Page 91

関数の結果を画面に表示… されない

計算した結果をどこにも使わない

$f = fn($x) => 2 * $x;

計算資源の無駄!

$f(2);sin(0);

SDGsに反する!無駄なので除去する

最適化

Page 92

ではこれを実装すればうまくいくのか?

Page 93

そう上手くいかない…

Page 94

これは両方純粋なので警告されるべき

Page 95

純粋な呼び出しなので警告されるべきではない

Page 96

原因はわかっていて書きかけのPRがある

Page 97

Page 98

_人人人人人人人人人_> 僕の怠惰が原因 <‾Y^Y^Y^Y^Y^Y^Y‾

Page 99

正直、体調不良と若干の燃えつきが重なっていた

Page 100

PHPStanを理解している人は限られている

Page 101

ガチでPHPStanを極めてみたい仲間を増やしたい!!!

Page 102

phpusers-ja Slack

Page 103