かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

PHP 型チェックを厳密にしたい

前回 PHP の型宣言が暗黙の型変換をしてしまってそんなものかーと思っていたのですが、厳密にするオプションが有ることを教えてもらったので記録に残しておきます。

環境

declare(strict_types=1); 宣言を行うと厳密な方チェックができる

declare(strict_types=1); 宣言なし (default)

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}
myFunc(10); // => 10
myFunc('1'); // => 1
myFunc(null); // => 'default'
myFunc(true); // => 1
myFunc(false); // => 0
myFunc('-2.5'); // => -2
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

文字列の数字や float 型、Bool値など暗黙の型変換ができる値がすり抜けてしまう

declare(strict_types=1); 宣言あり

<?php declare(strict_types=1);
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}
myFunc(10); // => 10
myFunc('1');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given 
myFunc(null); // => 'default'
myFunc(true);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, bool given
myFunc(false);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, bool given
myFunc('-2.5');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, float given
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

null は通りますが、int 型以外の引数はエラーになりました!
型宣言を使う場合は基本的に declare(strict_types=1); 宣言を行うのが良さそうです

declare(strict_types=1); の影響範囲について

1. declare(strict_types=1); 宣言はファイルの先頭でなければならない

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
  echo "\n";
}
declare(strict_types=1);
// Fatal error: strict_types declaration must be the very first statement in the script

2. declare(strict_types=1); は宣言されたファイル内でのみ有効

  1. strict モードは宣言されたファイル内でのみ有効になる
  2. strict モード宣言のあるファイルでも、呼び出し側のファイルが strict モードで無ければ厳密な型チェックは行われない
    => 呼び出し元の指定が優先される
呼び出し元が strict モードでない場合、読み込んだファイルが strict モードでも厳密な型チェックは行われない

function.php

<?php declare(strict_types=1);
function add(int $x, int $y): int {
    return $x + $y;
}

index.php

<?php
include 'functions.php';
echo add(1.5, 2.5);
// => 3
呼び出し元が strict モードなら、読み込んだファイルが strict モードでなくとも厳密な型チェックが行われる

function.php

<?php
function add(int $x, int $y): int {
    return $x + $y;
}

index.php

<?php declare(strict_types=1);
include 'functions.php';
echo add(1.5, 2.5);
// Fatal error: Uncaught TypeError: add(): Argument #1 ($x) must be of type int, float given

PHP は include / require をしているファイルにコードが挿入されて strict モードの指定は大本のファイルのでの指定が残ると捉えると理解しやすそう

Note that the include construct loads the code from another file into a file. And you’ll learn more about it in the later tutorial.
When you call a function defined in a file with strict typing (functions.php) from a file without strict typing (index.php), PHP will respect the preference of the caller (index.php). That means it’s up to the caller to decide whether to use the strict mode or not. In this case, the index.php won’t execute in the strict mode.
When you include code from another file, PHP uses the mode of the caller.
cf. PHP strict_types

Note. strict モードのファイルが中間にある場合

strict モードの指定は呼び出し元の指定が優先されるとあり、Qiitaの記事 に多重でファイルを読み込んだ場合、関数の実行ファイルに declare(strict_types=1); があれば呼び出し元に strict モード宣言がなくともエラーになったとあったので複数ファイルが作成できる paiza.io の PHP v8.1.9 で試してみました

b.php

<?php
function add(int $x, int $y): int {
    return $x + $y;
}

a.php (strict モード)

<?php declare(strict_types=1);
include_once('b.php');

function add_square(int $x, int $y): int {
  $sum = add($x, $y);
  return pow($sum, 2);
}

Main.php

<?php 
include_once('a.php');
echo phpversion();
// => 8.1.9

echo add_square(2, 3);
// => 25
echo add_square(2.0, 3.0);
// => 25
echo add_square(true, false);
// => 1

👇実行結果サンプル

Qiita の記事の例だと strict 宣言のある a.php からの呼び出しで add_square(2.0, 3.0) がエラーになると思ったのですが、そのまま実行されました。PHP のバージョンで挙動が異なるのでしょうか?

今回は詳細には調べませんが、phptutorial.net に書かれているように最終的な呼び出し元 今回は Main.php の指定が優先される。という認識で良いのかな〜と思いました。

まとめ

PHP でプロジェクトを作成するときはファイルの先頭に declare(strict_types=1); 宣言を書いて厳密な型チェックを行う方が幸せになれそう!
厳密な型チェックの存在を教えてくれた いち(@ichiart4) さんありがとう〜♡♡♡ (アイカツ!のフレンズ優秀な方が多すぎてありがたい〜)

おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]

アイカツ!を見るとプログラミングも描けるようになる!!!!

PHP オプショナルな引数の初期値を設定したい

最近 WordPress を触る機会が復活してきて PHP の書き方忘れまくってるので TIL 的にメモを残しておこうと思いました。

オプショナルな仮引数を持つ関数内で変数の初期値を設定したい

<?php
function myFunc($x = null) {
  // $foo に初期値を設定したい
  $foo = $x;
  var_export($foo);
}
環境

🙅 ?: 三項演算子の省略形は falsy な値も弾かれてしまう

$x = $a ?: $b$x = $a ? $a : $b と同等。
この時 $a ?!empty($a) ? と同等なので falsy な値が引数として渡された時にデフォルト値が使われてしまう

<?php
function myFunc($x = null) {
  $foo = $x ?: 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => 'default'
myFunc(0); // => 'default'
myFunc(''); // => 'default'
myFunc([]); // => 'default'

Falsy な値の時もデフォルト値を使いたい場合は良いと思うけど、その挙動にしていることを覚えてないとバグの温床になりそうな気がする。

🙆 ?? null合体演算子を使うのが良い (PHP v7.x 以上)

$x = $a ?? $bif (isset($a)) { $x = $a } else { $x = $b } と同等。

<?php
function myFunc($x = null) {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => false
myFunc(0); // => 0
myFunc(''); // => ''
myFunc([]); // => array()

isset() なので引数に null を意図的に渡した場合はデフォルト値が返される

cf. PHP 7 変数に値がなければデフォルト値を設定したい。 - かもメモ

PHP v7.4 以上では ??= null合体代入演算子 が使える

$a ??= $b$anull なら $a$b を代入する。

<?php
$a ??= $b;
// 下記と同等
$a = $a ?? $b;

null合体代入演算子 ??= を使うと下記の用に書き換えられる

<?function myFunc($x = null) {
  $x ??= 'default';
  var_export($x);
}

myFunc(); // => 'default'
myFunc(1); // => 1
myFunc(null); // => 'default'
myFunc(false); // => false
myFunc(0); // => 0
myFunc(''); // => ''
myFunc([]); // => array()

個人的に null合体代入演算子 ??= は元の引数の状態を変えてしまうのであまり好みではないかも。


引数を型で縛る

そもそも引数に何でも渡せてしまうのがバグ発生の元。
PHP の型宣言を使って入力値を絞ってしまえば関数内での考慮することがシンプルになります

PHP5から「タイプヒンティング(型宣言)」は存在したが、そこからずっと中途半端な状態が続きようやくPHP7で機能拡張されたことによってスカラ型の宣言も行えるようになった。 また、PHP7.2からobject型も扱えるようになっている。
cf. [PHP] 型宣言(PHP7) - Qiita

null を許容する型宣言

nullable (PHP v7.1 以降)
型名の前に ? を付けると nullable な型になる

<?php
function myFunc(?int $x): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(10); // => 10
myFunc(null); // => 'default'
myFunc("");
// => Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int
myFunc();
// => Fatal error: Uncaught ArgumentCountError: Too few arguments to function myFunc(), 0 passed

nullable な型は null を渡すことができるがデフォルト引数を設定してないとオプショナルな引数という感じではない
そもそも null を明示的に渡したい理由がある場合に使ったほうが良さそう

デフォルト引数だけで null を許容する

仮引数の型宣言があってもデフォルト引数で null を設定することができる
これを利用すると型付のオプショナルな引数を作れる

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(10); // => 10
myFunc([]);
// => Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int,

PHP の型宣言は型を厳密にチェックしてるのではない!?

先の例を試していて int: $x と型宣言をしているにも関わらず 1null, true, false などは型エラーにならず関数が実行されてしまいました。

<?php
function myFunc(int $x = null): void {
  $foo = $x ?? 'default';
  var_export($foo);
}

myFunc(); // => 'default'
myFunc(10); // => 10
// 型が異なるが実行されてしまう
myFunc('1'); // => 1
myFunc(null); // => 'default'
myFunc(true); // => 1
myFunc(false); // => 0

// float は warning が出るが int に変換されて実行される
myFunc(1.2); // => 1
// Deprecated: Implicit conversion from float 1.2 to int loses precision
myFunc('-2.5'); // => -2
// Deprecated: Implicit conversion from float-string "-2.5" to int loses precision

// 型エラーになるもの
myFunc('');
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, string given
myFunc([]);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, array given
class Bar { public $x = 1; };
$bar = new Bar();
myFunc($bar);
// Fatal error: Uncaught TypeError: myFunc(): Argument #1 ($x) must be of type ?int, Bar given

挙動を見ている感じ暗黙の型変換が行えるかどうかをチェックしているように感じました。
TypeScript などのような型でのチェックができる訳ではなさそうなので注意が必要そうです

結論: オプショナルな引数の初期値を設定した関数の作成

PHP 7 以上であれば関数の仮引数に 型宣言 + デフォルト引数に null を設定して、null 合体演算子を使ってデフォルト値を与えるのが良さそう

<?php
function myFunc(int $x = null): void {
  $a = $x ?? 'default value';
  // ...
}

⚠ ただし PHP の型宣言は型変換が可能なものは通ってしまうので注意が必要

おわり ₍ ᐢ. ̫ .ᐢ ₎

追記 厳密な型チェックの方法を教えてもらいました!


[参考]

お仕事先でこれ読んでますが凄く知見がたまる感があって大変良いです!

TIL: 冪等性、参照透過性、純粋関数 についてのメモ

勉強会で 読みやすいコードのガイドライン をやっていて冪等であるのがよい。と書かれていたのですが冪等についてふわふわした感覚だったので調べたメモ。
⚠ しっかり調べた訳ではないので解釈が間違っている可能性があります :pray:

冪等性 idempotency, idempotence

  • 同じ操作を何度繰り返しても、同じ結果が得られるという性質
    • f(x) = f(f(x)) が成立する (数学的な厳密な冪等性?)
      • abs(-100) = abs(abs(-100)) なので abs() 関数は冪等性がある
      • sqrt(16) === sqrt(sqrt(16)) は成立しないので sqrt() 関数は冪等性がない
      • Math.random() は実行する度に得られる結果が異なるので冪等性はない
      • 圧縮する zip を zip(zip(x)) とすると archive が二重になるので zip(x) = zip(zip(x)) は成立しないから zip() は冪等性がない
    • 何回呼び出しても同じ結果になる
      • import 'react' は何回呼び出しても同じなので import は冪等性がある
      • tsc でのコンパイルはコードと設定が同じなら何回実行しても同じ結果になるので tsc は冪等性がある
      • buttom.toggle() のような関数は実行のたびに対象の状態が true 又は false と変化するので冪等性はない
      • toggle() 関数が状態に関わらず常に void undefined を返すのなら外からみて冪等性があるのだと思うが、対象の状態が副作用的に変化して扱う側はそれを覚えておかなければならないので冪等性が無いとして扱ったほうが良さそう
  • 冪等性は外からみた性質で、内部実装に違いが合っても良い
    • 例: メールアドレスをアップデートするAPI関数で、現在のメールアドレスと同じであれば関数内では変更しないように処理が異なっても API を実行する側が内部や状態を意識しなくて住むのであれば冪等性がある

下記は API を使う側からすれば「同じ操作を何度繰り返しても、同じ結果が得られるという性質」があるので冪等性がある

// 常にアップデートする API関数
function updateEmail(user, newEmail) {
  sql.exec("UPDATE users SET email = {newEmail} WHERE id = {user.id}");
  return;
}

// 内部で処理が異なるが常に同じ結果を返す API 関数
function updateEmail(user, newEmail) {
  if (user.email == newEmail) {
    // 更新はしない
  } else {
    sql.exec("UPDATE users SET email = {newEmail} WHERE id = {user.id}")
  }
  return;
}

cf.

参照透過性 Referential transparency

  • プログラムの構成要素が同じ者同士は等しい -> A === A
    • 2 === 2 は等しい
    • x = 1 の時 x === x は等しい
  • 変数の値は最初に定義した値と常に同じ -> var, let でなく const
  • 変数に値を割り当てなおす演算である代入 (Assignment) を行う式は存在しない

変数などがどこに記録されている値を参照しているのかを考慮する必要がない = 参照透過性がある

  • abs(-100) は常に 100 で副作用も持たないので abs() 関数には参照透過性がある (冪等性もある)
  • sqrt(16) は常に 4 で副作用も持たないので sqrt() 関数には参照透過性がある (冪等性は無い)
1. 可変なデータは参照透過性を破壊する

JavaScriptarray.push() のような破壊的メソッドは参照している値を変化させるので参照透過性がない

const arr = [1];
arr.push(2);
console.log(arr); // => [1, 2];
arr.push(3);
console.log(arr); // => [1, 2, 3];
2. 代入は参照なので代入を用いると参照透過性が破壊される
let x = 0;
add = (y: number): number => {
  x = x + 1;
  return x + y;
}

add(1); // => 2
add(1); // => 3
3. 副作用(side effect) は、関数外の機能・結果に影響をもたらすので参照透過性がない

React の非同期関数でデータを取得し状態を変更するのは、入力値 (props) とは別に返却するコンポーネント (return される JSX) を変更するので副作用であり、参照透過性が無い

import { useState, useEffect, FC } from 'react';

type UserInfoProps = {
  userId: number;
};

const UserInfo: FC<UserInfoProps> = ({ userId }) => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/user/{userId}`)
      .then((res) => res.json())
      .then((json) => setUser(json));
  }, [userId]);
  
  return user ? <h1>Hello, {user.name}</h1> : <div>Loading…</div>
}

最初にUserInfoコンポーネント(関数)は userId を入力に受け取り実行された時は <div>Loading...</div> 返す。しかしその後で useEffect 内の非同期処理が完了した後に <h1>Hello, {user.name}</h1> を返すので返却する値が変わってしまう

同様に head の title などコンポーネント外の DOM を書き換えるのもコンポーネント ( = 関数) の外の DOM を参照して変更しているので副作用であり、参照透過性がない

import { useEffect, FC } from 'react';

const Example: FC = () => {
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>Example Component</div>
  );
}

cf.

関数の参照透過性

同じ引数で何度実行されても同じ値を返す

純粋関数 Pure functional

  • 参照透過性が担保された関数
    • 同じ引数で何度実行されても同じ値を返す
  • 副作用がない
    • 変数の値は最初に定義した値と常に同じ
    • 代入を用いないので関数外の変数を変更することもない

下記の関数は引数が同じなら何度実行されても同じ値を返すので参照透過性のある純粋関数

function succ(x: number) {
  return x + 1;
}

参照透過性が成り立つ言語は式の値がプログラムのテキストから定まるという特徴から宣言型言語 (Declarative language) と呼ばれたり、関数の数学的性質が保たれるという特徴から純粋関数型言語 (Pure functional language) と呼ばれたりする。
一方変数の値の変更が認められているような参照透過的でない言語を手続き型言語と呼ぶ。
cf. 参照透過性 - Wikipedia


[参考]

ぼっち・ざ・ろっく 面白い