vjsingla/php-functional

函子、应用函子和单子是迷人的概念。这个库的目的是在面向对象PHP世界中探索它们。

1.0.0 2024-07-10 13:30 UTC

This package is not auto-updated.

Last update: 2024-09-19 12:42:02 UTC


README

Build Status Test Coverage Maintainability

简介

函数式编程是一个迷人的概念。这个库的目的是在面向对象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 示例

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

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 的应用,了解它与其他项目的比较,以及如何轻松地将函数式思维应用于日常工作。

在 PacktPub 购买此书

参考资料

这里列出了帮助我理解该领域的文章/库的链接