so.dang.cool:z

Function combinators


License
MIT

Documentation

Fearless function combination in Java

Maven Central Javadoc License

Lines of code codecov GitHub repo size jar size dependency count

Techniques

Unlock your functional programming potential with these combination techniques:

Fusion

Z.fuse(fn1).fuse(fn2) Combine two functions.

var internedTrim = Z.fuse(String::trim).fuse(String::intern);

assertEquals("hello", internedTrim.apply(" hello "));

// Protip: Interned strings can use == directly.
assertTrue("hello" == internedTrim.apply(" hello "));

Fission

Z.split(fn) Split a multiargs function into a curried form.

var concat = Z.split(String::concat);

assertEquals("hotpot", concat.apply("hot").apply("pot"));

// Protip: "Curried" functions can be partially applied.
var preSomething = concat.apply("pre");

assertEquals("prefix", preSomething.apply("fix"));
assertEquals("presume", preSomething.apply("sume"));

// Protip: Z also has a "flip" function to change order.
var fixedSomething = Z.flip(concat).apply("fix");

assertEquals("prefix", fixedSomething.apply("pre"));
assertEquals("suffix", fixedSomething.apply("suf"));

Assimilation

Z.assimilate[N](curriedFn) Flatten a curried function into a multiargs function.

var checkoutMessage = Z.assimilate2(
    (String item) ->
        (String name) -> String.format("Enjoy your %s, %s!", item, name)
);

assertEquals(
    "Enjoy your bike, Alice!",
    checkoutMessage.apply("bike", "Alice")
);

Absorption

Z.fuse(fn1).absorb(fn2) Unnaturally combine two functions.

This is an evil technique. It's provided as a tool to control evil problems, not to encourage evil code.

var heroes = new ArrayList<>(List.of("joker"));

var emptiedHeroes = Z.fuse(heroes::clear).absorb(() -> heroes);

assertEquals(List.of(), emptiedHeroes.get());

heroes.add("twoface");
emptiedHeroes.get().add("batman");
assertEquals(List.of("batman"), heroes);

Super Fusion

Z.fuse(fn1).fuse(fn2).[...].fuse(fn[N]) Combine N functions. To start with an object or primitive use Z.with(var)...

var isLocalHost = Z.with("https?://localhost(:\\d+)?(/\\S*)?")
    .fuseFunction(Pattern::compile)
    .fuse(Pattern::matcher)
    .fuse(Matcher::matches);

assertTrue(isLocalHost.test("https://localhost:443"));

Note: fuseFunction above is an explicit choice of a single-arg variant of Pattern::compile. Similar fuse[Etc] exist for other functional interfaces.

Z goals

  1. Z only provides function combinators
  2. Z will never need other dependencies
  3. Z will never rely on reflection or code generation
  4. Techniques with the potential to cause problems are annotated as @Evil; choose your path wisely...

Comparisons and advantages

Z fusion

var asciiSum = Z.fuse(String::chars, IntStream::sum);

int sum = asciiSum.applyAsInt("abc");

Equivalent with Function::compose

Function<IntStream, Integer> sumInts = IntStream::sum;
var asciiSum = sumInts.compose(String::chars);

// Captures as a Function<String, Integer> (autoboxing/unboxing occurs)
int sum = asciiSum.apply("abc");

Equivalent with a lambda

// Inference (e.g. with var) is not possible
ToIntFunction<String> asciiSum = s -> s.chars().sum();

int sum = asciiSum.applyAsInt("abc");

Some advantages of Z here:

  1. Tacit yet explicit - Z allows for point-free function combination. This means you state your logic as a fact, and don't worry as much about the exact syntax for instructions. (Of course, Z can accept lambdas!)
  2. Explicit ordering of actions - Z lets you consistently define actions in the order they'll execute.
  3. "Just works" inference - Z techniques are optimized for a wider variety of functional interfaces. It's not necessary to define (or cast) things to a Function<A, B> in order just to expose Function::compose.
  4. Idiomatic functions - Z doesn't reimplement functional interfaces or conventions that already exist. A Predicate will have a test method, a Consumer will have an accept method, etc.

More examples can be found in the Usage Examples in the project's tests.