Giter Club home page Giter Club logo

functional's Introduction

fp4php

Functional programming utilities for PHP.

psalm level psalm type coverage phpunit coverage

This library extensively uses curried functions and the pipe combinator.

If you don't like the following code:

self::someMethod(
    array_filter(
        array_map(
            fn ($i) => $i + 1,
            [1, 2, 3],
        ),
        fn ($i) => $i % 2 === 0,
    );
);

Or:

self::someMethod(
    ArrayList::fromIterable([/*...*/]),
        ->map(fn ($i) => $i + 1),
        ->filter(fn ($i) => $i % 2 === 0),
);

Then this library might interest you:

<?php

declare(strict_types=1);

use Fp4\PHP\ArrayList as l;

use function Fp4\PHP\Combinator\pipe;

final class App
{
    /**
     * @param list<int> $list
     * @return list<string>
     */
    public function run(array $list): array
    {
        return pipe(
            $list,
            l\map(fn($i) => $i + 1),
            l\filter(fn($i) => 0 === $i % 2),
            self::toString(...),
        );
    }

    /**
     * @param list<int> $list
     * @return list<string>
     */
    public static function toString(array $list): array
    {
        return pipe(
            $list,
            l\map(fn($i) => (string)$i),
        );
    }
}

To learn more about this library, read the following:

functional's People

Contributors

github-actions[bot] avatar innot20 avatar klimick avatar mavlitov98 avatar nzour avatar whsv26 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

functional's Issues

Add Either::mapLeft

Currently there is no way to change left side of the Either.

Syntactic example where it can be useful:

/**
 * @return Either<ApplicationError, OrderOutput>
 */
function createOrder(OrderInput $input, OrderService $service): Either
{
    return $service->create($input)
        ->map(fn(Order $o) => OrderOutput::from($o))
        ->mapLeft(fn(OrderError $e) => ApplicationError::from($e));
        // map concrete domain error to more generic error
}

it is unnecessary.

it is unnecessary.
non-empty-array already include non-empty-list

Return type should be:

@psalm-return (
    $collection is non-empty-array
        ? non-empty-array<TGroupKey, array<TK, TV>>
        : array<TGroupKey, array<TK, TV>>
)

Originally posted by @klimick in #22 (comment)

Option::do implementation incorrect

This snippet type checks by psalm, but at runtime it fails:

/**
 * @return Option<stdClass>
 */
function toSomething(mixed $_): Option
{
    return Option::none();
}

$items = [];

Option::do(function() use ($items) {
    $mapped = [];

    /** @psalm-suppress MixedAssignment */
    foreach ($items as $item) {
        $mapped[] = yield toSomething($item);
    }

    return $mapped;
});
PHP Fatal error:  Uncaught Error: Call to a member function isEmpty() on null in /home/app/vendor/whsv26/functional/src/Fp/Functional/Option/Option.php:82
Stack trace:
#0 /home/app/run.php(26): Fp\Functional\Option\Option::do()
#1 {main}
  thrown in /home/app/vendor/whsv26/functional/src/Fp/Functional/Option/Option.php on line 82

Process finished with exit code 255

This only happens if no yield has been called in Option::do.

Fp\Json\jsonDecode is unsafe

If you run the following piece of code, it will lead to a runtime error:

<?php

use Fp\Functional\Either\Right;
use Fp\Functional\Either\Left;
use function Fp\Json\jsonDecode;

require __DIR__ . '/vendor/autoload.php';

$decoded = jsonDecode('"kek"')
    ->map(fn(array $val) => array_merge($val, ['lol' => '_']));
PHP Fatal error:  Uncaught TypeError: {closure}(): Argument #1 ($val) must be of type array, string given, called in /home/php/whsv26/functional/src/Fp/Functional/Either/Either.php on line 83 and defined in /home/php/whsv26/functional/run.php:12
Stack trace:
#0 /home/php/whsv26/functional/src/Fp/Functional/Either/Either.php(83): {closure}()
#1 /home/php/whsv26/functional/run.php(15): Fp\Functional\Either\Either->flatMap()
#2 {main}
  thrown in /home/php/whsv26/functional/run.php on line 12

Because json_decode can deserialize literals like numbers, strings, booleans.

Do we need function 'but_last' ?

Been thinking about function but_last, like this

I am not sure if this function is necessary.
Also we just may code something like this:

$withoutLast = \Fp\Collection\reverse(
    \Fp\Collection\tail(
        \Fp\Collection\reverse($collection)
    )
);

// or simplified version

$withoutLast = reverse(tail(reverse($collection)));

Maybe native function but_last would be more understandable and less resource consuming?
I would be pleasured to hear your opinion.

Any cases to add @psalm-pure for really pure functions?

In some cases it may be useful to use few functions inside pure context.

Example:

/**
 * @psalm-immutable
 */
class Foo
{
    /** 
     * @var list<Bar>
     */
    public array $bars;

    public function __construct(Bar $first, Bar ...$rest)
    {
        // Impure method call issue occurred here (because Foo is immutable, so requires pure context)
        $this->bars = asList([$first], $rest);
    }
}

Of course, there are more ways to compile list, but if we have asList why wouldn't we use it whenever we want?

I was just thinking about function asList, maybe there are few others function that are actually pure but not annotated yet

ArrayList::prependedAll infer ArrayList<T|mixed> instead ArrayList<T>

/**
 * @param ArrayList<int> $numbers
 */
function test(ArrayList $numbers): void {}

/**
 * @return ArrayList<int>
 */
function getNumbers(): ArrayList
{
    return ArrayList::empty();
}

/**
ERROR: MixedArgumentTypeCoercion
    at /app/run.php:34:5
    Argument 1 of test expects Fp\Collections\ArrayList<int>, parent type Fp\Collections\ArrayList<int|mixed> provided (see https://psalm.dev/194)
    getNumbers()->prepended(getNumbers())
*/
test(
    getNumbers()->prependedAll(getNumbers())
);

Assertion memoization for `proveTrue` inside `Option::do`

It would be nice if proveTrue can memoize assertions:

/**
 * @return Option<positive-int>
 */
function provePositiveInt(?int $number): Option
{
     return Option::do(function() use ($number) {
         yield proveTrue(null !== $number);
         yield proveTrue($number > 0);

         // for now it will be type error, because assertions from above will be ignored
         return $number;
     });
}

Incorrect `Left` and `Right` class definition

Current definitions for Right and Left is incorrect:

/**
 * @template-covariant L
 * @template-covariant R
 * @psalm-immutable
 * @extends Either<L, R>
 */
final class Right extends Either { ... }

/**
 * @template-covariant L
 * @template-covariant R
 * @psalm-immutable
 * @extends Either<L, R>
 */
final class Left extends Either { ... }

Should be:

/**
 * @template-covariant R
 * @psalm-immutable
 * @extends Either<empty, R>
 */
final class Right extends Either { ... }

/**
 * @template-covariant L
 * @psalm-immutable
 * @extends Either<L, empty>
 */
final class Left extends Either { ... }

Change return types of `Either::right` and `Either::left`

Like in #15.
Either::left and Either::right currently returns Left<LI, empty> and Right<empty, RI>.

For this snippet psalm infers weird type for the $result Either<string, Left<"less than 10", empty> | float | int>
But if we change return types of Either::right and Either::left inferred type will be correct: Either<string, int>

/**
 * @return Either<string, int>
 */
function getNum(): Either
{
    return Either::right(10);
}

/** @psalm-trace $result */
$result = Either::do(function() {
    $num1 = yield getNum();
    $num2 = yield Either::right(20);

    if ($num1 < 10) {
        return yield Either::left('less than 10');
    }

    return $num1 + $num2;
});

Streams

image

  • Make Stream::range general

Q: why does Option::get() may return null?

Just noticed that Option::get() may return null value.
Are there any specific reasons for this behavior?

I mean, there are other implementations of Option and non of them do return null value.

Java

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }

    return value;
}

Scala

def get: Nothing = throw new NoSuchElementException("None.get")

Rust

pub fn unwrap(self) -> T {
    match self {
        Some(val) => val,
        None => panic!("called `Option::unwrap()` on a `None` value"),
    }
}

Maybe there is a contract for Option to have that kind of behavior?

Add method match on Option and Either?

Greetings, sir.
Just got a few thoughts about implementing method match on Option and Either.

Like C# functional lib has

This would look like

Option:

$hello = Option::of('Hello');

$result = $hello->match(
    some: fn(string $v) => "{$v} world",
    none: fn() => 'Hello from none'
);

Either:

/** @var Either<string, FooInterface> $found */
$found = Some::of(new FooImpl());

$found->match(
    right: \Fp\id,
    left: function(string $errorMessage) {
        // sorry for side effect here
        $this->logger->logWarning($errorMessage);

        return new BarImpl();
    },
);

So the method signature would look like:

Option:

/**
 * @template TR
 * @param callable(A): TR $some
 * @param callable(): TR $none
 * @return TR
 */
public function match(callable $some, callable $none): mixed
{

Either:

/**
 * @template TR
 * @param callable(R): TR $right
 * @param callable(L): TR $left
 * @return TR
 */
public function match(callable $right, callable $left): mixed
{

I think this may be useful.
What do you think?

Allow `null` values for `Option`

/**
 * Returns value from $array by $path if present
 *
 * @param non-empty-list<string> $path
 * @psalm-pure
 */
function dotAccess(array $path, array $array): Option
{
    $key = $path[0];
    $rest = array_slice($path, offset: 1);

    if (empty($rest) && array_key_exists($key, $array)) {
        return Option::of($array[$key]);
    }

    if (array_key_exists($key, $array) && is_array($array[$key])) {
        return dotAccess($rest, $array[$key]);
    }

    return new None();
}

$arr = [
    'person' => [
        'name' => 'Jhon',
        'address' => null,
    ],
];

// Prints Some('Jhon') - good
print_r(dotAccess(['person', 'name'], $arr));

// Prints None - not good
print_r(dotAccess(['person', 'address'], $arr));

Partition function type inference issues

/**
 * @return list<array{mixed, Either<string, int>}>
 */
function getValues() { ... }
$values = getValues();

$res = partition($values, static function (array $v) {
    [, $either] = $v; // type of $v is lost somewhere, it actually is array<TKey: array-key, TValue: mixed>
  
    return $either instanceof Right;
});

// but for $res variable - type inference would work correctly

Add type refinements for `Fp\Collection\filter`

use function Fp\Collection\filter;

/**
 * @psalm-return array<string, array>
 */
function getCollection(): array { return []; }

/**
 * @psalm-type Shape = array{name: string, postcode: int}
 * @psalm-assert-if-true Shape $shape
 */
function isValidShape(array $shape): bool
{
    return array_key_exists("name", $shape) &&
        array_key_exists("postcode", $shape) &&
        is_int($shape["postcode"]);
}

/** @psalm-trace $result */
$result = filter(
    getCollection(),
    fn(array $v) => isValidShape($v)
);

$result will be refined from array<string, array> to array<string, array{name: string, postcode: int}>

Change return types of `Option::some` and `Option::none`

Currently Option::some returns Some<A> and Option::none returns None.

/**
 * @return Option<int>
 */
function getNum(): Option
{
    return Option::some(10);
}

/** @psalm-trace $result */
$result = Option::do(function() {
    $num = yield getNum();

    if ($num < 10) {
        return yield Option::none();
    }

    return $num;
});

For the snippet above psalm infers a Option<int> | None for the $result

But if would the Option::none returned a Option<empty> then psalm inferred right type a Option<int> for the $result.

Help needed with PHP bug workaround

Need to find workaround for this and this PHP bug,

It's important for trampolining, linked list and other deep nested objects.

Currently there is segfault in garbage collector for deep nested object destruction.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.