Introducing Z

Z is a Java library for bold, exciting, and fearless function combination.

I’ve been working on something that, if you’re programming in Java, (or a compatible JVM language?) that I hope you’ll enjoy.

Z lets you combine and manipulate functions at a higher level, while only introducing a handful of techniques. For teams using Java, this represents a much smaller learning curve and maintenance cost compared to adding other JVM languages or larger frameworks. (I still love you Vavr, Clojure, Kotlin, Scala, Groovy, and more!)

Z is a little side project of mine, but it’s inspired by years of real-world, production Java programming as an engineer at AWS. It’s also inspired by functional programming languages like Haskell, and, of course, inspired by Akira Toriyama’s various Dragon Ball series. It’s a unique combination of interests, but Z is all about combinations.

I plan to follow up in later articles with “Z for Haskell Developers” and “Z for Dragon Ball Fans” but in this first article I’ll skip the memes and dig right in.

Z gives you succinct, precise function combination

Function::compose combination:

// Need a capture (or cast) with explicit type to get access to compose
Function<IntStream, Integer> sumInts = IntStream::sum;
var asciiSum = sumInts.compose(String::chars);

// Only possible to use and capture as a Function<String, Integer>
// (So autoboxing/unboxing occurs)
int sum = asciiSum.apply("abc");

Function combination using a lambda:

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

Z fusion:

var asciiSum = Z.fuse(String::chars, IntStream::sum);int sum = asciiSum.applyAsInt("abc");

Some advantages of Z here:

  1. Tacit yet explicit — Z allows for point-free function combination. Without the mumbo-jumbo, this just means you state your logic as a fact. (Of course, Z can accept lambdas!)
  2. Consistent, explicit ordering of actions — Z lets you consistently define actions in the order they’ll execute. Function::compose creates an opposite ordering. With lambdas, the above example is pretty readable. Consider that x -> g(f(x)) could be written Z.fuse(f, g) and retain a consistent order regardless of the syntax to “plug together” in standard code.
  3. “Just works” inference — Z techniques are optimized for a wide 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 , and you’ll be able (if you think it’s appropriate) to capture with var.

Z can handle complex combinations

Consider some common code you might see for a regular expression:

private boolean isLocalHost(endpoint) {
return Pattern.compile("https?://localhost(:\\d+)?(/\\S*)?")
.matcher(endpoint)
.matches();
}

Maybe you’d go a bit farther and have the String or Pattern as a static final constant. Maybe you’d even provide them through dependency injection. It’s fairly common incantation that I’ve seen organized in many ways. (Sometimes there are even multiple functions involved!)

Personally I like taking turning something like this into a Predicate :

Predicate<String> isLocalHost = (String endpoint) ->
Pattern.compile("https?://localhost(:\\d+)?(/\\S*)?")
.matcher(endpoint)
.matches();

Now I can pass not just the pattern around, but the function itself. But I’d argue the code is still a bit wonky.

Let’s try with Z:

Predicate<String> isLocalHost = Z.with("https?://localhost(:\\d+)?(/\\S*)?")
.fusingFunction(Pattern::compile)
.fusing(Pattern::matcher)
.fuse(Matcher::matches);

Here we’re:

  1. Taking a String
  2. Calling a static Pattern::compile method as Function<String, Pattern> — we use a fusingFunction method instead of fusing to select a single-argument function among a few overloaded definitions
  3. Calling the instance method Pattern::matcher as BiFunction<Pattern, String, Matcher>
  4. Calling the instance method Matcher::matches as Predicate<Matcher>

Now that might all sound a little complex. In fact, it is pretty complex given that it’s 3~4 lines of code. That complexity exists in all three versions though. I’d like to propose that the Z version is not only a legitimate way to solve the problem, it’s a really nice way to represent it.

Some might be scared that the Z version abstracts away too much by hiding the “point.” (The point being theendpoint variable from non-Z versions above) We’re piping the results of each function into the first arguments of each next function up until we get to the BiFunction that introduces a new argument to take. It’s not clear from the Z code where points are introduced without looking at the functions, but also, it doesn’t have to be clear.

Abstracting away some of the detail gives us an advantage that now we know at a glance that the predicate definition using Z is giving us some kind of true/false test based on a string, and if we want to understand the implementation we can look at Pattern and Matcher methods.

In the non-Z versions it’s not clear that Matcher is involved, or that the chained functions extend across two classes. This is not too bad for the familiar regular expression classes, but in more complex code it’s common to have no clue which classes are exposing which methods without booting up an IDE (or something equivalently over-powered) and enduring a few seconds or minutes of indexing just to finally hover over it and see what the heck it is.

To recap: Z lets you perform sophisticated combinations with:

  • Static functions
  • Instance methods
  • Selection among many kinds of overloaded functions
  • Explicit

Z is Just a Few Techniques

The following assumes some familiarity with java.util.function classes.

Technique 1: Fusion

Z.fuse(fn1, fn2)

Fusion is the bread and butter of Z. It allows you to take any function and pipe its result as the first argument of the next function. (As long as types and such match up, of course — this is still Java!)

Fusion is detailed in earlier sections, but in general the idea is to let you represent and pass around logic as data.

Some simple fusion concepts:

  • Function<A, B> + Function<B, C> = Function<A, C>
  • IntToLongFunction + LongToDoubleFunction = IntToDoubleFunction
  • Supplier<A> + BiConsumer<A, B> = Consumer<B>

When there are more arguments added, the fusion result will be a “curried” version of the result.

A fusion concept with a curried result:

                  BiFunction<A, B, LINK>
+ BiFunction<LINK, C, D>
----------------------------------------
Function<A, Function<B, Function<C, D>>>

A more concrete example fusing some lambdas:

var concatThreeStrings = Z.fuse( 
(String a, String b) -> a.concat(b),
(String prev, String c) -> prev.concat(c)
);var result = concatThreeStrings.apply("a").apply("b").apply("c");assertEquals("abc", result);

Technique 2: Fission

Z.split(fn)

This takes a multi-argument function and splits it into a “curried” form.

BiFunction<A, B, C> = Function<A, Function<B, C>>

A more concrete example:

TriFunction<LocalDate, LocalTime, ZoneId, ZonedDateTime>
_createZdt = ZonedDateTime::of;
var createZdt = Z.split(_createZdt);ZonedDateTime myTime =
createZdt.apply(myLocalDate)
.apply(myLocalTime)
.apply(myZoneId);

The big benefit of curried functions like this is that it gives you a way to pass around partially-applied representations of a function. If you’ve ever seen code that keeps calling the same function with a bunch of unchanged parameters and just one changing, you might have needed a partially applied function.

Fission (split) works with functions up to 12 arguments. (Gosh I hope you don’t have anything nearly that big.)

Technique 3: Assimilation

Z.assimilate2(curriedFn)

This is the opposite of Fission, taking in a curried function and returning a multi-args version.

Function<A, Function<B, C>> = BiFunction<A, B, C>

Due to limitations in Java generics, (specifically “erasure” rules) a numeric specifier is required for the number of arguments. Z.assimilate2(...) results in a BiFunction, Z.assimilate3(...) results in a TriFunction, etc.

Like Fission (split), assimilation works with up to 12 arguments.

Technique 4: Absorption

Z.absorb(fn1, fn2)

Absorption is like fusion, except that it works when there is no result from the first function.

Consumer<A> + Supplier<B> = Function<A, B>

It’s a little more evil than fusion, so it got a little more evil of a name — it hides some heavily-implied side effects. The rationale is that sometimes the world itself is evil, and you do need to model that in code.

Technique 5: Super Fusion

Z.with(fn1).fusing(fn2).[…].fuse(fnN)

This is fusion, but at an arbitrary depth. This is here for when you want to represent lots of logic (please keep it simple!) as a single function.

You can start with:

  1. A first function
  2. An object — it’s the same as starting with a Supplier, and will be wrapped in a Supplier to ensure laziness
  3. A class — it’s the same as starting with the next function, but can be useful when you need to just specify the starting type without too much hullabaloo.

Note: Super fusion is implemented, yay! But as of Z version 1.0.0, not every variety of function combination is implemented in the API.

Try it Out!

Well, that’s a whirlwind tour. Thanks for sticking with me! If this sounds like fun, take a look at the project or take it for a test drive!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
J.R. Hill

J.R. Hill

26 Followers

I’m a programming philologist; I love languages and love learning. I work at AWS on language infrastructure, and mentor/maintain learning tracks at exercism.io.