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 timeis_ref,is_array,is_hash,is_code,is_defined- single SV flag checkis_true,is_false,bool- direct SvTRUE checkis_num,is_int,is_blessed,is_scalar_ref,is_regex,is_glob- extended type checksis_positive,is_negative,is_zero- numeric comparisonsis_even,is_odd- single bitwise ANDis_between- range check (two comparisons)is_empty_array,is_empty_hash- direct AvFILL/HvKEYS checkarray_len,hash_size- direct AvFILL/HvKEYS accessarray_first,array_last- direct av_fetch without slice overheadis_empty,starts_with,ends_with- direct SvPV/SvCUR string accesstrim,ltrim,rtrim- whitespace trimmingmaybe- conditional return (if defined)sign- return -1/0/1 based on signmin2,max2- two-value min/maxclamp- inlined numeric comparison
XS functions (faster than pure Perl, but still have call overhead):
memo,force,dig- memoization and safe navigationnvl,coalesce- null coalescingfirst,any,all,none- short-circuit list operationspipeline,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 scalars3-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.