Drawing inspiration mainly from Haskell, this library is trying to replicate in behaviour a lot of Haskell’s functional structures.
This library is my own effort and journey into better understanding these functional structures and explore their use in PHP. If you find any error, theoretical or implementation related, do not hesitate to point it out.
There is a set of goals that are driving the development of this library. Below is a list, in historical order they were decided and not in priority, no one goal is more important than the other. To reflect that they have been given colours instead of numbers.
The main idea driving the design is to have a public interface that is usable and allows static analysis to infer types. Under the public interface it is allowed to go to any length in design in order to satisfy the behaviour we are targeting. Things from Haskell do not necessarily map directly to PHP due to differences in capabilities offered. It is a fact we could re-implement the entirety of Haskell in PHP in behaviour but judgement must be used to bring things into perspective.
There is a mapping of concepts from Haskell to PHP. For example, a function in
Haskell can be represented as a Composition in PHP when you want to compose with
it. There is some juggling with the different perspectives still and things do
not map 1-1 under all use-cases. Another example is implementing type classes
solely as interfaces for flexibility and simplicity but it could also be
abstract classes or some other form all together. Another example is that <-
from Haskell is implemented as getValue() method call. It could be __invoke or
it could be a much more elaborate structure.
There is a mapping of concepts from Category Haskell to Category PHP and that is opinionated, possibly the gravity is on composition at this point. A different mapping is used from other libraries. This is what I assume is the design of this library. For example `lamphpda` has different objects for each instance: there are distinct implementations of MaybeFunctor, MaybeApplicative etc because probably they map a Haskell type class instance into a single object, which makes a lot of sense as PHP does not support the concept of type classes natively.
Another goal is to explore what is possible from the perspective of the user trying to use functional programming concepts from code that is not written in a functional programming style. Specifically, how do we reuse existing code that is not written in a functional programming way from the functional programming structures in this package. If I can create functors or monads for new code written specifically for this library it has very little benefit when this code has to interface with existing other code that is not written with functors or monads in mind (for example).
Some effort can be seen with FunctorAdapter. The belief is that although
functional programming in general is interesting and it has its benefits, if
everything else is written in a non-functional way how one can interface with
it. The essence of this is to be able to express functionally and reuse other
code in a functional programming way; rather than switching paradigms.
The use of multiple functions and closures can get expensive and sub-optimal from a direct implementation that does not use the constructs introduced by this library. However, it is, at least theoretically, possible to generate a more optimised version of the code.
This goal targets using this library from IDEs to generate quickly prefabricated code or trying to optimise an implementation’s performance.
A Maybe tries to replicate Haskell’s data type Maybe. As per definition it can be `Nothing` or `Just x` where x is any value.
To construct a `Maybe a` value you can:
$maybe = Maybe::just(123));
/** @var Maybe<int> */
$anotherMaybe = Maybe::nothing();
$actualValue = $maybe->getValue(); // returns Just 123, so you can match the type
$anotherActualvalue = $anotherMaybe->getValue(); // returns Nothing for the same reasonIn the case of Nothing, the annotation is required, otherwise the type is `Maybe<never>`. However, it can be inferred from the function or method return type.
/** @return Maybe<string> */
function mapToMaybe(?string $value): Maybe
{
if ($value === null) {
return Maybe::nothing();
}
return Maybe::just($value);
}
mapToMaybe(null); // Maybe<string>
mapToMaybe('something'); // Maybe<string>TODO: What about extending Maybe (ie MaybeString) to get the type from static analysis. (getValue() will already have the correct type)
Maybe implements Functor so you can apply a function to the underlying
value, taking into account the Maybe logic (it will apply only when it
is Just).
$maybe = Maybe::just(123));
$maybeTimes34 = $maybe->fmap(fn ($x) => $x * 34);
$result = $maybeTimes34->getValue(); // returns a Just(4182)
$maybe = Maybe::nothing();
$maybeTimes34 = $maybe->fmap(fn ($x) => $x * 34);
$result = $maybeTimes34->getValue(); // returns a NothingApart from this, Maybe implements Show and Eq.
IO is a wrapper for some action that will perform some IO or otherwise have some
side-effect. The action must be provided in a callable. However, there is no
strict check whether the provided action actually performs some IO or has some
side-effect.
$data = new IO(function () {
print "Hello world";
return time();
});
$result = $data->getValue(); // time() will run on the moment of this callIO implements Functor and can apply a function to the result of the IO operation.
$data = new IO(function () {
print "Hello world";
return \time();
});
$data = $data->fmap( fn ($seconds) => (int) ($seconds / 60 / 60 / 24) );
$result = $data->getValue(); // time() will run on this call (returns value in days)Composition is a general helper and a syntax helper. Function
composition is implented in its fmap() which essentially implements a
version of Functor ((->) r). However the class itself is bundling more
utilities and can better be seen as a form of expression.
Example usage:
$composition = new Composition(min(...));
$result = $composition([2, 3, 4]); // returns 2Alternative with the shorthand function c:
$composition = c (min(...));
$composition = c ('min'); // equivalent
$result = $composition ([2, 3, 4]);Note that spaces have been added for brevity, `c` is a regular function and this would be entirely fine:
$result = c('min')([2, 3, 4]);The above is a trivial example to show the syntax, if one is to use
min to calculate the minumum of an array there is no direct need to
use the Composition.
Among the features of this wrapper is composing functions:
$result = c ('array_filter') ->fmap('min') ([0, 2, 3, 4]);Which is the equivalent of:
$result = min( array_filter([0, 2, 3, 4]) );Note that the order of application is as they appear in the expression, making it the reverse of Haskell’s (.) which would be
let result = minimum . arrayFilter $ [0, 2, 3, 4]
where arrayFilter = filter (\x -> x > 0)
The important goal here is that using `c` and `fmap` we now have control over a “composition” of function calls and this is why it is considered an expression helper. Notable is that it also wraps around partial application.
$composition->fmap(fn ($x) => $x % 2);
$result = $composition([2, 3, 4]); // returns 0, effectively computing: min([2,3,4]) % 2 TODO: Ergonomics here, could also implement a __call magic function ?
Or pipe() so that the user can keep adding functions one after the
other?
Currently type classes have been implemented as an interface.
TODO: Add main part for FunctorInterface
Utility traits are provided to help proove that your implementation satisfies Functor laws. Example usage:
use FunctorProof;
public function testIsAFunctor(): void
{
$this->assertInstanceIsFunctor(
Maybe::just(5),
fn (int $x): bool => $x == 5,
fn (bool $x): string => $x == true ? '100' : '500'
);
}The first argument of assertInstanceIsFunctor() expects to receive your object
that implements FunctorInterface.
The second and third arguments are two possible functions to check whether your functor implementation is indeed associative.
TODO: What happens when the functor is not associative - expand.
To wrap around existing code and introduce some functionality, a
convenience class is provided. A Wrapper to wrap around some callable
and allow adjusting input (contramap) and output (fmap).
$wrapper = Wrapper::withAdjustedInput($psr3logger->critical(...)); // Arguments: string message, array $context = []The above will automatically wrap the input of the wrapper when called
to a Tuple. You may use the convenience function t() or t3() (Tuple3)
to construct tuples.
$wrapper( t("Log message", ["context"]) );Wrapper::withAdjustedInput will automatically reflect on the callable
passed and assume input in a Tuple or Tuple3 (2 or 3 arguments),
normalising the input to 1 argument. If a callable with 0 or 1
arguments is passed then the input will not be automatically adjusted.
You can further adjust the input by calling `adjustInput` or `contramap` directly.
$wrapper = Wrapper::withAdjustedInput($psr3logger->critical(...)); // Arguments: string message, array $context = []
$wrapper( t("Log message", ["context"]) );
$newWrapper = $wrapper->adjustInput(fn (Tuple $p) => t("prefix: " . $p->fst(), [$p->snd()]);The only difference between calling `contramap` or `adjustInput` is that `adjustInput` will perform an additional type check, ensuring that the passed callable does not have more arguments than 1.
withAdjustedInput accepts a second optional argument to pass the first
input adjustment function directly and avoid reflection.
$wrapper = Wrapper::withAdjustedInput(
$psr3logger->critical(...),
fn (Tuple $p) => t ($p->fst(), [$p->snd()]) // notice we need to return Tuple
);
$wrapper( t("Log message", "context") );For better control on how you call the wrapped dependency you may supply a closure.
$wrapper = Wrapper::withAdjustedInput(
fn (Tuple $p) => $psr3logger->critical($p->fst(), [$p->snd()])
);
$wrapper( t("Log message", "context") );The Wrapper supports adjusting the output as well by calling
adjustOutput or directly fmap. Again, adjustOutput will perform an
additional type-check, making sure the passed callable accepts no more
than 1 argument.
$wrapper = Wrapper::withAdjustedInput($psr3logger->critical(...)); // returns void
// we expect nothing to be passed as argument as the output is void
$newWrapper = $wrapper->fmap( fn () => time() );
$result = $newWrapper ("Log message", ["context"]);
print $result; // Prints the result of time()Generally we wrap the input in a Tuple (or Tuple3) so that the types can be inferred by static analysis and also to provide a unified interface for adjusting input, conveying the information that these are the parameters for the initially wrapped thing.
If for some reason the adjustment of input or output that the Wrapper
provides is not the desired one, then one can implement their own
wrapper class and can benefit from taking the existing code as an
example for implementing fmap and contramap. The author of this
library would be very interested to know about any interesting
use-cases and variations.
Example do-notation below.
dn(
writeFile("test.txt", show (123 * 123)),
appendFile("test.txt", "Hello!\n"),
readFile("test.txt"),
putStrLn(...)
)();Function dn will use bind (>>=) if an argument is callable or a
Composition otherwise will use then (>>) to combine actions. Expanded
and commented version below.
$ioAction = dn(
// IO<int|false>
writeFile("test.txt", show (123 * 123)),
// IO<int|false> therefore will use (>>)
appendFile("test.txt", "Hello!\n"),
// IO<string|false> therefore will use (>>)
readFile("test.txt"),
// callable therefore will use (>>=)
putStrLn(...)
);
$ioAction();Bartosz Milewski’s Programming Cafe - Specially the Category Theory for Programmers Functor design pattern - HaskellForAll
Learn You a Haskell for Great Good! Haskell Wikibook Dao of Functional Programming