Clojure library for enhanced Java interop that helps you make friends with Java's functional interfaces 😍
Java 8 introduced the concept of functional interfaces, and Java's lambdas are designed to work well with them. Java's Stream API, for example, makes heavy use of functional interfaces and if you use it in conjunction with lambdas, the code becomes very concise and easy to read:
IntStream.range(0, 10)
.filter(x -> x % 2 == 0)
.map(x -> x * x)
.forEach(System.out::println)
Unfortunately, Clojure's Java interop is not smart enough to automatically cast a Clojure
function to a functional interface, so you'll have to wrap it in a reify
form yourself:
(.. (IntStream/range 0 10)
(filter (reify IntPredicate (test [_ x] (even? x))))
(map (reify IntUnaryOperator (applyAsInt [_ x] (* x x))))
(forEach (reify IntConsumer (accept [_ x] (println x)))))
This library mitigates that kind of hassle and helps you make friends with functional interfaces.
It infers a matching method from the argument types at compile time, and automatically wrap
function arguments with reify
if necessary. Consequently, you can write concise code as below:
(require '[power-dot.core :as dot])
(dot/.. (IntStream/range 0 10)
(filter even?)
(map #(* % %))
(forEach println))
This library uses the implicit macro argument &env
at its core, so it may not be
compatible with other complicated macros (such as those that do code walking).
Add the following to your project :dependencies
:
The most fundamental operators of the library are power-dot.core/.
and power-dot.core/new
.
They can be used in much the same way as Clojure's counterparts (i.e. the .
and new
special operators),
except that if a function is fed for a parameter where the method or constructor expects
a functional interface, they handle the function as if it were an implementation of
that functional interface.
For example, IntStream#forEach
expects IntConsumer
(which is a functional interface)
as its argument, and you can pass a Clojure function to the method via
the power-dot.core/.
macro:
(require '[power-dot.core :as dot])
(import '[java.util.stream IntStream])
(dot/. (IntStream/range 0 10) (forEach (fn [n] (println n))))
In this case, the dot/.
form will be expanded to something like the following, and
the function successfully acts like an IntConsumer
:
(let [f (fn [n] (println n))]
(. (IntStream/range 0 10) (forEach (reify IntConsumer (accept [_ x] (f x))))))
dot/new
works almost the same as dot/.
, except that it invokes constructors
instead of ordinary methods:
(import '[java.util.concurrent.atomic LongAccumulator])
(def acc (dot/new LongAccumulator + 0))
;; This expands to:
;; (def acc
;; (new LongAccumulator
;; (reify java.util.function.LongBinaryOperator
;; (applyAsLong [this x y]
;; (+ x y))
;; 0))
You can pass a function in any form as long as the Clojure compiler statically tells that
it's a function. So, the following forms are all valid, besides the above one with fn
:
(dot/. (IntStream/range 0 10) (forEach #(println %)))
(dot/. (IntStream/range 0 10) (forEach println))
(let [p println]
(dot/. (IntStream/range 0 10) (forEach p)))
NOTE: If dot/.
tries to resolve the method from the provided arguments
ending up in failure, it will fall back to Clojure's ordinary .
and may emit
a reflection warning. So, it's highly recommended to do (set! *warn-on-reflection* true)
to be able to notice method resolution failure.
If a method resolution failed, due to e.g. the method's being overloaded for more than one functional interface types, you may need to explicitly specify the desired type. To do so, just add a type hint of the target type to the argument:
;; To coerce `println` to `IntConsumer`, add a type hint ^IntConsumer to `println`
;; (this example code actually doesn't need it, though).
(dot/. (IntStream/range 0 10) (forEach ^IntConsumer println))
power-dot
also has its own version of the ..
macro.
Analogous to the ..
macro defined in clojure.core
, the dot/..
form expands to
a chain of dot/.
invocations. It's useful to use in the context of
"fluent interface" (or method chaining) with heavy use of functional interfaces:
(dot/.. (ArrayList. [1 2 3 4 5])
(stream)
(filter odd?)
(forEach println))
This form will be expanded to:
(dot/. (dot/. (dot/. (ArrayList. [1 2 3 4 5])
(stream)))
(filter odd?)
(forEach println))
If the Clojure compiler cannot infer the type of an argument statically, you may need to
explicitly tell power-dot
that the argument is a function.
To do so, use dot/as-fn
for that argument:
;; For example, this doesn't work because the Clojure compiler cannot tell the type of
;; the return value from `(partial println "val:")`
(dot/.. (IntStream/range 0 5)
(forEach (partial println "val:")))
;; Execution error (ClassCastException) at user/eval332 (REPL:1).
;; class clojure.core$partial$fn__5839 cannot be cast to class java.util.function.IntConsumer
;; But, this one does work!!
(dot/. (IntStream/range 0 5)
(forEach (dot/as-fn (partial println "val:"))))
;; val: 0
;; val: 1
;; val: 2
;; val: 3
;; val: 4
dot/as-fn
is also useful if you want to use a keyword (or any other types other than functions
that implement clojure.lang.IFn
) as a function:
(dot/.. (ArrayList. [{:name "Rich"} {:name "Stu"} {:name "Alex"}])
(stream)
(map (dot/as-fn :name))
(forEach println))
;; Rich
;; Stu
;; Alex
For those who prefer Clojure's "sugared" interop syntax, that is,
;; instance method invocation
(.method object ...)
;; class method invocation
(Klass/method ...)
;; constructor invocation
(Klass. ...)
over the simplest (. obj-or-class method ...)
and (new Klass ...)
forms,
power-dot
provides convenient reader syntax: #dot/$
.
By prefixing the #dot/$
reader tag to those sugared interop forms, you can get them to work
exactly the same as the dot/.
or dot/new
form. For example:
#dot/$(.method obj ...)
;; expands to (dot/. obj method ...)
#dot/$(Klass/method ...)
;; expands to (dot/. Klass method ...)
#dot/$(Klass. ...)
;; expands to (dot/new Klass ...)
Note that #dot/$
cannot be used with doto
or thread-first macros
(->
, some->
and cond->
). This is because the expansion of #dot/$
is done at read time and it is expanded into a form that is not
compatible with them.
If you want to use the reader syntax in the thread-first context,
you can use #dot/>
instead:
;; This does not work
(-> (IntStream/range 0 5)
#dot/$(.map #(* % %))
(.toArray))
; Syntax error compiling at ...
; Unable to resolve symbol: p1__7844# in this context
;; But this works
(-> (IntStream/range 0 5)
#dot/>(.map #(* % %))
(.toArray))
Copyright © 2020 Shogo Ohta
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.