Tuples in Phunkie are immutable ordered collections of elements where each element can have a different type. Pairs are specialized tuples with exactly two elements that support additional type class instances.
There are several ways to create tuples and pairs:
// Empty tuple (Unit)
$unit = Tuple();
// Pair (2-tuple)
$pair = Pair("hello", 42);
// or
$pair = Tuple("hello", 42);
// Tuple with more elements
$tuple = Tuple("name", 25, true);
Tuples and Pairs use 1-based indexing with underscore prefix:
$pair = Pair("hello", 42);
$tuple = Tuple("name", 25, true);
// Pair access
$first = $pair->_1; // "hello"
$second = $pair->_2; // 42
// Tuple access
$first = $tuple->_1; // "name"
$second = $tuple->_2; // 25
$third = $tuple->_3; // true
// Pair accessor methods
$first = $pair->fst(); // "hello"
$second = $pair->snd(); // 42 (same as extract())
// Tuple get method (1-based)
$value = $tuple->get(2); // 25
Tuples support a special assignment syntax for destructuring:
use function Phunkie\Functions\function1\compose;
use function Phunkie\Functions\tuple\assign;
// Assign tuple elements to variables
$name = $age = $isStudent = null;
(compose(assign($name, $age, $isStudent)))(Tuple("John", 25, true));
echo $name; // "John"
echo $age; // 25
echo $isStudent; // true
// Works with pairs too
$name = $age = null;
(compose(assign($name, $age)))(Pair("John", 25));
Both Tuples and Pairs implement the Functor type class:
$tuple = Tuple(1, 2, 3);
// Map over all elements
$result = $tuple->map(fn($x) => $x * 2); // Tuple(2, 4, 6)
// Convert all elements to a single value
$zeros = $tuple->as(0); // Tuple(0, 0, 0)
// Convert all elements to Unit
$units = $tuple->void(); // Tuple(Unit(), Unit(), Unit())
// Zip with a function
$zipped = $tuple->zipWith(fn($x) => $x * 2);
// Tuple(Pair(1, 2), Pair(2, 4), Pair(3, 6))
For Pairs, map operates on the second element only:
$pair = Pair("label", 42);
$result = $pair->map(fn($n) => $n * 2); // Pair("label", 84)
Pairs implement the Bifunctor type class, allowing independent transformations of both elements:
$pair = Pair("hello", 42);
// Transform both elements
$result = $pair->bimap(
fn($s) => strtoupper($s),
fn($n) => $n * 2
);
// Pair("HELLO", 84)
// Transform only the first element
$result = $pair->first(fn($s) => strtoupper($s));
// Pair("HELLO", 42)
// Transform only the second element
$result = $pair->second(fn($n) => $n * 2);
// Pair("hello", 84)
Both Tuples and Pairs implement the Foldable type class:
// Tuple folds over all elements
$tuple = Tuple(1, 2, 3, 4);
$sum = ($tuple->foldLeft(0))(fn($acc, $x) => $acc + $x); // 10
$list = ($tuple->foldRight([]))(fn($x, $acc) => [$x, ...$acc]);
// [1, 2, 3, 4]
// Convert to ImmList
$immList = $tuple->toImmList(); // ImmList(1, 2, 3, 4)
// Pair folds over the second element only
$pair = Pair("label", 42);
$result = ($pair->foldLeft(10))(fn($acc, $x) => $acc + $x); // 52
$list = $pair->toList(); // ImmList(42)
Pairs implement the Comonad type class, where the first element provides context and the second is the focus:
$pair = Pair("context", 42);
// Extract the focus value (second element)
$value = $pair->extract(); // 42
// Duplicate: wrap in another layer
$nested = $pair->duplicate();
// Pair("context", Pair("context", 42))
// Extend: apply a function that operates on the whole Pair
$result = $pair->extend(fn($p) => $p->_1 . ": " . $p->extract());
// Pair("context", "context: 42")
$pair = Pair(10, 5);
$result = $pair->extend(fn($p) => $p->_1 + $p->_2);
// Pair(10, 15)
// coflatMap is an alias for extend
$result = $pair->coflatMap(fn($p) => $p->_1 + $p->_2);
$pair = Pair("name", 42);
// Swap elements
$swapped = $pair->swap(); // Pair(42, "name")
$pair->swap()->swap(); // Pair("name", 42) - swap is involution
// Merge both elements with a function
$result = Pair(10, 32)->merge(fn($a, $b) => $a + $b); // 42
$text = Pair("Hello, ", "World")->merge(fn($a, $b) => $a . $b);
// "Hello, World"
// Apply same function to both elements
$pair = Pair(3, 4);
$squared = $pair->mapBoth(fn($x) => $x * $x); // Pair(9, 16)
// forEach with both elements
$pair->forEach(fn($k, $v) => echo "$k: $v\n");
// Prints: name: 42
// Check predicates on both elements
$pair = Pair(10, 20);
$pair->forallBoth(fn($x) => $x > 5, fn($y) => $y > 15); // true
$pair->existsBoth(fn($x) => $x > 15, fn($y) => $y > 15); // true
// Check predicate on both elements (inherited from Tuple)
$pair->forall(fn($x) => $x > 5); // true (applies to both elements)
$pair->exists(fn($x) => $x > 15); // true (at least one element matches)
$tuple = Tuple("a", "b", "c", "d");
// Sequence operations
$head = $tuple->head(); // "a"
$last = $tuple->last(); // "d"
$tail = $tuple->tail(); // Tuple("b", "c", "d")
$init = $tuple->init(); // Tuple("a", "b", "c")
// Slicing
$taken = $tuple->take(2); // Tuple("a", "b")
$dropped = $tuple->drop(2); // Tuple("c", "d")
// Searching
$tuple = Tuple(1, 2, 3, 4, 5);
$contains = $tuple->contains(3); // true
$exists = $tuple->exists(fn($x) => $x > 3); // true
$forall = $tuple->forall(fn($x) => $x > 0); // true
$found = $tuple->find(fn($x) => $x > 3); // Some(4)
// Indexed operations
$result = $tuple->mapIndexed(fn($x, $i) => "$i:$x");
// Tuple("1:a", "2:b", "3:c", "4:d")
$tuple->forEachIndexed(fn($x, $i) => echo "$i => $x\n");
Both Tuples and Pairs support creating modified copies:
$pair = Pair("a", 1);
$modified = $pair->copy(["_1" => "b"]);
// Pair("b", 1)
$tuple = Tuple("a", 1, true);
$modified = $tuple->copy(["_2" => 2, "_3" => false]);
// Tuple("a", 2, false)
Tuples and Pairs maintain type information for all elements:
$pair = Pair("hello", 42);
echo $pair->showType(); // "(String, Int)"
echo $pair->toString(); // "Pair(hello, 42)"
$tuple = Tuple("name", 25, true);
echo $tuple->showType(); // "(String, Int, Boolean)"
echo $tuple->toString(); // "(name, 25, true)"
$arity = $tuple->getArity(); // 3
Tuples and Pairs are immutable - attempting to modify elements throws a TypeError:
$pair = Pair("hello", 42);
// These will throw TypeError
$pair->_1 = "world"; // Error: Pairs are immutable
$pair->_3 = true; // Error: Invalid index _3 for pair
// Identity: fa.map(x => x) == fa
$pair->map(fn($x) => $x) === $pair
// Composition: fa.map(f).map(g) == fa.map(x => g(f(x)))
$pair->map($f)->map($g) === $pair->map(fn($x) => $g($f($x)))
// Identity: bimap(id, id) == id
$pair->bimap(fn($x) => $x, fn($y) => $y) === $pair
// Composition
$pair->bimap($f1, $g1)->bimap($f2, $g2) ===
$pair->bimap(fn($x) => $f2($f1($x)), fn($y) => $g2($g1($y)))
// Left identity: extract(duplicate(wa)) == wa.extract()
$pair->duplicate()->extract() === $pair->extract()
// Right identity: map(extract)(duplicate(wa)) == wa
$pair->duplicate()->map(fn($p) => $p->extract()) === $pair
// Associativity: duplicate(duplicate(wa)) == map(duplicate)(duplicate(wa))
bimap, first, second for clean transformationsfoldLeft/foldRight over manual iteration$config = Pair("default_value", userValue);
$value = $config->snd(); // Get the value
$withContext = $config->extend(fn($p) =>
$p->snd() ?? $p->fst() // Use default if null
);
$computation = Pair("Step 1", 42);
$next = $computation->extend(fn($p) =>
Pair($p->_1 . " -> Step 2", $p->_2 * 2)
);
// Instead of array(mixed $a, mixed $b, mixed $c)
function processData(Tuple $data): Tuple {
return $data->map(fn($x) => transform($x));
}