PinkCrab Function Constructors

I am a fan of Function programming and while PHP is not exactly the first language you think of when you think about it, but it can be. One of the biggest things its lacking is being able to handle partial application better when using functions in High Order contexts or as simple callbacks.

Links

Installation

This is a native PHP lib that can be added via composer composer require pinkcrab/function-constructors

Usage

This library contains a collection of functions that can be used to combine to create unique, reuseable functions or as simple callbacks

Install

composer require pinkcrab/function-constructors

The library exposes six namespaces. Alias them once at the top of your file so the rest reads cleanly:

use PinkCrab\FunctionConstructors\Arrays           as Arr;
use PinkCrab\FunctionConstructors\Strings          as Str;
use PinkCrab\FunctionConstructors\Numbers          as Num;
use PinkCrab\FunctionConstructors\Comparisons      as C;
use PinkCrab\FunctionConstructors\Objects          as Obj;
use PinkCrab\FunctionConstructors\GeneralFunctions as F;

compose() — build once, reuse everywhere

compose stitches several callables into one. Each bound partial is named for what it does, so the pipeline reads like a spec.

$slugify = F\compose('trim', 'strtolower', Str\replaceWith(' ', '-'));

$slugify('  Hello World  ');
// 'hello-world'

array_map($slugify, ['  Hello World  ', 'FOO Bar']);
// ['hello-world', 'foo-bar']

pipe() — run it once, right now

Same shape as compose, but it takes the value as its first argument and runs the chain immediately. Useful when you don’t need to reuse the pipeline.

F\pipe(
    [3, 4, 5, 6, 8],
    Arr\filter(Num\isMultipleOf(2)),   // keep evens
    Arr\map(Num\multiply(2))           // double them
);
// [8, 12, 16]

Named predicates replace ad-hoc lambdas

Partially apply a predicate once, reuse it across the codebase. propertyEquals works on arrays or objects — same API.

$isAdmin    = F\propertyEquals('role', 'admin');
$isVerified = F\propertyEquals('verified', true);
$isTrusted  = C\groupAnd($isAdmin, $isVerified);

array_filter($users, $isTrusted);
// only users with role=admin AND verified=true

Domain pipelines — a pricing example

Currency × VAT × rounding × formatting — one composed Closure, reused across cart totals, invoices, emails. Change a rate once, every caller picks it up.

$priceForEU = F\compose(
    Num\multiply(1.17),    // GBP → EUR
    Num\multiply(1.20),    // + 20% VAT
    Num\round(2),
    Str\digit(2, '.', ','),
    fn($s) => '€' . $s
);

$priceForEU(100.00);           // '€140.40'
array_map($priceForEU, $cart); // whole cart in one line

Null-safe nested access

composeSafe halts at the first null — replaces an isset()/?-> ladder with one pipeline.

$getCountry = F\composeSafe(
    F\getProperty('profile'),
    F\getProperty('address'),
    F\getProperty('country')
);

$getCountry(['profile' => ['address' => ['country' => 'GB']]]); // 'GB'
$getCountry(['profile' => null]);                                // null — chain halted

Lazy pipelines over infinite sources

Every Arrays function accepts a Generator. Lazy ones stream; short-circuit ones stop at the answer. You can safely run a filter over a log file that’s too big to fit in memory.

$lines = function () {
    $fh = fopen('/var/log/access.log', 'r');
    while (($line = fgets($fh)) !== false) yield $line;
    fclose($fh);
};

// First 5xx line — the file is read only up to that point.
$firstServerError = Arr\filterFirst(Str\containsPattern('/\b5\d{2}\b/'));

echo $firstServerError($lines());

Quick reference — the six namespaces

  • Arrays — filter, map, sort, fold, groupBy, take, chunk, zip… 56 functions covering every collection operation. All accept iterables.
  • Strings — curried wrappers around PHP’s string functions: trim, append, prepend, replaceWith, contains, wrap, split, etc.
  • Numbers — arithmetic as partial applications: sum, multiply, divideBy, isMultipleOf, round, power…
  • Comparisons — predicate factories: isEqualTo, isGreaterThan, groupAnd, groupOr, not, notEmpty, isNumber.
  • Objects — type and trait predicates plus a small object factory: isInstanceOf, usesTrait, toArray, createWith.
  • GeneralFunctions — compose, pipe, getProperty, propertyEquals, ifElse, always, sideEffect, recordEncoder.

Additional Information

While this might seem like a silly package, it does come in really handy for things like tests and general callbacks for validation. Its also fun to play around and try to handle code all within a chain of functions.

$values = ['hello', '', 'world', 0, null, 'foo', 42, '   '];

// Vanilla PHP
$filtered = array_filter($values, function ($value) {
    return is_string($value) && !empty($value);
});

// Using inlined
$filtered = array_filter($values, C\all('is_string', F::NOT_EMPTY));

// As a compiled callback
$callback = C\all('is_string', F::NOT_EMPTY);
$filtered = array_filter($values, $callback);
if($callback($foo)){..}

Filtering Any

// Variant A: shorthand with hash, e.g. "#fff"
$isShortHashHex = C\all(
    'is_string',
    Str\startsWith('#'),
    Str\containsPattern('/^#[0-9a-f]{3}$/i')
);

// Variant B: full with hash, e.g. "#ffffff"
$isFullHashHex = C\all(
    'is_string',
    Str\startsWith('#'),
    Str\containsPattern('/^#[0-9a-f]{6}$/i')
);

// Variant C: full without hash, e.g. "ffffff"
$isBareHex = C\all(
    'is_string',
    C\not(Str\startsWith('#')),
    Str\containsPattern('/^[0-9a-f]{6}$/i')
);

// One predicate that accepts any of the three.
$isHexColour = C\any($isShortHashHex, $isFullHashHex, $isBareHex);

// Quick smoke test
$samples = [
    '#fff',     // ✅ A
    '#FFFFFF',  // ✅ B (case-insensitive)
    'a1b2c3',   // ✅ C
    '#ggg',     // ❌ non-hex chars
    '#ffff',    // ❌ wrong length
    'fff',      // ❌ short form must have hash in this scheme
    null,       // ❌ not a string
];

Leave a Reply

Your email address will not be published. Required fields are marked *