Language: Java Interop
This guide covers:
How to instantiate Java classes
How to invoke Java methods
How to extend Java classes with proxy
How to implement Java interfaces with reify
How to generate Java classes with gen-class
Other topics related to interop
This guide does not cover how to include Java files in Clojure projects. For that, head to including Java
code in a Clojure project (/articles/cookbooks/cli_build_projects/#including-java-code-in-a-clojure-project)
This work is licensed under a Creative Commons Attribution 3.0 Unported License
([Link] (including images & stylesheets). The source is available
on Github ([Link]
What Version of Clojure Does This Guide Cover?
This guide covers Clojure 1.11.
Overview
Clojure was designed to be a hosted language that directly interoperates with its host platform (JVM, JS,
CLR and so on). Clojure code is compiled to JVM bytecode. For method calls on Java objects, the Clojure
compiler will try to emit the same bytecode javac would produce.
It is possible to implement interfaces, and to extend and generate Java classes in Clojure.
Clojure also provides convenient functions and macros that make consuming Java libraries easier and
often more concise than it would be in Java code.
See also the official Java interop reference ([Link] on [Link].
Imports
Java classes can be referenced either using their fully-qualified names (FQNs) such as [Link]
or be imported in the current Clojure namespace using [Link]/import (or the :import clause
of ns ) and referenced by short names:
[Link] ; ⇒ [Link]
(import [Link])
Date ; ⇒ [Link]
The ns macro supports imports, too:
(ns [Link]
(:import [Link]))
More about the ns macro can be found in the article on Clojure namespaces
(/articles/language/namespaces/).
Dynamic (at runtime) imports are usually only used in the REPL and cases when there are multiple
implementations of a particular protocol/service/feature and it is not possible to tell which one should be
used until run time.
Automatic Imports For [Link].*
Most classes from the [Link] package are automatically imported. For example, you can use
String or Math without explicitly importing them:
(defn http-uri?
[^String uri]
(.startsWith (.toLowerCase uri) "http"))
(Math/round 0.7886)
You can avoid most direct uses of [Link] by using the [Link] namespace. You
can avoid most uses of [Link] by using the [Link] namespace (added in Clojure
1.11).
Clojure is compatible with Java 8 and later, so classes and interfaces added to Java 9 and later are not
automatically imported.
Inner (Nested) Classes
In Java, classes can be nested inside other classes. They are called inner classes and by convention,
separated from their outer class by a dollar sign ( $ ):
(import [Link]$Entry)
Map$Entry ; ⇒ [Link]$Entry
;; this example assumes RabbitMQ Java client is on classpath
(import [Link]$BasicProperties)
AMQP$BasicProperties ; ⇒ [Link]$BasicProperties
Note that if you need to use both a class and one or more of its inner classes, they all need to be imported
separately. As far as JVM is concerned, they are all separate classes, there is no "imports hierarchy".
How to Instantiate Java Classes
Java classes are instantiated using the new special form:
(new [Link]) ; ⇒ #inst "2012-10-09T21:23:57.278-00:00"
However, the Clojure reader provides a bit of syntactic sugar and you are much more likely to see this:
([Link].) ; ⇒ #inst "2012-10-09T21:24:43.878-00:00"
It is possible to use fully qualified names (e.g. [Link] ) or short names with imports:
(import [Link])
(Date.) ; ⇒ #inst "2012-10-09T21:24:27.229-00:00"
An example with constructor arguments:
([Link]. "[Link]
;;⇒ #object[[Link] 0x8bd076a "[Link]
How to Invoke Java Methods
Instance Methods
Instance methods are invoked using the . special form:
(let [d ([Link].)]
(. d getTime)) ; ⇒ 1349819873183
Just like with object instantiation, it is much more common to see an alternative version:
(let [d ([Link].)]
(.getTime d)) ; ⇒ 1349819873183
Static Methods
Static methods can be invoked with the same . special form:
(. Math floor 5.677) ; ⇒ 5.0
or (typically) the sugared version, ClassName/methodName :
(Math/floor 5.677) ; ⇒ 5.0
(Boolean/valueOf "false") ; ⇒ false
(Boolean/valueOf "true") ; ⇒ true
Chained Calls With The Double Dot Form
It is possible to chain method calls using the .. special form:
(.. ([Link].) getTime toString) ; ⇒ "1693344712616"
Multiple Calls On the Same Object
If you need to call several methods on the same (mutable) object, you can use the doto macro:
(doto ([Link].)
(.push 42)
(.push 13)
(.push 7)) ; ⇒ [42 13 7]
;; assume (import [Link])
(let [pt (Point. 0 0)]
(doto pt
(.move 10 0)))
;;⇒ #object[[Link] 0x1084ac45 "[Link][x=10,y=0]"]
(let [pt (Point. 0 0)]
(doto pt
(.move 10 0)
(.translate 0 10)))
;;⇒ #object[[Link] 0x7bc6935c "[Link][x=10,y=10]"]
Each method is called on the original object -- the first argument to doto -- and it returns that same
object as the result.
How to Access Java Fields
Public mutable fields are not common in Java libraries but sometimes you need to access them. It's done
with the same dot special form:
(import [Link])
(let [pt (Point. 0 10)]
(. pt x)) ; ⇒ 0
(let [pt (Point. 0 10)]
(. pt y)) ; ⇒ 10
and just like with instance methods, it is more common to see the following version:
(import [Link])
(let [pt (Point. 0 10)]
(.x pt)) ; ⇒ 0
(let [pt (Point. 0 10)]
(.y pt)) ; ⇒ 10
For compatibility with ClojureScript, the following syntax is also supported for field access:
(import [Link])
(let [pt (Point. 0 10)]
(.-x pt)) ; ⇒ 0
(let [pt (Point. 0 10)]
(.-y pt)) ; ⇒ 10
This is to distinguish between field access and method access.
How to Set Java Fields
To set a public mutable field, use [Link]/set! that takes a field in the dot notation demonstrated
earlier and a new value:
(import [Link])
(let [pt (Point. 0 10)]
(set! (.-y pt) 100)
(.-y pt)) ; ⇒ 100
Fortunately, mutable public fields are rare to meet in the JVM ecosystem so you won't need to do this
often.
How To Work With Enums
Enums (enumeration) type ([Link] values are
accessed the same way as static fields, except on enum classes:
[Link]/MILLISECONDS
;;⇒ #object[[Link] 0x4cc7d00d "MILLISECONDS"]
Determining Classes of Java Objects
To get class of a particular value, pass it to [Link]/class :
(class 1) ; ⇒ [Link]
(class 1.0) ; ⇒ [Link]
(class "docs") ; ⇒ [Link]
(class ([Link]. "[Link] ; ⇒ [Link]
As this example demonstrates, Clojure strings are JVM strings, integer literals are compiled as (boxed)
longs and floating point literals are compiled as (boxed) doubles.
You can also use [Link]/type to return either the class of the Java object, or the :type
metadata if it exists:
(def foo (with-meta [1 2 3] {:type :bar}))
(type foo)
;; ⇒ :bar
(type [1 2 3])
;; ⇒ [Link]
How To Get a Java Class Reference By Name
To obtain a class reference by its string name (fully qualified), use Class/forName via Java interop:
(Class/forName "[Link]") ; ⇒ [Link]
Array Types, Primitives
JVM has what is called primitive types (numerics, chars, booleans) that are not "real" objects. In addition,
array types have pretty obscure internal names.
An array of String , has a name of "[[Link];" . You can construct an array of String
using into-array :
(class (into-array String ["foo" "bar" "baz"]))
;; ⇒ [[Link];
If you need to obtain a reference to an array of primitives, for example longs, pass "[J" to
Class/forName . Below is the full table:
Internal JVM class name Array of ? (type)
short
"[S"
integer
"[I"
long
"[J"
"[F"
float
double
"[D"
byte
"[B"
Internal JVM class name Array of ? (type)
char
"[C"
boolean
"[Z"
For convenience, Clojure has *-array functions for each of the above types that help you create an
array of primitive values:
user=> (char-array [\h \e \l \l \o])
#object["[C" 0x7df60067 "[C@7df60067"]
user=> (apply str *1)
"hello"
If this does not make much sense, don't worry. Just remember to come back to this guide when you need
to extend a protocol for an array of primitives.
Implementing Java Interfaces With reify
It is possible to implement Java interfaces in Clojure. It is typically needed to interact with Java libraries
that take arguments implementing a particular interface.
Interfaces are implemented using the reify special form.
Given the following Java interface:
/* from [Link] package */
public
interface FilenameFilter {
/**
* Tests if a specified file should be included in a file list.
*
* @param dir the directory in which the file was found.
* @param name the name of the file.
* @return <code>true</code> if and only if the name should be
* included in the file list; <code>false</code> otherwise.
*/
boolean accept(File dir, String name);
}
here is how to implement it in Clojure:
;; a FileFilter implementation that accepts everything
(reify [Link]
(accept [this dir name]
true))
reify takes an interface (fully-qualified name or short name) and one or more method implementations
that mimic function definitions without the defn and with this (as in Java, JavaScript or self in Ruby,
Python) reference being the first argument:
(accept [this dir name]
true)
With reify , generally there is no need to add type hints on arguments: Clojure compiler typically will
detect the best matching method (by name and number of arguments).
reify returns a Java class instance. Clojure compiler will generate a class that implements the interface
and instantiate it. To demonstrate that reified objects indeed implement the interface:
(let [ff (reify [Link]
(accept [this dir name]
true))]
(instance? [Link] ff)) ; ⇒ true
reify can be used to implement multiple interfaces at once:
(let [ff (reify [Link]
(accept [this dir name]
true)
[Link]
(accept [this dir]
true))]
(instance? [Link] ff)) ; ⇒ true
reify, Parameter Destructuring and Varargs
reify does not support destructuring or variadic arguments in method signatures. You will not always
get an error from the compiler if you try to use them, but the resulting code will not work the way you
expect. For example:
(reify [Link]
(accept [a & more]
(comment ...)))
This will compile without error but when called, the first argument to accept -- the directory object -- will
be bound to & and the second argument to accept -- the filename string -- will be bound to more .
Example 1
The following example demonstrates how instances created with reify are passed around as regular
Java objects:
(require '[[Link] :as str])
(import [Link])
;; a file filter implementation that keeps only .edn files
(let [ff (reify [Link]
(accept [this dir name]
(str/ends-with? name ".edn")))
dir (File. "/home/sean/oss/[Link]/")]
(into [] (.listFiles dir ff)))
;; ⇒ [#object[[Link] 0x1450131a "/home/sean/oss/[Link]/deps.
reify forms a closure: it will capture locals in its scope. This can be used to make implemented methods
delegate to Clojure functions. The same example, rewritten with delegation:
user> (import [Link])
;; a file filter implementation that keeps only .edn files
(let [f (fn [_dir name]
(str/ends-with? name ".edn"))
ff (reify [Link]
(accept [this dir name]
(f dir name)))
dir (File. "/home/sean/oss/[Link]/")]
(into [] (.listFiles dir ff)))
;; ⇒ [#object[[Link] 0x5d512ddb "/home/sean/oss/[Link]/deps.
We have used [Link]/ends-with? so that no type hints are required: unlike in the "inline"
implementation (above), the Clojure compiler would not be able to infer the types of _dir and name
parameters in the function that does the filtering. When methods are implemented "inline", types can be
inferred from method signatures in the interface.
Extending Java Classes With proxy
proxy is one of two ways to generate instances of anonymous classes in Clojure. proxy takes two
vectors: one listing its superclass and (optional) interfaces, the other listing constructor signatures, as well
as zero or more method implementations. Method implementations are identical to reify except that the
this argument is not necessary.
A very minimalistic example, we instantiate an anonymous class that extends [Link] ,
implements no interfaces, has no explicitly defined constructors and overrides #toString :
(proxy [Object] []
(toString []
"I am an instance of an anonymous class generated via proxy"))
;; ⇒ #object[[Link]$[Link]$ff19274a 0x66bf40e5 "I am an instance of
The Clojure compiler will generate an anonymous class for this proxy and, at runtime, the cost of a
proxy call is the cost of instantiating this class (the class itself is generated just once).
A slightly more complex example where the generated class also implements [Link]
(runnable objects are commonly used with threads and [Link] classes) which defines
one method, run :
;; extends [Link], implements [Link]
(let [runnable (proxy [Object Runnable] []
(toString []
"I am an instance of an anonymous class generated via pr
(run []
(println "Run, proxy, run")))]
(.run runnable)) ; ⇒ nil
;; outputs "Run, proxy, run"
proxy forms a closure: it will capture locals in its scope. This is very often used to create an instance that
delegates to a Clojure function:
(let [f (fn [] (println "Executed from a function"))
obj (proxy [Object Runnable] []
(run []
(f)))]
(.run obj)) ; ⇒ nil
;; outputs "Executed from a function"
TBD: more realistic examples | How to Contribute ([Link]
[Link]#how-to-contribute)
Clojure Functions Implement Runnable and Callable
Note that Clojure functions implement [Link] and [Link]
directly so you can pass functions to methods found in various classes from the [Link]
package.
For example, to run a function in a new thread:
(let [t (Thread. (fn []
(println "I am running in a separate thread")))]
(.start t))
Or submit a function for execution to a thread pool (in JDK terms: an execution service):
(import [[Link] Executors ExecutorService Callable])
(let [^ExecutorService pool (Executors/newFixedThreadPool 16)
^Callable clbl (fn []
(reduce + (range 0 10000)))
task (.submit pool clbl)]
(.get task))
;; ⇒ 49995000
Note that without the ^Callable type, Clojure compiler would not be able to determine which exact
version of the method we intend to invoke, because
[Link]/submit has two versions, one for Runnable and one for
Callable . They work very much the same but return slightly different results ( Callable produces a
value while Runnable always returns nil when executed).
The exception we would get without the type hint is
Syntax error (IllegalArgumentException) compiling . at (REPL:4:29).
More than one matching method found: submit
gen-class and How to Implement Java Classes in
Clojure
Overview
gen-class is a Clojure feature for implementing Java classes in Clojure. It is relatively rarely used
compared to proxy and reify but is needed to implement executable classes (that java runners and
IDEs can use as program entry points).
Unlike proxy and reify , gen-class defines named classes. They can be passed to Java APIs that
expect class references. Classes defined with gen-class can extend base classes, implement any
number of Java interfaces, define any number of constructors and define both instance and static
methods.
AOT
gen-class requires ahead-of-time (AOT) compilation. It means that before using the classes defined
with gen-class , the Clojure compiler needs to produce .class files from gen-class definitions.
Class Definition With [Link]/gen-class
[Link]/gen-class is a macro that uses a DSL for defining class methods, base class,
implemented interfaces and so on.
It takes a number of options:
:name (a symbol): defines generated class name
:extends (a symbol): name of the base class
:implements (a collection): interfaces the class implements
:constructors (a map): constructor signatures
:methods (a collection): lists methods that will be implemented
:init (symbol): defines a function that will be invoked with constructor arguments
:post-init (symbol): defines a function that will be called with a constructed instance as its first
argument
:state (symbol): if supplied, a public final instance field with the given name will be created. Only
makes sense when used with :init . State field value should be an atom or other ref type to allow
state mutation.
:prefix (string, default: "-" ): methods will call functions named as (str prefix method-
name) , e.g. -getName for getName .
:main (boolean): if true , a public static main method will be generated for the class. It will
delegate to a function named main with the prefix ( (str prefix "main") ), -main by default
:exposes (map): if supplied, a map of protected fields names to getter/setter names so the
generated class implementation can access those protected fields
:exposes-methods (map): if supplied, a map of superclass methods names to local method
names so the generated class implementation can call those superclass methods (since the
implementation may contain implementations of those methods that would hide the superclass
methods)
:factory (symbol): if supplied, a name to be used for a (set of) public static factory methods that
will be generated that match the class's constructors
:load-impl-ns (boolean, default true ): if true , the static initializer for the generated class will
load the implementation namespace; if false , the static initializer will not load the implementation
namespace and users would need to load the implementation namespace manually
:impl-ns (symbol, default: the current namespace): if supplied, the namespace to look in for
implementations of the generated class's methods
For more details, see the generated API documentation for gen-class
([Link] the community-contributed
examples ([Link] on [Link], and the official reference for
Class Generation ([Link] on [Link].
gen-class In The ns Macro
gen-class can be used with existing namespaces by adding (:gen-class) to the ns macro. Here is
a "hello, world" example command line app that uses gen-class to generate a class that JVM launcher
( java ) can run:
(ns [Link]
(:gen-class))
(defn -main
[& args]
(println "Hello, World!"))
This will use the name of the namespace for class name and use the namespace for method
implementation (see the :impl-ns option above).
Examples
A medium size example taken from an open source library:
(ns [Link]
(:gen-class :implements [[Link]]
:init init
:state state
:constructors {[[Link] String String] []})
(:require [[Link] :as lhb]
[[Link] :as json])
(:use [[Link]])
(:import [[Link] SchedulerListener SchedulerException Trigger TriggerKey Jo
[[Link] Channel]
[[Link] Date]
[[Link] PublishingSchedulerListener]))
(defn publish
[^PublishingSchedulerListener this payload ^String type]
(let [{ :keys [channel exchange routing-key] } @(.state this)
payload (json/json-str payload)]
(lhb/publish channel exchange routing-key payload :type type)))
(defn -init
[^Channel ch ^String exchange ^String routing-key]
[[] (atom { :channel ch :exchange exchange :routing-key routing-key })])
(defmacro payloadless-publisher
[method-name message-type]
`(defn ~method-name
[this#]
(publish this# (json/json-str {}) ~message-type)))
(payloadless-publisher -schedulerStarted "[Link]")
(payloadless-publisher -schedulerInStandbyMode "[Link]")
(payloadless-publisher -schedulingDataCleared "[Link]")
(payloadless-publisher -schedulerShuttingDown "[Link]")
(defn -schedulerError
[this ^String msg ^SchedulerException cause]
(publish this (json/json-str { :message msg :cause (str cause) }) "[Link]
(defn -jobScheduled
[this ^Trigger trigger]
(publish this (json/json-str { :group (-> trigger .getKey .getGroup) :key (-> t
(defn -jobUnscheduled
[this ^TriggerKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -triggerFinalized
[this ^Trigger trigger]
(publish this (json/json-str { :group (-> trigger .getKey .getGroup) :key (-> t
(defn -triggerPaused
[this ^TriggerKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -triggersPaused
[this ^String trigger-group]
(publish this (json/json-str { :group trigger-group }) "[Link]
(defn -triggerResumed
[this ^TriggerKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -triggersResumed
[this ^String trigger-group]
(publish this (json/json-str { :group trigger-group }) "[Link]
(defn -jobAdded
[this ^JobDetail detail]
(publish this (json/json-str { :job-detail (from-job-data (.getJobDataMap detai
(defn -jobDeleted
[this ^JobKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -jobPaused
[this ^JobKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -jobsPaused
[this ^String job-group]
(publish this (json/json-str { :group job-group }) "[Link]-pause
(defn -jobResumed
[this ^JobKey key]
(publish this (json/json-str { :group (.getGroup key) :key (.getName key) }) "q
(defn -jobsResumed
[this ^String job-group]
(publish this (json/json-str { :group job-group }) "[Link]-resum
Inspecting Class Signatures
When using gen-class for interoperability purposes, sometimes it is necessary to inspect the API of the
class generated by gen-class .
It can be inspected using javap ([Link]
Given the following Clojure namespace:
(ns [Link]
(:gen-class))
(defn -main
[& args]
(println "Hello, World!"))
We can inspect the produced class like so:
# from target/classes, default .class files location used by Leiningen
javap [Link]
will output
public class [Link] {
public static {};
public [Link]();
public [Link] clone();
public int hashCode();
public [Link] toString();
public boolean equals([Link]);
public static void main([Link][]);
}
How To Extend Protocols to Java Classes
Clojure protocols can be extended to any java class (including Clojure's internal types) very easily using
extend :
Using the example of a json library, we can define our goal as getting to the point where the following
works:
(json-encode ([Link]/randomUUID))
First, let's start with the protocol for json encoding an object:
(defprotocol JSONable
(json-encode [obj]))
So, everything that is "JSONable" implements a json-encode method.
Next, let's define a dummy method to do the "encoding" (in this example, it just prints to standard out
instead, it doesn't actually do any json encoding):
(defn encode-fn
[x]
(prn x))
Now, define a method that will encode java objects by calling bean on them, then making each value of
the bean map a string:
(defn encode-java-thing
[obj]
(encode-fn
(into {}
(map (fn [m]
[(key m) (str (val m))])
(bean obj)))))
Let's try it on an example object, a UUID:
(encode-java-thing ([Link]/randomUUID))
;; ⇒ {:mostSignificantBits "-6060053801408705927",
;; :leastSignificantBits "-7978739947533933755",
;; :class "class [Link]"}
The next step is to extend the protocol to the java type, telling clojure which java type to extend, the
protocol to implement and the method to use for the json-encode method:
(extend [Link]
JSONable
{:json-encode encode-java-thing})
Alternatively, you could use the extend-type macro, which actually expands into calls to extend :
(extend-type [Link]
JSONable
(json-encode [obj] (encode-java-thing obj)))
Now we can use json-encode for the object we've extended:
(json-encode ([Link]/randomUUID))
;; ⇒ {:mostSignificantBits "3097485598740136901",
;; :leastSignificantBits "-9000234678473924364",
;; :class "class [Link]"}
You could also write the function inline in the extend block, for example, extending nil to return a
warning string:
(extend nil
JSONable
{:json-encode (fn [x] "x is nil!")})
(json-encode nil)
;; ⇒ "x is nil!"
The encode-java-thing method can also be reused for other Java types we may want to encode:
(extend [Link]
JSONable
{:json-encode encode-java-thing})
(json-encode ([Link]. "[Link]
;; ⇒ {:path "",
;; :protocol "http",
;; :authority "[Link]",
;; :host "[Link]",
;; :ref "",
;; :content "[Link]$HttpInputStream@4eca
;; :class "class [Link]",
;; :defaultPort "80",
;; :port "-1",
;; :query "",
;; :file "",
;; :userInfo ""}
Using Intrinsic Locks ("synchronized") in Clojure
Every object on the JVM has an intrinsic lock (also referred to as monitor lock or simply monitor). While
very rarely necessary, Clojure provides support for operations that acquire intrinsic lock of a mutable Java
object.
This is covered in the Concurrency and Parallelism guide
(/articles/language/concurrency_and_parallelism/#using-intrinsic-locks-synchronized-in-clojure).
Wrapping Up
TBD: How to Contribute ([Link]
Contributors
Michael Klishin michael@[Link] ([Link] (original author) Lee Hinman
lee@[Link] ([Link] gsnewmark gsnewmark@[Link]
([Link]
« Language: Namespaces (/articles/language/namespaces/) || Language: Polymorphism »
(/articles/language/polymorphism/)
Links
About (/articles/about/)
Table of Contents (/articles/content/)
Getting Started (/articles/tutorials/getting_started/)
Introduction to Clojure (/articles/tutorials/introduction/)
Clojure Editors (/articles/tutorials/editors/)
Clojure Community (/articles/ecosystem/community/)
Basic Web Development (/articles/tutorials/basic_web_development/)
Language: Functions (/articles/language/functions/)
Language: [Link] (/articles/language/core_overview/)
Language: Collections and Sequences (/articles/language/collections_and_sequences/)
Language: Namespaces (/articles/language/namespaces/)
Language: Java Interop
Language: Polymorphism (/articles/language/polymorphism/)
Language: Concurrency and Parallelism (/articles/language/concurrency_and_parallelism/)
Language: Macros (/articles/language/macros/)
Language: Laziness (/articles/language/laziness/)
Language: Glossary (/articles/language/glossary/)
Ecosystem: Library Development and Distribution (/articles/ecosystem/libraries_authoring/)
Ecosystem: Web Development (/articles/ecosystem/web_development/)
Ecosystem: Generating Documentation (/articles/ecosystem/generating_documentation/)
Building Projects: [Link] and the Clojure CLI (/articles/cookbooks/cli_build_projects/)
Data Structures (/articles/cookbooks/data_structures/)
Strings (/articles/cookbooks/strings/)
Mathematics with Clojure (/articles/cookbooks/math/)
Date and Time (/articles/cookbooks/date_and_time/)
Working with Files and Directories in Clojure (/articles/cookbooks/files_and_directories/)
Middleware in Clojure (/articles/cookbooks/middleware/)
Parsing XML in Clojure (/articles/cookbooks/parsing_xml_with_zippers/)
Growing a DSL with Clojure (/articles/cookbooks/growing_a_dsl_with_clojure/)
Copyright © 2024 Multiple Authors
Powered by Cryogen ([Link]