php-fp / php-fp-either
PHP中Either单子的实现。
Requires (Dev)
- phpunit/phpunit: ^5.3
This package is not auto-updated.
Last update: 2023-06-10 12:06:18 UTC
README
简介
在PHP中抛出异常时,实际上执行了一个 GOTO
:程序执行的位置跳转到适当的异常处理程序,然后继续执行。这很好,但显然意味着你的原始函数有一个副作用:调用它将根本改变程序流程。我们需要的是一个 函数式 的方法来完成同样的事情。
介绍,Either单子。 Either
有两个构造函数,Left
和 Right
。它们与 Maybe
的 Just
和 Nothing
非常相似:map
、ap
和 chain
在 Right
实例上按预期工作,但在 Left
上实际上是无效操作。但是,不同之处在于 Left
与 Nothing
不同,它包含一个值。
这意味着,通常,你的左分支将包含 '异常',而右分支将包含值。如果发生异常,所有后续的计算都将被忽略,异常可以以纯方式处理。例如
<?php use PhpFp\Either\Constructor\{Left, Right}; $login = function ($username, $password) { if ($username != 'foo') { return new Left( 'Invalid username' ); } if ($password != 'bar') { return new Left( 'Incorrect password' ); } return new Right(['hello' => 'world']); } $prop = function ($k) { return function ($xs) use ($k) { return isset ($xs[$k]) ? new Right($xs[$k]) : new Left('No such key.'); }; }; $id = function ($x) { return $x; }; // Some examples... $badUsername = $login('fur', 'bar')->chain($prop('id')); $badPassword = $login('foo', 'bear')->chain($prop('id')); $badKey = $login('foo', 'bar')->chain($prop('brian')); $good = $login('foo', 'bar')->chain($prop('id')); assert($badUsername->either($id, $id) === 'Invalid username'); assert($badPassword->either($id, $id) === 'Incorrect password'); assert($badKey->either($id, $id) === 'No such key.'); assert($good->either($id, $id) === 'world');
如上图所示,失败会传递给计算,并且所有后续的操作(目前唯一例外是下面的 bimap
),必须 由 either
函数处理,以检索内部值。
当然,异常是常见的类比,但 Either
是一个更通用的类型,在具有两个潜在值的计算中很有帮助。如果用户可以通过文件或 stdin
输入怎么办?我们可以使用 Either String File
,使用 File -> String
函数映射实例,然后在知道它们都可接受后提取值。
API
以下类型签名中,构造函数和静态函数被写作纯语言(如Haskell)中会看到的样子。其他包含一个管道,管道前面的类型代表当前 Either 实例的类型,管道后面的类型代表函数。
of :: a -> Either e a
这是 Either 单子的应用构造函数。它返回一个用 Right
实例包裹的给定值。
<?php use PhpFp\Either\Either; $id = function ($x) { return $x; }; assert(Either::of('test')->either($id, $id) == 'test');
left :: a -> Left e a
Left
实例的标准构造函数。
<?php use PhpFp\Either\Either; use PhpFp\Either\Constructor\Left; $either = Either::left('test'); assert($either instanceof Either); assert($either instanceof Left);
right :: a -> Right e a
Right
实例的标准构造函数。通常应调用 Either::of
。
<?php use PhpFp\Either\Either; use PhpFp\Either\Constructor\Right; $either = Either::right('test'); assert($either instanceof Either); assert($either instanceof Right);
tryCatch :: (-> a) -> Either e a
有时,你将有一段抛出异常的代码,你希望将其包裹在一个 Either
中,这个函数可以帮到你。如果发生异常,它将被包裹并返回一个 Left
。否则,返回的值将被包裹在一个 Right
中。
<?php use PhpFp\Either\Either; $id = function ($x) { return $x; }; $f = function () { throw new \Exception; }; $g = function () { return 'hello'; }; assert(Either::tryCatch($f)->either($id, $id) instanceof \Exception); assert(Either::tryCatch($g)->either($id, $id) === 'hello');
ap :: Either e (a -> b) | Either e a -> Either e b
将一个包裹在Either中的参数应用于一个包裹在Either中的函数,其中Left
函数将作为恒等函数。
<?php use PhpFp\Either\Constructor\{Left, Right}; $id = function ($x) { return $x; }; $addTwo = Either::of( function ($x) { return $x + 2; } ); $a = new Right(5); $b = new Left(4); assert($addTwo->ap($a)->either($id , $id) === 7); assert($addTwo->ap($b)->either($id, $id) === 4);
bimap :: Either e a | (e -> f) -> (a -> b) -> Either f b
有时,定义在Left
值上执行的运算可能很有用,这就是实现它的方法。对于此函数,您提供左右变换,然后使用适当的变换。
<?php use PhpFp\Either\Constructor\{Left, Right}; $addOne = function ($x) { return $x + 1; }; $subOne = function ($x) { return $x - 1; }; $id = function ($x) { return $x; }; assert (Either::right(2)->bimap($addOne, $subOne)->either($id, $id) === 1); assert (Either::left(2)->bimap($addOne, $subOne)->either($id, $id) === 3);
chain :: Either e a | (a -> Either f b) -> Either f b
标准的单调绑定函数(Haskell的>>=
)。这是用于使用返回Either值的函数进行映射:而不是使用map
并得到Either e (Either e a)
,您得到Either e a
,并且两个级别被“扁平化”。介绍部分有一个很好的例子,但这里有一个更小的例子
<?php use PhpFp\Either\Constructor\{Left, Right}; $f = function ($x) { return Either::of($x * 2); } $id = function ($x) { return $x; }; assert(Either::right(8)->chain($f)->either($id, $id) === 16); assert(Either::left(8)->chain($f)->either($id, $id) === 8);
map :: Either e a | (a -> b) -> Either e b
这是标准的函子映射,它转换内部值。与其他Either
操作一样,请记住,这不会对Left
值产生影响,只有bimap
可以转换它
<?php use PhpFp\Either\Constructor\{Left, Right}; $f = function ($x) { return $x - 5; }; $id = function ($x) { return $x; }; assert(Either::right(8)->map($f)->either($id, $id) === 3); assert(Either::left(8)->map($f)->either($id, $id) === 8);
either :: Either e a | (e -> b) -> (a -> b) -> b
这是应该用来从Either
单子中获取值的函数。严格来说,如果您表现良好并且注意类型,两个提供的函数,尽管可以接受不同类型的Left
和Right
输入,应该返回相同类型的值
<?php use PhpFp\Either\Constructor\{Left, Right}; $left = function ($x) { return (int) $x; }; $right = function ($x) { $x; }; assert(Either::left('7')->either($left, $right) === 7); assert(Either::right(2)->either($left, $right) === 2);
贡献
与其他类似,我了解到至少有几种类型类可以添加到这个实现中,所以如果您希望看到其他类型类被包含,请随时提交问题或PR。
然而,更紧迫的问题是文档:如果某些内容不够清晰,请留下问题或提交建议的修复,以便尽可能清晰和详细地描述!