widmogrod/php-functional

函子、应用函子和单子是非常吸引人的概念。这个库的目的是在面向对象的PHP世界中探索这些概念。

6.0.1 2024-03-25 10:44 UTC

README

Build Status Test Coverage Maintainability

简介

函数式编程是一个迷人的概念。这个库的目的是在面向对象的PHP中探索 函子应用函子单子,并提供实际用例的示例。

项目中可用的单子类型

  • 状态单子
  • IO 单子
  • 自由单子
  • Either 单子
  • Maybe 单子
  • Reader 单子
  • Writer 单子

在探索函数式编程空间时,我发现使用PHP的基本值非常困难,并且会复杂化许多函数式结构的实现。为了简化这种体验,该库引入了一系列高阶原始值。

  • Num
  • Sum
  • Product
  • Stringg
  • Listt(也称为列表单子,因为 list 是PHP中的受保护关键字)

应用

此项目的已知应用

  • Algorithm W 基于 Martin Grabmüller 的工作在PHP中实现。

安装

composer require widmogrod/php-functional

开发

此存储库遵循 语义版本控制概念。如果您想做出贡献,请按照 CONTRIBUTING.md 操作。

测试

质量保证由以下提供

  • PHPUnit
  • Eris - QuickCheck 和基于属性的测试工具到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 Monoid

使用 Maybe 作为 Monoid 的实例简化了 concat 和 reduce 操作,通过使用 Maybe 对可能缺失的值的抽象。参见 示例,从姓氏、中间名和姓氏构建一个人的全名,而不必显式检查每个部分是否存在。

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 语句。然而,Monad 展示了我们可以如何优雅地失败,而不会打断执行链,使代码更加易读。以下示例演示了将两个文件的内容合并为一个文件。如果其中一个文件不存在,操作将优雅地失败。

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。从 stdin 读取输入,并将其打印到 stdout

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 示例

BDD 测试的 DSL

示例使用 Free Monad 创建简单 DSL (领域特定语言) 以定义 BDD 类型的框架

$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 的局限性是什么?计算器示例就是这些问题的答案。

$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
);

Haskell 的 do notation 在 PHP 中的应用

为什么 Haskell 的 do notation 很有趣?

在 Haskell 中,它只是“语法糖”,在很多方面并不需要,但在 PHP 中,monads 的控制流程可能难以跟踪。

考虑以下示例,它仅使用链式 bind(),并将其与使用 do notation 的相同版本的 PHP 进行比较。

没有 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 by Gilles Crettenand 中,你可以了解 widmogrod/php-functional 的更多应用,看到它与其他项目的比较,以及如何轻松地将函数式思维应用到日常工作中。

在 PacktPub 购买此书

参考资料

以下是帮助我理解该领域的文章/库的链接