「Functional PHP」PHPのための関数型プリミティブライブラリを触ってみた

INDEX
1. Functional PHPとは
PHPのための関数型プリミティブの機能セットです。2020年11月現在ではGitHubのスターを1.7k稼いでいるライブラリです。
2. 開発環境(Docker)の準備
筆者の環境
- Windows 10 Home
- WSL2
- Docker Desktop for Windows
検証ではPHP7.4.11を使用します。
PHPのDockerfileの作成
まずは Dockerfileをプロジェクト直下のphpディレクトリに配置します。
php/Dockerfile
1 2 3 4 5 6 7 |
FROM php:7.4.11-cli-alpine3.12 COPY --from=composer:2.0.3 /usr/bin/composer /usr/bin/composer RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" WORKDIR /app |
docker-compose.ymlの作成
docker-compose.yml をプロジェクト直下に作成します。docker-compose.yml
1 2 3 4 5 6 7 8 9 10 |
version: '3.8' services: php: image: functional-php build: context: php volumes: - .:/app - .composer:/.composer |
Dockerイメージのビルド
Dockerイメージをビルドします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
docker-compose build Building php Step 1/4 : FROM php:7.4.11-cli-alpine3.12 7.4.11-cli-alpine3.12: Pulling from library/php 188c0c94c7c5: Pull complete 45f8bf6cfdbe: Pull complete ce5be7974012: Pull complete a99dd6507fe5: Pull complete 0cee627b08be: Pull complete 188c0c94c7c5: Already exists 45f8bf6cfdbe: Already exists ce5be7974012: Already exists a99dd6507fe5: Already exists 0cee627b08be: Already exists 3254298999d0: Already exists da334ff0aaed: Already exists 4af1c15ddf7f: Already exists 671eb66ee260: Already exists b5c7f3b07799: Pull complete dadd4f316251: Pull complete 5ae0c75b60c1: Pull complete ef7aea22e3f7: Pull complete b1d073c7016a: Pull complete 3d1b220888ad: Pull complete Digest: sha256:42208c454c29be9303b0e4688e6d4f2dde25b968cf1128d8b2e790bd184bb33c Status: Downloaded newer image for composer:2.0.3 ---> be7bc09f0fb9 Step 3/4 : RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" ---> Running in 20694db3e1b2 Removing intermediate container 20694db3e1b2 ---> 468abf5ee70f Step 4/4 : WORKDIR /app ---> Running in f99b55db4553 Removing intermediate container f99b55db4553 ---> 43002b85da26 Successfully built 43002b85da26 Successfully tagged functional-php:latest |
ビルドしたDockerイメージでPHP7.4.11の実行を確認します。
1 2 3 4 5 6 |
docker-compose run --rm -u $(id -u):$(id -g) php php -v Creating functional-php_php_run ... done PHP 7.4.11 (cli) (built: Oct 22 2020 06:47:39) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies |
DockerイメージのビルドとPHP7.4.11の実行環境の動作確認ができました。
ここまでのディレクトリ構成
1 2 3 4 5 6 7 |
. ├── .composer/ │ └── .gitkeep ├── php/ │ └── Dockerfile ├── docker-compose.yml └── .gitignore |
3. Functional PHPのインストール
Functional PHPを Packagist からcomposerでインストールしていきます。
以下のコマンドでインストールします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
docker-compose run --rm -u $(id -u):$(id -g) php composer require lstrojny/functional-php Creating functional-php_php_run ... done Using version ^1.14 for lstrojny/functional-php ./composer.json has been created Running composer update lstrojny/functional-php Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - Locking lstrojny/functional-php (1.14.1) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals - Downloading lstrojny/functional-php (1.14.1) - Installing lstrojny/functional-php (1.14.1): Extracting archive Generating autoload files |
※ docker-compose コマンドの実行オプションで -u $(id -u):$(id -g) を指定する事でrootユーザーでvendorディレクトリが作られることを回避します。
学習テスト用にPHPUnitもインストールします。
1 2 3 4 5 6 7 8 9 10 |
docker-compose run --rm -u $(id -u):$(id -g) php composer require --dev phpunit/phpunit Creating functional-php_php_run ... done Using version ^9.4 for phpunit/phpunit ./composer.json has been updated Running composer update phpunit/phpunit ⋮ 6 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files 26 packages you are using are looking for funding. Use the `composer fund` command to find out more! |
composer.json にautoload-devの設定を追加します。
composer.json
1 2 3 4 5 6 7 8 9 10 11 12 13 |
--- a/composer.json +++ b/composer.json @@ -4,5 +4,10 @@ }, "require-dev": { "phpunit/phpunit": "^9.4" + }, + "autoload-dev": { + "psr-4": { + "Test\\": "tests/" + } } } |
phpunit.xml.dist
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" verbose="true" stopOnFailure="false" processIsolation="false" backupGlobals="false" cacheResult="false" > <testsuites> <testsuite name="Testing"> <directory>tests</directory> </testsuite> </testsuites> </phpunit> |
ここまでのディレクトリ構成
1 2 3 4 5 6 7 8 9 10 |
. ├── .composer/ ├── php/ │ └── Dockerfile ├── vendor/ ├── composer.json ├── composer.lock ├── docker-compose.yml ├── .gitignore └── phpunit.xml.dist |
Functional PHPを触ってみる準備が完了しました。
4. map コレクションの各要素をマッピング
mapはコレクションの各要素にコールバック関数を適用して結果を返す関数です。
ドキュメントとテストコードを見ながらmapを使ってみます。
以下はコレクションの各要素にプラス1する関数を適用するテストコードです。
tests/MapTest.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?php declare(strict_types=1); namespace Test; use ArrayIterator; use Generator; use PHPUnit\Framework\TestCase; use function Functional\map; class MapTest extends TestCase { /** * @dataProvider コレクションの型別データ * @param mixed $collection */ public function test_コレクションの各要素にプラス1するコールバック関数を渡すと適用した結果を返す($collection) { $fn = function (int $v) { return $v + 1; }; $expected = range(2, 11); $this->assertSame($expected, map($collection, $fn)); } /** * @return Generator */ public function コレクションの型別データ(): Generator { $parameter = range(1, 10); yield 'Array' => [$parameter]; yield 'ArrayIterator' => [new ArrayIterator($parameter)]; } } |
実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
docker-compose run --rm -u $(id -u):$(id -g) php vendor/bin/phpunit --testdox Creating functional-php_php_run ... done PHPUnit 9.4.2 by Sebastian Bergmann and contributors. Runtime: PHP 7.4.11 Configuration: /app/phpunit.xml.dist Map (Test\Map) ✔ コレクションの各要素にプラス 1するコールバック関数を渡すと適用した結果を返す with Array 3 ms ✔ コレクションの各要素にプラス 1するコールバック関数を渡すと適用した結果を返す with ArrayIterator 1 ms Time: 00:00.004, Memory: 6.00 MB OK (2 tests, 2 assertions) |
1から10の配列やイテレータの各要素にコールバック関数が適用された結果が取得できました。
5. select (filter) コールバックを使用してリスト内の各要素をフィルタリング
selectはコレクションの各要素にコールバック関数を適用してフィルタリングした結果を返す関数です。 JavaScriptの配列型オブジェクトAPIのfilterと同じ用途ですね。
Functional PHPにもエイリアスとしてfilter関数が定義されています。
こちらもドキュメントとテストコードを見ながらfilterを使ってみます。
以下はコレクションの各要素で偶数の数値を抽出するテストコードです。
tests/SelectTest.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?php declare(strict_types=1); namespace Test; use ArrayIterator; use Generator; use PHPUnit\Framework\TestCase; use function Functional\select; class SelectTest extends TestCase { /** * @dataProvider コレクションの型別データ * @param mixed $collection */ public function test_コレクションの各要素で偶数を抽出した結果を返す($collection) { $fn = function (int $v) { return $v % 2 === 0; }; $expected = [2, 4, 6, 8, 10]; $this->assertSame($expected, select($collection, $fn)); } /** * @return Generator */ public function コレクションの型別データ(): Generator { $parameter = range(1, 10); yield 'Array' => [$parameter]; yield 'ArrayIterator' => [new ArrayIterator($parameter)]; } } |
実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
docker-compose run --rm -u $(id -u):$(id -g) php vendor/bin/phpunit tests/SelectTest.php --testdox Creating functional-php_php_run ... done stty: standard input PHPUnit 9.4.2 by Sebastian Bergmann and contributors. Runtime: PHP 7.4.11 Configuration: /app/phpunit.xml.dist Select (Test\Select) ✘ コレクションの各要素で偶数を抽出した結果を返す with Array 3 ms ┐ ├ Failed asserting that two arrays are identical. ┊ ---·Expected ┊ +++·Actual ┊ @@ @@ ┊ Array &0 ( ┊ -····0·=>·2 ┊ -····1·=>·4 ┊ -····2·=>·6 ┊ -····3·=>·8 ┊ -····4·=>·10 ┊ +····1·=>·2 ┊ +····3·=>·4 ┊ +····5·=>·6 ┊ +····7·=>·8 ┊ +····9·=>·10 ┊ ) │ ╵ /app/tests/SelectTest.php:24 ┴ ✘ コレクションの各要素で偶数を抽出した結果を返す with ArrayIterator 1 ms ┐ ├ Failed asserting that two arrays are identical. ┊ ---·Expected ┊ +++·Actual ┊ @@ @@ ┊ Array &0 ( ┊ -····0·=>·2 ┊ -····1·=>·4 ┊ -····2·=>·6 ┊ -····3·=>·8 ┊ -····4·=>·10 ┊ +····1·=>·2 ┊ +····3·=>·4 ┊ +····5·=>·6 ┊ +····7·=>·8 ┊ +····9·=>·10 ┊ ) │ ╵ /app/tests/SelectTest.php:24 ┴ Time: 00:00.012, Memory: 6.00 MB FAILURES! Tests: 2, Assertions: 2, Failures: 2. |
テストが通りませんでした。
結果を見るとコレクションのインデックスを維持した結果を返すようです。
テストコードを修正して再度テストを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
--- a/tests/SelectTest.php +++ b/tests/SelectTest.php @@ -21,7 +21,8 @@ class SelectTest extends TestCase }; $expected = [2, 4, 6, 8, 10]; - $this->assertSame($expected, select($collection, $fn)); + // array_valuesでインデックスを振りなおす + $this->assertSame($expected, array_values(select($collection, $fn))); } /** |
再実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
docker-compose run --rm -u $(id -u):$(id -g) php vendor/bin/phpunit tests/SelectTest.php --testdox Creating functional-php_php_run ... done PHPUnit 9.4.2 by Sebastian Bergmann and contributors. Runtime: PHP 7.4.11 Configuration: /app/phpunit.xml.dist Select (Test\Select) ✔ コレクションの各要素で偶数を抽出した結果を返す with Array 3 ms ✔ コレクションの各要素で偶数を抽出した結果を返す with ArrayIterator 1 ms Time: 00:00.005, Memory: 6.00 MB OK (2 tests, 2 assertions) |
テストが通るようになりました。
JavaScriptのfilter関数がお馴染みの方はselect関数のエイリアスとしてfilter関数も定義されていますので好みで使う関数を選べるようになっています。
6. 実行速度の計測
どのくらい実行速度に差が出るか比較してみます。
1から100万までの数値が入った配列の各要素を1加算する処理の実行時間を簡易的な方法ですが計測してみました。
書き方 | 実行時間 |
---|---|
foreach | 0.38007283210754 |
array_map | 0.5925190448761 |
Functional\map | 1.2633831501007 |
Oh… 😱
動作速度が遅く速度が求められる要件では注意が必要ですね。
計測で使用したソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<?php declare(strict_types=1); use function Functional\map; require_once __DIR__.'/vendor/autoload.php'; function createStopwatch(): Closure { $start = microtime(true); return function() use ($start) { $end = microtime(true); return $end - $start; }; } function loop(array $range): float { $stopwatch = createStopwatch(); $result = []; foreach ($range as $item) { $result[] = $item + 1; } return $stopwatch(); } function arrayFunction(array $range): float { $stopwatch = createStopwatch(); array_map( function(int $i): int { return $i + 1; }, $range ); return $stopwatch(); } function functionalPHP(array $range): float { $stopwatch = createStopwatch(); map($range, function(int $i):int { return $i + 1; }); return $stopwatch(); } function xrange() { return range(1, 10000000); }; echo loop(xrange()), PHP_EOL; echo arrayFunction(xrange()), PHP_EOL; echo functionalPHP(xrange()), PHP_EOL; |
7. まとめ
for文でコレクションのループ中に何かしら処理を行う場合には、一時変数に結果を代入する事が多く、下の例では一時変数 result が可変になります。
1 2 3 4 |
$result = []; for ($i = 0; $i < 10; $i++) { $result[] = $i + 1; } |
好みにもよると思いますが、 筆者は上の例では簡単な処理なので脳内でどういう状態かを把握できますが、 複雑な処理になってきた場合に、一時変数が脳内のメモリを圧迫していきます。
その点、一時変数を使わない、変数を不変に保つようなmapなどの関数型な操作は便利だと思います。
ただし、計測の通り実行速度は確実にその他の方法よりも遅くなるので速度が求められる要件では注意が必要です。 その他でもテストコードを書いていて配列以外の型を渡した場合でも結果が全てプリミティブな配列に変換されてしまうのが少し気になりました。
簡易的ですがFunctional PHPのご紹介は以上です。
公式のGitHubではその他の関数型プリミティブのドキュメントがありますので、 興味のある方は覗いてみてください。

美髭公

最新記事 by 美髭公 (全て見る)
- プロダクトにReact Testing Library(RTL)を導入してみてハマったこと - 2022年5月13日
- Reactのerror boundaryでキャッチされないエラーをキャッチできるようにする - 2022年3月17日
- Google Workload identity federationでGitHub Actionsを設定してみた - 2022年3月17日
- Reactのprops drilling(バケツリレー)とhooksに我々はどう立ち向かっていけばよいのか - 2021年11月8日
- Windowsで至高のターミナル生活を求めて(番外編:ArchLinux on WSL2) - 2021年9月9日
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー