Stars: 107
Forks: 8
Pull Requests: 20
Issues: 25
Watchers: 4
Last Updated: 2023-08-18 08:02:24
A collection of type-safe functional data structures
License: Other
Languages: Shell, PHP, Nix
A collection of type-safe functional data structures
The aim of this library is to provide a collection of functional data structures in the most type safe way currently possible within the PHP ecosystem, still providing a generic and consistent API.
The two ideas which differentiate this from other functional libraries in PHP are:
composer require marcosh/lamphpda
We use Psalm as a type checker. It basically works as a compilation step, ensuring that all the types are aligned.
To benefit from this library, it is compulsory that your code runs through a Psalm check.
This library includes a flake.nix
file, enabling you to access a development
environment equipped with PHP 8.1 and Composer. To utilize it, simply execute
nix develop
within the directory. For those using direnv
(with nix-direnv
), a preconfigured .envrc
file is also provided, which will
automatically load the environment upon entering the library directory.
The relevant decisions regarding the project are collected in the adr
folder, following the
Architectural decision record format
The library provides several immutable data structures useful to write applications in a functional style.
Currently, the implemented data structures are:
You can find more details about the implementation and the idea behind each data structure in the docs/data-structures folder.
The library is built to be extremely abstract and generic to allow extreme composability and reusability.
There are various ways which you can use to interact with the provided data structures.
You can think of typeclasses as of behaviours which could be attached to a data structure. Since a data structure could in principle have more than one way to implement a specific behaviour (e.g., there's more than one way to use two integers to compute a new integer), we can not use directly interfaces to be implemented by our data structures. Therefore, typeclass instances are implemented as separate independent objects implementing an interface which describes the typeclass itself.
For example, the Semigroup
typeclass, which describes the behaviour of putting together two things of the same type
to obtain a thing of the same type,
could be implemented as
/**
* @template A
*/
interface Semigroup
{
/**
* @param A $a
* @param A $b
* @return A
*/
public function append($a, $b);
}
Now we can implement a Semigroup
instance for any type we want, even for native types. For example, we could implement
a semigroup for addition between integers
/**
* @implements Semigruop<int>
*/
final class IntAddition implements Semigroup
{
/**
* @param int $a
* @param int $b
* @return int
*/
public function append($a, $b): int
{
return $a + $b;
}
}
Then we could use it to sum two integers
(new IntAddition())->append(1, 2); // returns 3
This specific instance is not that interesting, but the fact that you could write code which depends on a generic
Semigroup
definitely is!
The typeclasses we are currently exposing are:
More details on each typeclass can be found in the docs/typeclasses folder.
As a design principle for this library, we try to expose on our data structures only methods which come from a typeclass. This means that the provided data structure have a standard common API which makes use of typeclasses instances.
For example, Either
has two Apply
instances. To choose which one you want to use, Either
exposes the iapply
method which takes as first argument an instance of an Apply
typeclass for Either
.
/**
* @template A
* @template B
*/
final class Either
{
/**
* @template C
* @param Apply<EitherBrand<A>> $apply
* @param HK1<EitherBrand<A>, callable(B): C> $f
* @return Either<A, C>
*/
public function iapply(Apply $apply, HK1 $f): self
}
We are able to specify that a typeclass instance refers to a specific data structure using the so-called
Brands
, which are nothing else that tags at the type level which enable us to simulate higher
kinded types.
More often than not a data structure admits only one instance of a typeclass, or there exists one which is considered standard in the literature. In such cases it is quite inconvenient to sustain the burden of passing the typeclass instance; to ease the pain, we expose also the method where the default typeclass instance is already provided.
Continuing with the example in the previous section, Either
exposes also a method apply
where the EitherApply
instance is hardcoded.
/**
* @template A
* @template B
*/
final class Either
{
/**
* @template C
* @param HK1<EitherBrand<A>, callable(B): C> $f
* @return Either<A, C>
*/
public function apply(HK1 $f): self
{
return $this->iapply(new EitherApply(), $f);
}
}
If you wish to contribute to the project, please read the CONTRIBUTING notes.