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 0 2 4 6

Page 14

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

2x 0 2 4 6

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 0 2 4 6

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 0 2 4 6

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(), Vle(), 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ではビルトイン関数に対しては
    (cid:138)ock()と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