Read README.md before this file as that is the official introduction to Paguro. This file contains additional information for contributors, or maybe people who are considering opening an issue.
java.util.Comparator, but for hash-based collections.Some of the people I'm listing as contributors may not actually be aware of this project, but I found them inspiring in various ways.
As Norm Zeck pointed out by sending me Ropes: an Alternative to Strings, an RRB-Tree might make a great implementation of both String and StringBuilder. We might want to add a Char8 (UTF-8 Character class pronounced "crate") and make Str8 (UTF-8 String pronounced "straight") a sub-class of RRB-Tree. Just a thought.
The bulk of this project started as a simple question on StackExchange: Why doesn't Java 8 include immutable collections? People's answers were a big help in figuring out what this project should and shouldn't do.
Paul Philips Scala Collections talk was a huge inspiration to me. I watched it over and over and tried to learn from his experience as much as possible.
Greenville Clojure (and Jeff Dik before that): for bearing with my newbie Clojure questions.
John Tollison, Norm Zeck, Mike Greata, Nathan Williams, and Landon Burch: For encouragement, advice, and patiently listening to me drone on about this project ad nauseum.
GreenJUG: for bearing with talks on early versions of this code two years in a row.
Context-sensitive equality: prefer Equator and Comparator to equals(), hashcode() and compareTo() (Daniel Spiewak, Viktor Klang, Rúnar Óli Bjarnason, Hughes Chabot, java.util.TreeSet, java.util.TreeMap)
Everyone whose ideas are collected in this project: I tried to put names in as close as possible to the contributions.
Bodil Stokke for pointing out the EPL/GPL compatibility issue and work-around.
Joshua Bloch for his book, Effective Java.
Rich Hickey for Clojure
Use with filter(...).head() instead
reduceLeft() is like fold without the "u" parameter. I implemented it, but deleted it because it seemed like a very special case of fold that only operated on items of the same type as the original collection. I didn't think it improved readability or ease of use to have both methods. How hard is it to pass a 0 or 1 to fold? It's easy enough to implement if there is a compelling use case where it's significantly better than fold. Otherwise, fewer methods means a simpler interface to learn.
Java 8 has void forEach(Consumer<? super T> action) on both Iterable and Stream that does what
Transformable.forEach() used to do. The old Transformable method overloaded (but did not override)
this method which is problematic for the reasons Josh Bloch gives in his Item 41. Either make
use of the Java 8 void forEach(i -> log(i)) or pass a constant function like
i -> { print(i); return Boolean.TRUE; } to
Transformable<T> filter(Fn1<? super T,Boolean> predicate) instead.
I also implemented interpose(), but took it out because my only use case was to add commas to a list to display it in English and for that, you also need a conjunction, and often a continuation symbol:
a, b, c, or d.
a, b, c, and d.
a,b,c...
None of those are simple uses of interpose.
Paguro tried two alternatives. One was based on the Clojure idea of a sequence: immutable, lazy, and cached. The signature looked something like this:
interface Sequence1<T> {
Option<T> first();
Sequence1<T> rest();
}
Unfortunately, at 30 Million items it was at least 30x slower than using Iterators. Another attempt was a single-shot, but still thread-safe, sequence that wraps each item in an Option. You would check whether that Option is None to see if you've passed the last element or not. This attempt was only about 3x slower than native iterators.
interface Sequence2<T> {
Option<T> next();
}
Ultimately, Transformable took the place of a sequence abstraction in Paguro. It's safe, easy to use, and about 98% as fast as native Java iteration. If you really need to pretend you have a Sequence1, Transformable has take(1) and drop(1) that you can use like first() and rest() in a pinch. That said, everything you could do with Sequence1 you can do faster and just as clearly with Transformable. Presumably, this is why Clojure now has Transducers.
This was more complexity than value.
It was also a little unsafe if you had a map with the same type for keys and values. Like a Map, there was no visual indicator what were keys and what were values. The "tup()" shows the pairs unambiguously.
If you need this, it's not that hard to write your own functions:
public static Map<K,V> map(K k1, V v1) { return map(tup(k1, v1)); }
public static Map<K,V> map(K k1, V v1, K k2, V v2) { return map(tup(k1, v1), tup(k2, v2)); }
If you like brevity, you can even call them m() instead of map().
To be able to write Java at work more like the way I write Clojure without taking any significant performance penalty for doing so. Also, to be able to use the Clojure collections in a type-safe language. I was thinking "Java" but really Scala can take advantage of the type safety improvements as well.
The goals of this project are to make it easy to use Java:
null (Josh Bloch Item 43, also Clojure and Scala)equals(), hashcode() and compareTo() (Daniel Spiewak, Viktor Klang, Rúnar Óli Bjarnason, Hughes Chabot, java.util.TreeSet, java.util.TreeMap)Higher order functions are not just briefer to write and read, they are less to think about.
They are useful abstractions that simplify code and focus your attention on your goals rather than the details of how to accomplish them.
Function chaining: xs.map(x -> x + 1).filter(x -> x > 7).head() defines what you are doing in the simplest possible way while hiding all details about how to iterate through the underlying collection.
The alternative - loops - are bundles of unnecessary complexity.
Loops generally require setting up accumulators, then running a gamut of if, break, and continue statements, like some kind of mad obstacle race that involves as many state changes as possible.
Different kinds of collections require different looping constructs - more complexity.
Looping code is vulnerable to "off-by-one" boundary overflow/underflow, improper initialization, accidental exit, infinite loops, forgetting to update a counter, updating the wrong counter... The list goes on!
None of that has anything to do with why the loop was created in the first place which is to transform the underlying data.
You don't have to write that kind of code any more. If you want to map one set of values according to a given function, say so with xs.map(). Filter? xs.filter(). It's clearer, simpler, and like type safety, it eliminates whole classes of errors.
Clojure works like this, only the syntax makes the evaluation go inside out from the order you read the statements in (hence Clojure's two arrow operators). With method chaining, the evaluation happens in the same order as the methods are written on the page, much like piping commands to one another in shell scripts.
The Xform class is the third one of its kind that I have written. For a single thread, my timings show that its speed is comparable to a for loop. In general, the overhead for using these transformations is minimal or non-existant. In the cases where imutability does cause overhead (and there definitely are) it is generally well worth the clarity, safety, and productivity benefits it provides. If you find a better/faster implementation, please submit your improvements!
Within your own FP-centric world, you will use the Im interfaces and implementations and transform them with the Transformation abstraction. Methods that interact with imperative Java code will take the java.util interfaces and return either the Im- interfaces (or Un- interfaces) as necessary.
In Java, variables declared outside a lambda and used within one must be effectively finial. The Mut
$ claude mcp add Paguro \
-- python -m otcore.mcp_server <graph>