vjsingla / php-functional
函子、应用函子和单子是迷人的概念。这个库的目的是在面向对象PHP世界中探索它们。
Requires
- php: ^7.1|^8.0|^8.1
- functional-php/fantasy-land: ^1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^v3.59
- giorgiosironi/eris: ^0.14
- phpunit/phpunit: ^9.6
This package is not auto-updated.
Last update: 2024-09-19 12:42:02 UTC
README
简介
函数式编程是一个迷人的概念。这个库的目的是在面向对象PHP中探索函子
、应用函子
和单子
,并提供实际用例的示例。
项目中的单子类型
State 单子
IO 单子
Free 单子
Either 单子
Maybe 单子
Reader 单子
Writer 单子
在探索函数式编程空间时,我发现处理PHP中的原始值非常困难,这给许多函数式结构的实现增加了复杂性。为了简化这一体验,这个库引入了一套高阶原始值。
Num
Sum
Product
Stringg
Listt
(又称列表单子,因为PHP中的list
是一个受保护的保留关键字)
应用
该项目的已知应用
- Algorithm W,基于Martin Grabmüller的工作,在PHP中实现。
安装
composer require widmogrod/php-functional
开发
这个仓库遵循语义版本控制概念。如果你想贡献,请遵循CONTRIBUTING.md
测试
质量保证由以下提供
- PHPUnit
- Eris - 快速检查和基于属性的测试工具,用于PHP和PHPUnit生态系统。
- PHP-CS-Fixer - 一个自动修复PHP编码标准问题的工具
composer test
composer fix
用例
你可以在示例目录中找到更多用例和示例。
注意:在浏览示例时,你将看到类似“列表函子”的短语,在这个库中你将看到
Widmogrod\Primitive\Listt
。单子是函子和应用函子。你可以这样说,单子实现了函子和应用函子。
列表函子
use Widmogrod\Functional as f; use Widmogrod\Primitive\Listt; $list = f\fromIterable([ ['id' => 1, 'name' => 'One'], ['id' => 2, 'name' => 'Two'], ['id' => 3, 'name' => 'Three'], ]); $result = $list->map(function($a) { return $a['id'] + 1; }); assert($result === f\fromIterable([2, 3, 4]));
列表应用函子
将函数应用于值的列表,并将结果作为所有可能的组合返回,即将左列表中的函数应用于右列表中的值。
[(+3),(+4)] <*> [1, 2] == [4, 5, 5, 6]
use Widmogrod\Functional as f; use Widmogrod\Primitive\Listt; $listA = f\fromIterable([ function($a) { return 3 + $a; }, function($a) { return 4 + $a; }, ]); $listB = f\fromIterable([ 1, 2 ]); $result = $listA->ap($listB); assert($result === f\fromIterable([4, 5, 5, 6]));
Maybe 合并
将Maybe用作Monoid的一个实例,通过使用Maybe对可能缺失的值的抽象,简化了concat和reduce操作。请参见示例,从姓名、中间名和姓氏构建一个人的全名,而无需显式检查每一部分是否存在。
Maybe和List单子
从值列表中提取值可能会很棘手,并产生包含大量if (isset)
语句的糟糕代码。通过结合List和Maybe单子,这个过程变得更简单、更易读。
use Widmogrod\Monad\Maybe; use Widmogrod\Primitive\Listt; $data = [ ['id' => 1, 'meta' => ['images' => ['//first.jpg', '//second.jpg']]], ['id' => 2, 'meta' => ['images' => ['//third.jpg']]], ['id' => 3], ]; // $get :: String a -> Maybe [b] -> Maybe b $get = function ($key) { return f\bind(function ($array) use ($key) { return isset($array[$key]) ? Maybe\just($array[$key]) : Maybe\nothing(); }); }; $result = f\fromIterable($data) ->map(Maybe\maybeNull) ->bind($get('meta')) ->bind($get('images')) ->bind($get(0)); assert(f\valueOf($result) === ['//first.jpg', '//third.jpg', null]);
Either 单子
在PHP世界中,表示错误最流行的做法是抛出异常。这会导致糟糕的try catch
块和许多if语句。Either单子展示了我们如何优雅地失败,而不破坏执行链并使代码更易读。以下示例演示了将两个文件的内容合并为一个。如果其中一个文件不存在,则操作会优雅地失败。
use Widmogrod\Functional as f; use Widmogrod\Monad\Either; function read($file) { return is_file($file) ? Either\Right::of(file_get_contents($file)) : Either\Left::of(sprintf('File "%s" does not exists', $file)); } $concat = f\liftM2( read(__DIR__ . '/e1.php'), read('aaa'), function ($first, $second) { return $first . $second; } ); assert($concat instanceof Either\Left); assert($concat->extract() === 'File "aaa" does not exists');
IO 单子
《IO Monad》的使用示例。从标准输入读取输入,并将其打印到标准输出。
use Widmogrod\Monad\IO as IO; use Widmogrod\Functional as f; // $readFromInput :: Monad a -> IO () $readFromInput = f\mcompose(IO\putStrLn, IO\getLine, IO\putStrLn); $readFromInput(Monad\Identity::of('Enter something and press <enter>'))->run();
Writer 单子
Writer monad
有助于以纯方式记录日志。例如,与 filterM
结合使用,这可以让你确切知道为什么一个元素被过滤。
use Widmogrod\Monad\Writer as W; use Widmogrod\Functional as f; use Widmogrod\Primitive\Stringg as S; $data = [1, 10, 15, 20, 25]; $filter = function($i) { if ($i % 2 == 1) { return W::of(false, S::of("Reject odd number $i.\n")); } else if($i > 15) { return W::of(false, S::of("Reject $i because it is bigger than 15\n")); } return W::of(true); }; list($result, $log) = f\filterM($filter, $data)->runWriter();
Reader 单子
Reader monad
提供了一种在多个函数之间共享公共环境的方法,例如配置信息或类实例。
use Widmogrod\Monad\Reader as R; use Widmogrod\Functional as f; function hello($name) { return "Hello $name!"; } function ask($content) { return R::of(function($name) use($content) { return $content. ($name == 'World' ? '' : ' How are you?'); }); } $r = R\reader('hello') ->bind('ask') ->map('strtoupper'); assert($r->runReader('World') === 'HELLO WORLD!') assert($r->runReader('World') === 'HELLO GILLES! HOW ARE YOU?')
PHP 中的 Free Monad
想象一下,你首先编写业务逻辑,而不关心实现细节,比如
- 如何以及从何获取用户折扣
- 如何在篮子里保存产品
- 等等...
当你的业务逻辑完成后,你就可以专注于这些细节。
Free monad 正是你所需要的,还有更多
- 首先编写业务逻辑
- 编写你自己的 DLS(领域特定语言)
- 将实现与解释解耦。
回声程序
可以在以下位置找到 echo program
的 Free Monad 示例
- 查看源代码 FreeMonadTest.php - 基于 Free 的第二个实现示例,基于 Haskell 实现
BDD 测试的 DSL
示例使用 Free Monad
创建简单的 DSL(领域特定语言)来定义 BDD 类型的框架
- 查看源代码 FreeBddStyleDSLTest.php
$state = [ 'productsCount' => 0, 'products' => [], ]; $scenario = Given('Product in cart', $state) ->When("I add product 'coca-cola'") ->When("I add product 'milk'") ->Then("The number of products is '2'"); $result = $scenario->Run([ "/^I add product '(.*)'/" => function ($state, $productName) { $state['productsCount'] += 1; $state['products'][] = $productName; return $state; }, ], [ "/^The number of products is '(\d+)'/" => function ($state, int $expected) { return $state['productsCount'] === $expected; }, ]);
Free Monad 计算器示例
这是一个使用 FreeMonad 实现的简单计算器 DSL 的示例。
Free monad 可以被解释为真实的计算器或计算格式化器,也就是所谓的“美化打印器”。我还想解决的问题还包括 Free Monad 优化。
考虑到 Free Monad 类似于 AST(抽象语法树),我心中产生了疑问 - 我能否遍历它并更新它以简化计算?如何做?Free Monad 的局限性是什么?计算器示例就是这些问题的结果。
- 查看源代码 FreeCalculatorTest.php
$calc = mul( sum(int(2), int(1)), sum(int(2), int(1)) ); $expected = '((2+1)^2)'; $result = foldFree(compose(interpretPrint, optimizeCalc), $calc, Identity::of); $this->assertEquals( Identity::of(Stringg::of($expected)), $result );
PHP 中的 Haskell do notation
为什么 Haskell 的 do notation 很有趣?
Haskell 中的 do notation 只是一种“语法糖”,在很多方面并不是必需的,但在 PHP 中,monads 的控制流可能难以追踪。
考虑以下示例,它只使用了链式 bind()
,并将其与带有 do notation
的相同版本进行比较。
没有 do notation 的控制流
$result = Identity::of(1) ->bind(function ($a) { return Identity::of(3) ->bind(function ($b) use ($a) { return Identity::of($a + $b) ->bind(function ($c) { return Identity::of($c * $c); }); }); }); $this->assertEquals(Identity::of(16), $result);
带有 do notation 的控制流
$result = doo( let('a', Identity::of(1)), let('b', Identity::of(3)), let('c', in(['a', 'b'], function (int $a, int $b): Identity { return Identity::of($a + $b); })), in(['c'], function (int $c): Identity { return Identity::of($c * $c); }) ); assert($result === Identity::of(16));
每个人都需要自己判断,但在我看来,do notation
提高了 PHP 代码的可读性。
《Functional PHP》一书由 Gilles Crettenand 编著 
在最近出版的《Functional PHP》一书中,你可以了解更多关于 widmogrod/php-functional
的应用,了解它与其他项目的比较,以及如何轻松地将函数式思维应用于日常工作。
参考资料
这里列出了帮助我理解该领域的文章/库的链接
- http://drboolean.gitbooks.io/mostly-adequate-guide
- https://github.com/fantasyland/fantasy-land
- http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
- http://learnyouahaskell.com/functors-applicative-functors-and-monoids
- http://learnyouahaskell.com/starting-out#im-a-list-comprehension
- http://robotlolita.me/2013/12/08/a-monad-in-practicality-first-class-failures.html
- http://robotlolita.me/2014/03/20/a-monad-in-practicality-controlling-time.html
- https://github.com/folktale/data.either
- https://github.com/widmogrod/php-algorithm-w