NAME

util - Functional programming utilities with XS acceleration

SYNOPSIS

use util qw(
    memo pipeline compose partial lazy force dig tap clamp identity always
    noop stub_true stub_false stub_array stub_hash stub_string stub_zero
    nvl coalesce first any all none
    first_gt first_lt first_ge first_le first_eq first_ne
    final final_gt final_lt final_ge final_le final_eq final_ne
    any_gt any_lt any_ge any_le any_eq any_ne
    all_gt all_lt all_ge all_le all_eq all_ne
    none_gt none_lt none_ge none_le none_eq none_ne
    uniq partition pick omit pluck defaults count replace_all negate once
    is_ref is_array is_hash is_code is_defined is_string
    is_empty starts_with ends_with trim ltrim rtrim
    is_true is_false bool
    is_num is_int is_blessed is_scalar_ref is_regex is_glob
    is_positive is_negative is_zero
    is_even is_odd is_between
    is_empty_array is_empty_hash array_len hash_size
    array_first array_last
    maybe sign min2 max2
);

# Type predicates - compile-time optimized
if (is_array($data)) { ... }
if (is_hash($config)) { ... }
if (is_code($callback)) { ... }
if (is_defined($value)) { ... }

# Boolean/Truthiness predicates
if (is_true($value)) { ... }   # Perl truth semantics
if (is_false($value)) { ... }  # Perl false semantics
my $normalized = bool($value); # Normalize to 1 or ''

# Extended type predicates
if (is_num($value)) { ... }        # Numeric value or looks like number
if (is_int($value)) { ... }        # Integer value
if (is_blessed($obj)) { ... }      # Blessed reference
if (is_scalar_ref($ref)) { ... }   # Scalar reference
if (is_regex($qr)) { ... }         # Compiled regex (qr//)
if (is_glob(*FH)) { ... }          # Glob

# Numeric predicates
if (is_positive($num)) { ... }     # > 0
if (is_negative($num)) { ... }     # < 0
if (is_zero($num)) { ... }         # == 0
if (is_even($num)) { ... }         # n & 1 == 0
if (is_odd($num)) { ... }          # n & 1 == 1
if (is_between($n, 1, 10)) { ... } # Range check (inclusive)

# Collection predicates - direct AvFILL/HvKEYS access
if (is_empty_array($aref)) { ... }
if (is_empty_hash($href)) { ... }
my $len = array_len($aref);        # Direct AvFILL access
my $size = hash_size($href);       # Direct HvKEYS access
my $first = array_first($aref);    # Without slice overhead
my $last = array_last($aref);      # Without slice overhead

# String predicates - direct SvPV/SvCUR access
if (is_empty($str)) { ... }
if (starts_with($filename, '/')) { ... }
if (ends_with($filename, '.txt')) { ... }

# Memoization - cache function results
my $fib = memo(sub {
    my $n = shift;
    return $n if $n < 2;
    return $fib->($n-1) + $fib->($n-2);
});

# Pipelines - chain transformations
my $result = pipeline($data,
    \&fetch,
    \&transform,
    \&process
);

# Lazy evaluation - defer computation
my $expensive = lazy { heavy_computation() };
my $result = force($expensive);

# Safe navigation - no exceptions
my $val = dig($hash, qw(deep nested key));

# Null coalescing
my $val = nvl($maybe_undef, $default);
my $val = coalesce($a, $b, $c);  # First defined

# List operations with callbacks
my $found = first(sub { $_->{active} }, \@users);
if (any(sub { $_ > 10 }, \@numbers)) { ... }
if (all(sub { $_->{valid} }, \@records)) { ... }

# Specialized predicates - pure C, no callback overhead
my $large = first_gt(\@numbers, 100);              # first > 100
my $adult = first_ge(\@users, 'age', 18);          # first user age >= 18
my $last_minor = final_lt(\@users, 'age', 18);     # last user age < 18
if (any_gt(\@values, $threshold)) { ... }          # any > threshold
if (all_ge(\@scores, 60)) { ... }                  # all >= 60
if (none_lt(\@ages, 18)) { ... }                   # no minors

# Debugging helper - execute side effect, return original
my $result = tap(sub { print "Got: $_\n" }, $value);

# Constrain value to range
my $clamped = clamp($value, $min, $max);

# Identity function - returns argument unchanged
my $same = identity($x);

# Constant function factory
my $get_zero = always(0);
my $get_config = always({ debug => 1 });
$get_zero->();  # Always returns 0

DESCRIPTION

util provides functional programming utilities implemented in XS/C.

Custom ops (compile-time optimization, no function call overhead):

  • identity - eliminated entirely at compile time

  • is_ref, is_array, is_hash, is_code, is_defined - single SV flag check

  • is_true, is_false, bool - direct SvTRUE check

  • is_num, is_int, is_blessed, is_scalar_ref, is_regex, is_glob - extended type checks

  • is_positive, is_negative, is_zero - numeric comparisons

  • is_even, is_odd - single bitwise AND

  • is_between - range check (two comparisons)

  • is_empty_array, is_empty_hash - direct AvFILL/HvKEYS check

  • array_len, hash_size - direct AvFILL/HvKEYS access

  • array_first, array_last - direct av_fetch without slice overhead

  • is_empty, starts_with, ends_with - direct SvPV/SvCUR string access

  • trim, ltrim, rtrim - whitespace trimming

  • maybe - conditional return (if defined)

  • sign - return -1/0/1 based on sign

  • min2, max2 - two-value min/max

  • clamp - inlined numeric comparison

XS functions (faster than pure Perl, but still have call overhead):

  • memo, force, dig - memoization and safe navigation

  • nvl, coalesce - null coalescing

  • first, any, all, none - short-circuit list operations

  • pipeline, compose - micro improvements (~15-20%)

  • lazy, tap, always - deferred evaluation and debugging

Functions that call arbitrary Perl coderefs (pipeline, compose, tap, first, any, all, none) are limited by call_sv() overhead and cannot achieve the same performance as pure data operations.

FUNCTIONS

memo

my $cached = memo(\&expensive_function);
my $result = $cached->($arg);

Returns a memoized version of the given function. Results are cached based on arguments, so repeated calls with the same arguments return instantly from the cache.

pipeline

my $result = pipeline($initial_value, \&fn1, \&fn2, \&fn3);

Pipes a value through a series of functions, passing the result of each function as the argument to the next. Equivalent to fn3(fn2(fn1($value))) but more readable.

compose

my $pipeline = compose(\&fn3, \&fn2, \&fn1);
my $result = $pipeline->($value);

Creates a new function that composes the given functions right-to-left. compose(\&c, \&b, \&a) creates a function equivalent to sub { c(b(a(@_))) }.

partial

my $add5 = partial(\&add, 5);
my $result = $add5->(3);  # add(5, 3) = 8

Creates a partially applied function with some arguments pre-bound. The returned function, when called, prepends the bound arguments to any new arguments.

Note: Creating AND calling a partial is 125% faster than pure Perl. However, repeatedly calling an already-created partial is ~20% slower than a hand-written closure. Use partial when you create once and call many times from different contexts, or for cleaner functional code.

lazy

my $deferred = lazy { expensive_computation() };

Creates a lazy value that defers computation until forced. The computation runs at most once; subsequent forces return the cached result.

force

my $result = force($lazy_value);

Forces evaluation of a lazy value, returning the computed result. If the value has already been forced, returns the cached result. Non-lazy values pass through unchanged.

dig

my $val = dig($hashref, @keys);
my $val = dig($hashref, 'a', 'b', 'c');  # $hashref->{a}{b}{c}

Safely traverses a nested hash structure. Returns undef if any key is missing, without throwing an exception.

tap

my $result = tap(\&block, $value);
my $result = tap(sub { print "Debug: $_\n" }, $value);

Executes a side-effect block with the value (setting $_ and passing as argument), then returns the original value unchanged. Useful for debugging pipelines without affecting data flow.

clamp

my $clamped = clamp($value, $min, $max);

Constrains a numeric value to a range. Returns $min if $value < $min, $max if $value > $max, otherwise returns $value.

identity

my $same = identity($value);

Returns the argument unchanged. Uses compile-time optimization to eliminate the function call entirely. Useful as a default transformer in pipelines or when an API requires a function but you want a no-op.

always

my $get_value = always($constant);
$get_value->();        # Returns $constant
$get_value->(1,2,3);   # Still returns $constant (args ignored)

Creates a function that always returns the same value, ignoring any arguments. Useful for callbacks that need to return a fixed value.

noop

noop();           # Returns undef
noop(1, 2, 3);    # Ignores args, returns undef

Does nothing, returns undef. Ignores all arguments. Useful as a default callback or placeholder.

Note: This returns undef (not empty list) for correct behavior in map contexts. The standalone noop module returns empty list which is ~45% faster but produces different results in map { noop() } @list.

stub_true, stub_false

stub_true();      # Always returns 1
stub_false();     # Always returns ''

Constant functions that always return true or false. Useful as default predicates:

my @all = grep { stub_true() } @items;   # Accepts all
my @none = grep { stub_false() } @items; # Rejects all

stub_array, stub_hash

my $arr = stub_array();   # Returns new []
my $hash = stub_hash();   # Returns new {}

Factory functions that return new empty arrayrefs or hashrefs. Each call returns a fresh reference.

stub_string, stub_zero

stub_string();    # Returns ''
stub_zero();      # Returns 0

Return empty string or zero. Unlike stub_false, these return specific values rather than just falsy values.

nvl

my $val = nvl($value, $default);

Returns $value if defined, otherwise returns $default. This is the null coalescing operator found in many languages (?? in C#, // in Perl 5.10+).

coalesce

my $val = coalesce($a, $b, $c, ...);

Returns the first defined value from the argument list. If all arguments are undefined, returns undef.

first

my $found = first(sub { $_->{active} }, \@list);

Returns the first element in \@list for which the block returns true. Sets $_ to each element in turn. Returns undef if no element matches. Short-circuits on first match. Takes an arrayref to avoid stack flattening overhead (5-6x faster than list-based version for early matches).

any

my $bool = any(sub { $_ > 10 }, \@list);

Returns true if the block returns true for any element in \@list. Short-circuits on first match.

all

my $bool = all(sub { $_->{valid} }, \@list);

Returns true if the block returns true for all elements in \@list. Returns true for an empty list (vacuous truth). Short-circuits on first failure.

none

my $bool = none(sub { $_->{error} }, \@list);

Returns true if the block returns false for all elements in \@list. Equivalent to not any { ... } @list. Short-circuits on first match.

SPECIALIZED ARRAY PREDICATES

These functions perform pure C comparisons without any Perl callback overhead.

All functions support two forms:

  • 2-arg: first_gt(\@numbers, $threshold) - array of scalars

  • 3-arg: first_gt(\@users, 'age', $threshold) - array of hashes

first_gt, first_ge, first_lt, first_le, first_eq, first_ne

# Find first element > 500
my $found = first_gt(\@numbers, 500);

# Find first user with age >= 18
my $adult = first_ge(\@users, 'age', 18);

Returns the first element matching the comparison, or undef if none match.

final, final_gt, final_ge, final_lt, final_le, final_eq, final_ne

# Find last element > 500 (with callback)
my $found = final(sub { $_ > 500 }, \@numbers);

# Find last element > 500 (specialized)
my $found = final_gt(\@numbers, 500);

# Find last user with age < 18 (most recent minor)
my $minor = final_lt(\@users, 'age', 18);

Returns the last element matching the comparison, or undef if none match. Uses backwards iteration for efficiency - stops as soon as a match is found from the end of the array.

any_gt, any_ge, any_lt, any_le, any_eq, any_ne

# Check if any element > threshold
if (any_gt(\@numbers, 100)) { ... }

# Check if any user is under 18
if (any_lt(\@users, 'age', 18)) { ... }

Returns true if any element matches the comparison.

all_gt, all_ge, all_lt, all_le, all_eq, all_ne

# Check if all scores are passing
if (all_ge(\@scores, 60)) { ... }

# Check if all users are adults
if (all_ge(\@users, 'age', 18)) { ... }

Returns true if all elements match the comparison. Returns true for empty arrays.

none_gt, none_ge, none_lt, none_le, none_eq, none_ne

# Check if no element exceeds limit
if (none_gt(\@values, 1000)) { ... }

# Check if no user is a minor
if (none_lt(\@users, 'age', 18)) { ... }

Returns true if no element matches the comparison.

DATA MANIPULATION

These functions transform and extract data from arrays and hashes.

uniq

my @unique = uniq(@list);

Returns a list with duplicate values removed, preserving order. The first occurrence of each value is kept. Uses a hash for O(1) lookups.

partition

my ($evens, $odds) = partition(sub { $_ % 2 == 0 }, \@numbers);

Splits an array into two arrayrefs based on a predicate. The first contains elements for which the predicate returns true, the second contains elements for which it returns false.

pick

my $subset = pick(\%hash, @keys);

Returns a new hashref containing only the specified keys from the source hash. Missing keys are silently ignored.

my $user_info = pick(\%user, 'name', 'email');

omit

my $filtered = omit(\%hash, @keys);

Returns a new hashref with the specified keys removed. Opposite of pick.

my $safe = omit(\%user, 'password', 'secret_token');

pluck

my @ids = pluck(\@users, 'id');

Extracts a single field from an array of hashes. Returns a list of values for that field from each hash.

my @names = pluck(\@employees, 'name');

defaults

my $merged = defaults(\%hash, \%defaults);

Returns a new hashref with values from %defaults filled in for any missing keys in %hash. Does not modify the original hashes.

my $config = defaults(\%user_config, { timeout => 30, retries => 3 });

count

my $n = count(sub { $_ > 10 }, \@numbers);

Counts how many elements in the list satisfy the predicate. More efficient than scalar grep { ... } @list because it doesn't build an intermediate list.

replace_all

my $result = replace_all($string, $search, $replace);

Replaces all occurrences of $search in $string with $replace. Faster than $str =~ s/\Q$search\E/$replace/g for literal strings because it avoids regex compilation.

negate

my $not_even = negate(sub { $_ % 2 == 0 });

Returns a new function that negates the result of the given predicate. Useful for inverting filters.

my @odds = grep { negate(\&is_even)->($_) } @numbers;

once

my $init_once = once(\&initialize);
$init_once->();  # Runs initialize()
$init_once->();  # Returns cached result, doesn't run again

Wraps a function to ensure it only executes once. Subsequent calls return the cached result of the first call.

TYPE PREDICATES

These functions use custom ops and are replaced at compile time with direct SV flag checks. They have zero function call overhead.

is_ref

my $bool = is_ref($value);

Returns true if $value is a reference (any type).

is_array

my $bool = is_array($value);

Returns true if $value is an array reference.

is_hash

my $bool = is_hash($value);

Returns true if $value is a hash reference.

is_code

my $bool = is_code($value);

Returns true if $value is a code reference.

is_defined

my $bool = is_defined($value);

Returns true if $value is defined (not undef).

is_string

my $bool = is_string($value);

Returns true if $value is a plain scalar (defined and not a reference). This is useful when you want to check if a value is a simple string or number, not undef and not a reference to something else.

is_string("hello");      # true
is_string(42);           # true
is_string(undef);        # false
is_string([1,2,3]);      # false (arrayref)
is_string({a=>1});       # false (hashref)

STRING PREDICATES

These functions use custom ops with direct SvPV/SvCUR access.

is_empty

my $bool = is_empty($value);

Returns true if $value is undefined or an empty string.

starts_with

my $bool = starts_with($string, $prefix);

Returns true if $string starts with $prefix. Uses direct memcmp. Returns false if either argument is undefined.

ends_with

my $bool = ends_with($string, $suffix);

Returns true if $string ends with $suffix. Uses direct memcmp. Returns false if either argument is undefined.

trim

my $trimmed = trim($string);

Removes leading and trailing whitespace from $string. Returns a new string with whitespace removed. Returns undef if $string is undefined. Whitespace includes spaces, tabs, newlines, and other ASCII whitespace characters.

ltrim

my $trimmed = ltrim($string);

Removes leading whitespace only from $string. Trailing whitespace is preserved. Returns undef if $string is undefined.

rtrim

my $trimmed = rtrim($string);

Removes trailing whitespace only from $string. Leading whitespace is preserved. Returns undef if $string is undefined.

CONDITIONAL OPS

These functions use custom ops for conditional operations.

maybe

my $result = maybe($value, $then);

Returns $then if $value is defined, otherwise returns undef. Conditionally returns a value based on whether another value is defined.

# Instead of: defined($x) ? $y : undef
my $result = maybe($x, $y);

# Useful for safe transformations:
my $upper = maybe($input, uc($input));

NUMERIC OPS

These functions use custom ops for numeric operations.

sign

my $s = sign($number);

Returns -1 if $number is negative, 0 if zero, 1 if positive. Returns undef for non-numeric values.

Note: If you only need the comparison result and don't need undef handling, the spaceship operator $number <=> 0 is faster.

min2

my $smaller = min2($a, $b);

Returns the smaller of two numeric values.

max2

my $larger = max2($a, $b);

Returns the larger of two numeric values.

BOOLEAN/TRUTHINESS PREDICATES

These functions use custom ops for Perl truth semantics checks.

is_true

my $bool = is_true($value);

Returns true if $value is truthy according to Perl semantics. This means: defined, non-empty string, non-zero number.

is_false

my $bool = is_false($value);

Returns true if $value is falsy according to Perl semantics. This includes: undef, empty string "", string "0", numeric 0.

bool

my $normalized = bool($value);

Normalizes $value to a boolean (1 for true, '' for false). Useful when you need a consistent boolean representation.

EXTENDED TYPE PREDICATES

These functions use custom ops for extended type checking.

is_num

my $bool = is_num($value);

Returns true if $value is numeric (has a numeric value or looks like a number). Uses looks_like_number for strings.

is_int

my $bool = is_int($value);

Returns true if $value is an integer. Returns true for whole number floats like 5.0.

is_blessed

my $bool = is_blessed($value);

Returns true if $value is a blessed reference (an object). Uses sv_isobject.

is_scalar_ref

my $bool = is_scalar_ref($value);

Returns true if $value is a scalar reference (not array/hash/code).

is_regex

my $bool = is_regex($value);

Returns true if $value is a compiled regular expression (qr//).

is_glob

my $bool = is_glob($value);

Returns true if $value is a glob (like *STDIN, *main::foo).

NUMERIC PREDICATES

These functions use custom ops for numeric comparisons. They first check if the value is numeric, then perform the comparison.

is_positive

my $bool = is_positive($value);

Returns true if $value is numeric and greater than zero. Returns false for non-numeric values.

is_negative

my $bool = is_negative($value);

Returns true if $value is numeric and less than zero. Returns false for non-numeric values.

is_zero

my $bool = is_zero($value);

Returns true if $value is numeric and equals zero. Returns false for non-numeric values.

is_even

my $bool = is_even($value);

Returns true if $value is an integer and even (divisible by 2).

is_odd

my $bool = is_odd($value);

Returns true if $value is an integer and odd (not divisible by 2).

is_between

my $bool = is_between($value, $min, $max);

Returns true if $value is numeric and between $min and $max (inclusive). Returns false for non-numeric values.

COLLECTION PREDICATES

These functions use custom ops for collection operations with direct AvFILL/HvKEYS access.

is_empty_array

my $bool = is_empty_array($arrayref);

Returns true if $arrayref is an array reference with no elements. Returns false for non-arrayrefs. Uses direct AvFILL check.

is_empty_hash

my $bool = is_empty_hash($hashref);

Returns true if $hashref is a hash reference with no keys. Returns false for non-hashrefs. Uses direct HvKEYS check.

array_len

my $len = array_len($arrayref);

Returns the length of the array using direct AvFILL access. Returns undef for non-arrayrefs.

hash_size

my $size = hash_size($hashref);

Returns the number of keys in the hash using direct HvKEYS access. Returns undef for non-hashrefs.

array_first

my $elem = array_first($arrayref);

Returns the first element of the array without slice overhead. Returns undef for empty arrays or non-arrayrefs.

array_last

my $elem = array_last($arrayref);

Returns the last element of the array without slice overhead. Returns undef for empty arrays or non-arrayrefs.

AUTHOR

LNATION <email@lnation.org>

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.