chippyash / monad
函数式编程 Monad 支持
Requires
- php: >=8.0
Requires (Dev)
- mikey179/vfsstream: 1.6.*
- phpunit/phpunit: ~9.5
README
质量保证
上述徽章代表当前的开发分支。一般来说,我不会在 GitHub 上提交代码,除非测试、覆盖率和使用性是可接受的。在假期、需要为其他下游项目编写代码等短期时间内,这可能不成立。如果您需要稳定的代码,请使用标记版本。请阅读“进一步文档”和“安装”。
测试合同 在文档目录中。
该库在 2.0.0 版本中撤回了 PHP5.4 & 5.5 的开发者支持。如果您需要 PHP 5.4 或 5.5 的支持,请使用版本 >=1,<2
该库在 3.0.0 版本中撤回了 PHP <8 的开发者支持。如果您需要 PHP 7.x 的支持,请使用版本 >=2,<3
是什么?
提供 Monadic 类型
根据我的导师的说法,Monads 要么难以解释,要么难以编码,即你可以说出 how
或 what
,但不能同时说。如果您需要进一步说明,请从 维基百科 开始
支持的类型
- Monadic 接口
- 抽象 Monad
- Identity Monad
- Option Monad
- Some
- None
- FTry Monad
- Success
- Failure
- FMatch Monad
- Collection Monad
- Map Monad
- Set Monad
为什么?
PHP 正在受到来自 Scala 等函数式混合语言越来越多的攻击。区别在于 函数式编程
的热点。PHP 可以支持这种模式,而这个库引入了一些基本的 monadic 类型。实际上,学习函数式编程实践可以使 PHP 中的解决方案更加健壮。
Monadic 类型的大部分功能都来自于函数式 FMatch、Try 和 For Comprehension 语言结构的用法。PHP 没有这些。这个库提供了
- FMatch
- FTry
- FFor 这是由 Assembly-Builder 包 提供的
函数式编程的关键在于使用严格类型并在语言语法中提升函数为第一类公民。PHP5.4+ 允许函数作为有类型的参数(Closure)使用。而且,似乎 PHP 开发者正在接受严格或硬类型,正如我的 strong-type 库 所证明的那样。
如何?
Monadic 接口
一个 Monad 有三个要素(根据我对它的理解)
- a value (which may be no value at all, a simple type, an object or a function)
- 获取其值的方法,通常称为 return()
- 将值绑定(或使用)到某个函数的方法,通常称为 bind(),其返回值是另一个 Monad,通常但不仅限于与捐赠者 Monad 相同的类型。 (很少,它可能是另一个类类型。)
此处提供的 Monadic 接口定义了
- bind(\Closure $function, array $args = []):Monadic
- 函数签名应至少包含一个参数,用于接收 Monad 的值。例如:
function($val){return $val * 2;}
如果你在 $args 数组中使用其他参数,需要将它们添加到参数列表中,例如:
- 函数签名应至少包含一个参数,用于接收 Monad 的值。例如:
$ret = $m->bind(function($val, $mult){return $val * $mult;}, [4]);
请注意,您可以在定义函数时像通常一样使用 use
子句来公开外部参数值。警告:开始使用纯异步 PHP 编程后,您就不能使用 use
子句了。已经被警告了!
- value():mixed - 作为
return
是 PHP 中的一个保留字,所以使用了return
方法
此外,还定义了两个辅助方法用于该接口
- flatten():mixed - 将 monadic 值
flattened
为 PHP 原生类型或非 monadic 对象 - static create(mixed $value):Monadic 一个用于创建具体派生 Monad 实例的工厂方法
Monad 有一个不可变值,也就是说,bind() 方法的返回结果是另一个(monadic)类。原始值保持不变。
Monad 抽象类
包含 Monad 值持有者和一个 syntactic sugar
助手魔法 __invoke() 方法,如果未提供参数则代理到 value(),如果提供了闭包(带有/不带可选参数)则代理到 bind()。
Monadic 接口或抽象 Monad 类没有定义如何设置具体 Monad 的值。通常在构造时设置 Monad 的值是有意义的。因此,在大多数情况下,您会使用某种形式的构造函数创建具体的 Monad 类。
提供具体的 Monad 类
Identity
最简单的 Monad 类型
use Monad\Identity; $id = new Identity('foo'); //or $id = Identity::create('foo'); $fConcat = function($value, $fudge){return $value . $fudge}; $concat = $id->bind($fConcat, ['bar']) ->bind($fConcat, ['baz']); echo $concat->value(); //'foobarbaz' echo $id->value(); //'foo'
Option
Option 是一种多态的 Maybe Monad
,它可以存在于两种状态之一
- Some - 有值的 Option
- None - 无值的 Option
由于 PHP 没有语言构造来通过构造创建多态对象,您需要使用 Option::create() 静态方法。但是,您可以将 Option 用作其他类方法和返回值的类型提示
use Monad\Option; use Monad\Option\Some; use Monad\Option\None; /** * @param Option $opt * @return Option */ function doSomethingWithAnOption(Option $opt) { if ($opt instanceof None) { return $opt; } //must be a Some return $opt(doMyOtherThing()); //use magic invoke to bind } $someOption = Option::create('foo'); $noneOption = Option::create(); $one = doSomethingWithAnOption($someOption); $two = doSomethingWithAnOption($noneOption);
在正常情况下,Option 使用 null
值来确定是否创建 Some 或 None;也就是说,传递给 create() 的值将针对 null
进行测试。如果它 === null,则创建 None,否则创建 Some。您可以向 create() 提供一个替代测试值作为第二个参数
$mySome = Option::create(true, false); $myNone = Option::create(false, false);
一旦成为 None,就永远是 None。无论绑定多少次,都不会返回除 None 以外的任何内容。
另一方面,Some 可以通过绑定变为 None(或者说 bind() 的结果,因为当然原始 Some 保持不可变)。为了帮助做到这一点,Some->bind() 可以接受一个可选的第三个参数,该参数是要测试 None 的值(即 Option::create() 的可选第二个参数)
您还应该注意,对 None 调用 ->value() 将生成 RuntimeException,因为当然 None 没有值!
支持的其他方法
- getOrElse(mixed:elseValue) 如果 Option 是 Some,则返回 Option->value(),否则返回 elseValue
FTry
FTry 是一种多态的 Try Monad
,它可以存在于两种状态之一
- Success - 有值的 FTry
- Failure - 有 PHP 异常值的 FTry
Try
是 PHP 中的一个保留字,因此我将其称为 FTry,表示 Functional Try
。
由于 PHP 没有语言构造来通过构造创建多态对象,您需要使用 FTry::with()(或 FTry::create())静态方法。但是,您可以将 FTry 用作其他类方法和返回值的类型提示
FTry::on(value) 将捕获处理 value 时发生的任何异常,并相应地返回 Success 或 Failure 类。这使得它非常适合将 PHP 事务包装在 Try - Catch 块中的简单情况
use Monad\FTry; use Monad\FMatch; FMatch::on(FTry::with($myFunction($initialValue()))) ->Monad_FTry_Success(function ($v) {doSomethingGood($v);}) ->Monad_FTry_Failure( function (\Exception $e) { echo "Exception: " . $e->getMessage(); } );
这是一个相当简单的示例,你可能会质疑它的价值,因为它完全可以像常规PHP一样轻松地编写。但是:成功或失败仍然是一种Monad,因此你仍然可以将其绑定(映射)到结果类,将其扁平化等。
类似于Option,FTry也支持getOrElse(mixed:elseValue)
方法,允许实现默认行为
echo FTry::with(myComplexPrintableTransaction()) ->getOrElse('Sorry - that failed');
为了完整性,FTry也支持isSuccess()
echo 'The colour is' . FTry::with(myTest())->isSuccess() ? 'blue' : 'red';
一旦失败,就永远失败。然而,成功可以在绑定后产生成功或失败的结果。
如果你真的想抛出失败中包含的异常,请使用pass()
方法
$try = FTry::with($myFunction()); if (!$try->isSuccess()) $try->pass();
FMatch
FMatch Monad允许你执行类型模式匹配,以创建强大的动态函数式case语句
的等价物。
由于PHP8中'Match'是保留字,因此对于本库的V3版本,我将Match重命名为FMatch。
基本语法是
use Monad\FMatch; $result = FMatch::on($initialValue) ->test() ->test() ->value();
其中test()可以是原生PHP类型或类的名称,例如
$result = FMatch::on($initialValue) ->string() ->Monad_Option() ->Monad_Identity() ->value()
您可以使用FMatch::any()方法来捕获所有特定匹配器未匹配到的内容
$result = FMatch::on($initialValue) ->string() ->int() ->any() ->value();
您可以为每个测试提供一个具体值作为参数,或一个函数。例如
$result = FMatch::on($initialValue) ->string('foo') ->Monad_Option( function ($v) { return FMatch::on($v) ->Monad_Option_Some(function ($v) { return $v->value(); }) ->Monad_Option_None(function () { throw new \Exception(); }) ->value(); } ) ->Monad_Identity( function ($v) { return $v->value() . 'bar'; } ) ->any(function(){return 'any';}) ->value();
您可以在FMatchTest::testYouCanNestFMatches()中找到这个测试
支持的本地类型匹配
- 字符串
- 整数|int|long
- 浮点数|double|real
- null
- 数组
- 布尔值|boolean
- 可调用|function|closure
- 文件
- 目录|directory
- 对象
- 标量
- 数值
- 资源
支持类匹配
使用类的完全命名空间名称进行匹配,将反斜杠\替换为下划线,例如,要测试Monad\Option
,请使用Monad_Option
集合
Monad集合提供了一个作为Monad行为的结构化数组。它基于SPL ArrayObject。
然而,非常重要的一点是,与PHP数组不同,集合是类型特定的,即您需要指定集合类型,或者默认为构造数组的第一个成员。
另一个“陷阱”:由于集合是一个对象,调用Collection->value()将仅返回集合本身。如果您想从集合中获取PHP数组,请使用toArray()
,它代理底层的getArrayCopy()
,并且大多数PHP开发者熟悉toArray
作为缺失的“魔术”调用。
为什么要重新发明轮子?ArrayObject(集合的基础)的行为与普通数组略有不同。一是它是一个对象,因此可以按引用传递,二是由于一,它(希望TBC)停止了多线程环境中的segfault。即使二不起作用,那么一仍然有效。
use Monad\Collection; $c = Collection::create([1,2,3,4]); //or $c = Collection::create([1,2,3,4], 'integer'); //to create an empty collection, you must specify type $c = Collection::create([], 'integer'); $c = Collection::create([], 'Monad\Option');
您可以通过get和test获取集合
$c = Collection::create([1,2,3,4]); $v = $c[2] // == 3 if (!isset($c[6]) { ... }
尽管集合实现了ArrayAccess接口,但尝试设置或删除值$mCollection[0] = 'foo'
或unset($mCollection[0])
将抛出异常,因为集合默认是不可变的。在某些情况下,您可能想更改这一点。使用MutableCollection允许可变性。
通常,这不是真正的问题,因为您可以在集合上调用bind()或each()以返回另一个集合(该集合可以包含不同类型的数据)。
我在可能的地方都用FMatch语句表达了集合的实现,这不仅意味着代码更紧凑,而且可以作为您可以查看(并希望批评!)的示例。
您可以向集合追加内容,并返回一个新的集合
$s1 = new Collection([1,2,3]); $s2 = $s1->append(4); //or $s2 = $s1->append(['foo'=>4]);
您可以获得两个集合的差集
$s1 = Collection::create([1, 2, 3, 6, 7]); $s2 = Collection::create([6,7]); $s3 = $s1->vDiff($s2); //difference on values $s4 = $s1->kDiff($s2); //difference on keys
并且交集
$s1 = Collection::create([1, 2, 3, 6, 7]); $s2 = Collection::create([6,7]); $s3 = $s1->vIntersect($s2); //intersect on values $s4 = $s1->kIntersect($s2); //intersect on keys
uDiff
、kDiff
、vIntersect
和kIntersect
可以接受第二个可选的Closure参数,用作比较方法。
您可以通过值或键获取两个集合的并集
$s1 = Collection::create([1, 2, 3, 6, 7]); $s2 = Collection::create([3, 6, 7, 8]); $valueUnion = $s1->vUnion($s2); $keyUnion = $s1->kUnion($s2);
您可以获得集合的头部和尾部
$s1 = Collection::create([1, 2, 3, 6, 7]); echo $s1->head()[0] // 1 echo $s1->tail()[0] // 2 echo $s1->tail()[3] // 7
对于集合有四种函数映射方法
-
标准的 Monadic bind(),其函数将集合的整个
value 数组
作为其参数。您应该返回一个数组作为函数的结果,但如果您不这样做,它将被强制转换为集合。 -
each() 方法。与 bind() 类似,它接受一个函数和一个可选的额外参数值数组。然而,each 函数会对集合中的每个成员调用。函数的结果将收集到一个新的集合中并返回。这种方式与 PHP 的原生 array_map 很相似。
-
reduce() 方法。与 array_reduce 类似,它返回一个由作为参数传入的函数计算出的单个值。
-
filter() 方法。与 array_filter 类似,但它返回一个由缩减操作生成的新集合。
请注意,您可以通过这些映射方法()改变结果集合的基本类型。
我选择 Collection 作为名称,因为它不会与 PHP 的保留名称 list
冲突。本质上,Collection 将在所有实际目的上都是一个列表,但对于坚持使用 PHP 的用户来说,它仍然表现得像一个数组。
次要的设计考虑因素是,您应该能够在不了解它是 Monad 的情况下使用 Collection,除了它是类型特定的。
映射
映射是集合的一个简单扩展,它要求其条目具有字符串(哈希)键。它遵守集合的所有规则,除了以下规则
use Monad/Map; $m1 = new Map(['foo']);
将不起作用,但
$m1 = new Map(['foo'=>'bar']);
将起作用。您通常可以指定类型作为第二个参数。对于 vUnion
、vIntersect
和 vDiff
方法,在映射中未指定,将抛出 BadMethodCallException
。
集合
集合是集合的一个简单扩展,它强制执行以下规则
- 集合只能有唯一值(相同类型的)
- 集合不关心键,重要的是值
- 集合上的操作返回一个集合
use Monad/Set; $setA = new Set(['a','b','c']); $setB = new Set(['a','c']); $setC = $setA->vIntersect($setB); $setD = $setA->vUnion($setB); $setE = $setA->vDiff($setB);
与集合一样,您可以为空构造值和第二个类型值指定。您还可以向集合追加以返回一个新集合。
对于映射,未指定 kUnion
、kIntersect
和 kDiff
方法,将抛出 BadMethodCallException
。
所有其他集合方法都受支持,在期望的地方返回集合。
->vIntersect()、->vUnion() 和 ->diff() 方法都接受一个第二个相等性函数参数,如集合。然而,对于集合,如果没有提供,它将默认使用合理的默认值,即将不可序列化的值转换为值的序列化哈希,并使用该哈希进行比较。如果此默认值不足以满足您的需求,请提供自己的函数。
其他文档
请注意,您在 Github 上看到的此文档显示的内容始终是最新版本的 dev-master。它描述的功能可能尚未发布。请检查您所编写的版本的文档,或下载。
测试合同 在文档目录中。
查看 ZF4 包 了解更多包
UML
更改库
- 分叉它
- 编写测试
- 修改它
- 发起一个拉取请求
发现了一个您无法解决的错误吗?
- 分叉它
- 编写测试
- 发起一个拉取请求
注意。在您的拉取请求之前,请确保您已重新基准到 HEAD
或者 - 提出一个问题票据。
在哪里?
库托管在 Github 上。它在 Packagist.org 上可用
安装
安装 Composer
对于生产
"chippyash/monad": "~3.0"
或者使用最新版本,可能是不稳定版本
"chippyash/monad": "dev-master"
对于开发
克隆此仓库,然后在本地仓库根目录中运行Composer以拉取依赖项
git clone git@github.com:chippyash/Monad.git Monad cd Monad composer install
运行测试
cd Monad vendor/bin/phpunit -c test/phpunit.xml test/
调试
由于PHP在核心层面并不真正支持函数式编程,使用XDebug等工具进行调试会变得非常复杂。以下是一些有帮助的做法
-
至少在初始阶段将测试隔离。如果遇到问题,创建一个只做一件事情的测试——就是你试图调试的事情。以此为起点。
-
注意value()和flatten(),前者获取直接的Monad值,后者提供PHP的基本类型。
-
在构造FMatches时,确保FMatch中包含的值符合你期望的类型。记住,FMatch返回一个包含值的FMatch。是的,我自己也在这方面犯了错误。
-
继续运行其他测试。这看起来很简单,但在追求单一目标的匆忙中,很容易忘记库是相互依赖的(随着我们能够将新功能包装回原始代码,这种依赖性将越来越强。例如,Collection依赖于FMatch:当FFor实现时,FMatch将发生变化。)定期运行整个测试套件。这样,你可以捕捉到任何破坏上游功能的问题。当这个库尽可能多地以自身为基准表达自己时,它将是完整的!
-
现有的测试都有其存在的理由:如果你认为它们方向错误、思路不清晰等,请提出问题
许可证
此软件库根据BSD 3条款许可证发布
此软件库版权所有(c)2015,2021 Ashley Kitson,英国
此软件库包含来自其他作品的代码项
据我所知,包含的代码项没有违反覆盖的许可证,反之亦然。如果有任何疑问,请寻求适当的建议。
如果衍生代码项的原始版权所有者反对这种包含,请与作者联系。
谢谢
这不是我自己做的。我深深感激那些在我之前走过这条路的人。
以下人员为此库的工作做出了贡献
历史
V1.0.0 初次发布
V1.1.0 添加FTry
V1.2.0 添加Collection
V1.2.1 对Collection进行修复
V1.2.2 为vUnion方法添加排序顺序
V1.2.3 允许后代单子类型
V1.2.4 向Collection添加each()方法
V1.2.5 从coveralls迁移到codeclimate
V1.2.6 添加链接到包
V1.2.7 代码清理 - 验证PHP7兼容性
V1.3.0 Collection是不可变的。为方便起见,添加了MutableCollection
V1.4.0 添加Map类 - 强制集合成员的键为字符串类型
Add convenience method append() to Collection === ->vUnion(new Collection([$nValue]))
V1.5.0 添加Set类
V1.5.1 向Map和Set添加额外检查。diff()和intersect()已弃用,使用kDiff()、uDiff、kIntersect()和uIntersect()方法;
V1.5.2 构建脚本更新
V1.5.3 更新composer - 由packagist composer.json格式更改强制
V2.0.0 BC中断。放弃对PHP <5.6的支持
V2.0.1 对PHP >= 7.1的修复
V2.1.0 许可证从GPL V3更改为BSD 3条款
V2.1.1 在FTry的bind方法中展开值,以便在绑定的函数返回Success时,我们不会得到嵌套的Success。由josselinauguste提供的PR
V3.0.0 BC中断。放弃对PHP <8的支持。Match重命名为FMatch。