Fork me on GitHub
#beginners
<
2019-06-12
>
hiredman00:06:35

the last place I worked use chef, but you don't need to install lein

hiredman00:06:49

build a jar, deploy the jar

Javier Gonzalez-Compte00:06:17

gotcha so no need for jenkins as well to deploy

hiredman00:06:28

oof, well, that is a whole other thing, if you are building a ci box, for whatever reason our "devops" team refused to take care of our ci box at the last job so it wasn't managed using chef, just a shell script

hiredman00:06:53

so I have very little good advice there

Javier Gonzalez-Compte00:06:51

yea i'm in the process of putting our clojure app on a server so looking for best practices since its a new job and the previous place i worked we only used the jar method you said but the devops want to use jenkins and chef

Javier Gonzalez-Compte01:06:16

nvm I was thinking it the wrong way just have jenkins do the lein test, uberjar and deploy the uberjar into the server

Javier Gonzalez-Compte01:06:14

idk why I was thinking I would need lein in the server for the chef recipe to build since the chef recipe is being used just to automate building the server

adrian00:06:27

create a lein uberwar and drop it on a java container

johnjelinek01:06:33

clj.dal=> 
(s/gen (aws/response-spec-key secretsmanager :GetSecretValue))

Execution error (FileNotFoundException) at clj.dal/eval18601 (REPL:1).
Could not locate clojure/test/check/generators__init.class, clojure/test/check/generators.clj or clojure/test/check/generators.cljc on classpath.
class java.io.FileNotFoundException

johnjelinek01:06:50

am I missing an import?

hiredman01:06:44

You are missing a dep on test.check

johnjelinek01:06:38

... but the stuff generated isn't super helpful

johnjelinek01:06:43

thanks though

johnjelinek01:06:30

I have a map: '({:a 1 :b2}) collection .. I want to take the first map and get all the keys ... but what I'm tryin' ain't workin'

Alex Miller (Clojure team)01:06:51

(-> '({:a 1 :b 2}) first keys)

šŸ‘ 4
johnjelinek01:06:30

my uuid-string function looks identical to this one ... does this mean I need to add the APACHE license to my project? https://github.com/cognitect-labs/aws-api/blob/master/src/cognitect/aws/util.clj#L250-L253

seancorfield02:06:31

@johnjelinek I would expect everyone who needs a UUID string has written that exact same function independently (so, no, you only need to keep the license when you copy in code from an OSS project).

johnjelinek02:06:30

so ... it won't look like I copied uuid-string if I rename the function and then I don't have to adopt the license?

seancorfield02:06:17

You pretty much can't write that function any other way.

seancorfield02:06:46

For example, I've never looked at that code and here's what's in our (proprietary) code at work

(defn uuid
  "Return a standard Type 4 (random) UUID."
  ^java.util.UUID []
  (UUID/randomUUID))

(defn uuid-as-string ^String []
  (str (uuid)))

šŸ‘ 4
johnjelinek02:06:10

what gives? my :gen-class from yesterday doesn't work today when I AOT?

johnjelinek02:06:29

clojure -A:aot
Exception in thread "main" Unexpected error macroexpanding clojure.core/gen-class at (clj/core.clj:1:1).
        at clojure.lang.Compiler.macroexpand1(Compiler.java:7018)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:7092)
        at clojure.lang.Compiler.analyze(Compiler.java:6789)
        at clojure.lang.Compiler.analyze(Compiler.java:6745)
        at clojure.lang.Compiler$BodyExpr$Parser.parse(Compiler.java:6118)
        at clojure.lang.Compiler$TryExpr$Parser.parse(Compiler.java:2314)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:7106)
        at clojure.lang.Compiler.analyze(Compiler.java:6789)
        at clojure.lang.Compiler.analyze(Compiler.java:6745)
        at clojure.lang.Compiler$BodyExpr$Parser.parse(Compiler.java:6120)
        at clojure.lang.Compiler$FnMethod.parse(Compiler.java:5467)
        at clojure.lang.Compiler$FnExpr.parse(Compiler.java:4029)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:7104)
        at clojure.lang.Compiler.analyze(Compiler.java:6789)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:7094)
        at clojure.lang.Compiler.analyze(Compiler.java:6789)
        at clojure.lang.Compiler.analyze(Compiler.java:6745)
        at clojure.lang.Compiler$InvokeExpr.parse(Compiler.java:3820)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:7108)
        at clojure.lang.Compiler.analyze(Compiler.java:6789)
        at clojure.lang.Compiler.analyze(Compiler.java:6745)
        at clojure.lang.Compiler.compile1(Compiler.java:7725)
        at ...
Caused by: java.lang.ClassNotFoundException: com.amazonaws.services.lambda.runtime.RequestStreamHandler
        at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:466)

johnjelinek02:06:44

I've confirmed the dep is still there:

{:deps {com.amazonaws/aws-lambda-java-events {:mvn/version "2.2.6"}}}

johnjelinek02:06:01

and I see the classes that sit adjacent to the interface in the repl

johnjelinek02:06:30

code:

(ns clj.core
  (:gen-class
   :implements [com.amazonaws.services.lambda.runtime.RequestStreamHandler])
  (:require [clojure.data.json :as json]
            [clojure.string :as s]
            [ :as io]
            [clojure.pprint :refer [pprint]]))

(defn handle-event [event]
  (pprint event)
  {:who-done-it (get-in event [:records 0 :request-parameters :source-ip-address])
   :bucket-owner (get-in event [:records 0 :s3 :bucket :owner-identity :principal-id])})

(defn- key->keyword [key-string]
  (-> key-string
      (s/replace #"([a-z])([A-Z])" "$1-$2")
      (s/replace #"([A-Z]+)([A-Z])" "$1-$2")
      (s/lower-case)
      (keyword)))

(defn -handleRequest [this is os context]
  (let [w (io/writer os)]
    (-> (json/read (io/reader is) :key-fn key->keyword)
        (handle-event)
        (json/write w))
    (.flush w)))

johnjelinek02:06:40

here's my deps.edn:

{:deps {org.clojure/clojure {:mvn/version "1.10.0"}
        org.clojure/data.json {:mvn/version "0.2.6"}
        com.amazonaws/aws-lambda-java-events {:mvn/version "2.2.6"}
        com.cognitect.aws/api {:mvn/version "0.8.305"}
        com.cognitect.aws/endpoints {:mvn/version "1.1.11.565"}
        com.cognitect.aws/secretsmanager {:mvn/version "707.2.405.0"}
        http-kit {:mvn/version "2.3.0"}
       ;  org.clojure/test.check {:mvn/version "0.10.0-alpha4"}
        }
 :aliases {:pack {:extra-deps {pack/pack.alpha {:git/url ""
                                                :sha "81b9e47d992b17aa3e3af1a47aed1f0287ebe9b8"}}
                  :main-opts ["-m"]}
           :aot {:extra-paths ["classes"]
                 :main-opts ["-e" "(compile,'clj.core)"]}}}

johnjelinek02:06:59

# tree src/
src/
ā””ā”€ā”€ clj
    ā”œā”€ā”€ core.clj
    ā”œā”€ā”€ dal.clj
    ā””ā”€ā”€ util.clj

johnjelinek02:06:08

maybe it doesn't like the new files I introduced into the namespace

johnjelinek02:06:04

nope, took those files out and no change

seancorfield02:06:05

You're missing at least one dep...

seancorfield02:06:09

(! 513)-> clj -Sdeps '{:deps {com.amazonaws/aws-lambda-java-events {:mvn/version "2.2.6"} com.amazonaws/aws-lambda-java-core {:mvn/version "1.2.0"}}}'
Downloading: com/amazonaws/aws-lambda-java-core/1.2.0/aws-lambda-java-core-1.2.0.pom from 
Downloading: com/amazonaws/aws-lambda-java-core/1.2.0/aws-lambda-java-core-1.2.0.jar from 
Clojure 1.10.1
user=> (import 'com.amazonaws.services.lambda.runtime.RequestStreamHandler )
com.amazonaws.services.lambda.runtime.RequestStreamHandler
user=> 

seancorfield02:06:31

You need aws-lambda-java-core for RequestStreamHandler

seancorfield02:06:03

(I tried with just aws-lambda-java-events and got the same error you saw)

johnjelinek02:06:40

I'm not sure how I would've figured that out

johnjelinek03:06:07

@seancorfield: thanks for your help. Worked great

seancorfield03:06:42

I searched for that full class name. Looked in the Github repo where it showed up. That listed for libraries to depend on. Events, Core, and two logging adapter libraries.

seancorfield03:06:50

You actually posted the link to that interface -- in aws-lambda-java-core

šŸ˜® 4
seancorfield04:06:55

@deleted-user CSP -- Communicating Sequential Processes -- is the background to read for this.

seancorfield04:06:18

Yeah, Go comes from CSP as well.

seancorfield04:06:39

Yeah, I learned it via Occam in the early 80's.

seancorfield04:06:49

A lot of solid production code is built on core.async...

seancorfield04:06:25

I wouldn't worry too much about futures, given what you can already do šŸ™‚

seancorfield05:06:34

BEAM is an interesting environment. I took a workshop back at an FP conference years ago and was impressed by what Erlang can do. But I've been on the JVM for... 22 years at this point... so I both love it and hate it and I've learned to live with its limitations in order to take advantage of everything else.

seancorfield05:06:23

I've never felt the loss of that -- in 22 years on the JVM. Make of that what you will.

didibus05:06:52

Hum.. interesting aside, Java actually used to have only green threads

didibus05:06:05

Like in Java 1 or some old version like that

didibus05:06:35

But it later moved to OS threads, because they are better for true parallelism and thus performance

didibus05:06:54

And I guess convenient

didibus05:06:12

Shows how the world goes in circle

didibus05:06:30

That said, I think you're missing the point of CSP

seancorfield06:06:55

OK. I mean, CSP was the fundamental stuff I studied at uni in my postgrad days back in the very early 80's, so I don't think I was missing the point of it šŸ™‚

didibus06:06:03

Ya, I meant Nate. Also, I had misunderstood the statement. So disregard. I thought we were wondering why the channel parks on put/take

seancorfield06:06:22

Okay, fair enough. Context is everything šŸ™‚

didibus05:06:59

Or I don't know about CSP, but the point of the "go" macro

didibus05:06:08

Blocking is a feature

didibus05:06:32

Its how you synchronize producers and consumers and coordinate the concurrent processes

didibus05:06:12

From my understanding, you have to go to great lengths of additional complexity in Actor models to perform such coordination

didibus05:06:34

And I think that was one reason Rich Hickey went with CSP over Actors

didibus05:06:04

Also, isn't Erlang a preemptive scheduler? Are you sure its threads are lightweight?

didibus06:06:04

Oh. Sorry, I thought you meant parking, or blocking the go-process

didibus06:06:43

Ya, unfortunately, Clojure can't prevent the actual thread running the process from being accidentally blocked

didibus06:06:53

So you just have to know what you're doing

didibus06:06:41

Is project loom implementing a preemptive scheduler?

didibus06:06:42

So, that's exactly what core.async does

didibus06:06:06

I don't think the issue has anything to do with that, but all to do with blocking IO

didibus06:06:24

Does erlang have blocking io?

didibus06:06:50

If so, it would also block one of the threads in its pool. Meaning there is one less thread to run the processes on.

didibus06:06:34

So unless Project Loom can magically rewrite calls to Blocking IO or other blocking operations, it would suffer the same fate.

didibus06:06:14

Well... except core.async isn't preemptive. So your go block won't park on its own

orestis06:06:47

Erlang has its own IO implementations so that you can do IO without blocking the underlying OS thread.

didibus06:06:44

Ya, so, that's really the difference here (minus preemption)

didibus06:06:08

The issue is just Java legacy blocking operations

didibus06:06:18

And how so many things rely on them

didibus06:06:37

When using core.async go block, you just need to make sure you're only using non blocking IO

orestis06:06:58

Thatā€™s the appeal of the Loom - it aims to make the legacy IO stuff just work (tm)

didibus06:06:17

Okay, that's where it gets interesting

didibus06:06:42

I guess a virtual layer that handles the callback for you

orestis06:06:24

That, I donā€™t remember. But the videos that they have put out so far are really interesting and full of the nitty gritty.

orestis06:06:19

> The implementation of the networking APIs in the http://java.net and java.nio.channels packages have as been updated so that fibers doing blocking I/O operations park, rather than block in a system call, when a socket is not ready for I/O. When a socket is not ready for I/O it is registered with a background multiplexer thread. The fiber is then unpacked when the socket is ready for I/O. These same blocking I/O are also updated to support cancellation. If a fiber is cancelled while in a blocking I/O operation then it will abort with an IOException.

didibus06:06:21

Hum... still requires the use of nio then

didibus06:06:52

But I guess ya, they can just update all IO code and have a if called by Fiber.. then park instead of blocking

didibus06:06:13

Ya, that be neat

orestis06:06:49

Itā€™s a huge undertaking but they seem to be chipping at it and making good progress.

orestis06:06:27

Regarding blocking IO, isnā€™t it true that most file access IO is blocking at the OS level and itā€™s hard to overcome that? Thatā€™s what Iā€™ve been hearing for years. So if you make socket calls parking you get huge practical value.

didibus06:06:13

I had gone down a big rabbit whole about this a while ago

didibus06:06:25

It seems only Windows can do non blocking file IO

seancorfield06:06:40

Yay for Windows? šŸ™‚

didibus06:06:02

Ya, it is really advanced in its async IO

didibus06:06:08

In this case

didibus06:06:17

You can actually hand off a callback to the OS and register a user thread with it. And on interrupt from the peripheral, it'll switch to your registered user thread, hand of execution to your callback and pass you the IO result

didibus06:06:29

So it's truly push all the way down

didibus06:06:34

Where as linux only really supports multiplexed io

seancorfield06:06:13

Does this low-level stuff really matter tho' in the grand scale of things? (serious question)

didibus06:06:22

But it provides an API called like Asyncio which abstract it away, but it's still multiplexed under the hood

seancorfield06:06:36

I mean, we build production scale stuff on Windows, Linux... without worrying about this...?

didibus06:06:50

Well, if you wanted to go from say 10k requests/s to 100 000k maybe šŸ˜‹

didibus06:06:03

I.mean, probably not, probably the gain are minor in performance but wtv

seancorfield06:06:09

But how many people need to do that?

seancorfield06:06:43

I think the vast majority "don't care". Regular JVM stuff is "fast enough", right?

didibus06:06:53

Ya, I'd agree

seancorfield06:06:18

I worry that we go down rabbit holes in the name of (unnecessary) performance.

didibus06:06:37

That's why I'm with you, for all practical purpose, core.async solves all the use cases I have. When I need it, I'm just careful not to block

seancorfield06:06:41

Google, Facebook, Twitter... they care but most of us don't.

seancorfield06:06:03

(and I'm not even sure they care really, in all of these cases)

didibus06:06:43

I doubt they even care. All this is about scaling single host concurrency, and for most things, especially big tech, adding a few more hosts is way cheaper then the maintenance cost of more complex software

didibus06:06:01

I actually think startup care more

seancorfield06:06:05

I see some people's "obsession" with async I/O and I'm like "really?". I get asked about it with clojure.java.jdbc and next.jdbc for example...

gklijs06:06:45

They often have the assumption it's faster.. I don't really get it, especially not with Java. You can easily do multiple things in parallel in different threads if you need to.

seancorfield06:06:09

Yeah, and in the real world they'd be surprised how often it isn't faster to do stuff in parallel. Tuning is hard.

gklijs06:06:46

True.. recently did some benchmarks with rust where the same obsession lives. Because they decoupled getting data from Kafka, and processing it async, it was putting unnecessary stress on Kafka, making the performance worse..

didibus06:06:13

Shops trying to run everything on like 1 box

didibus06:06:22

Cause they can't afford more servers

seancorfield06:06:48

Async JDBC is horribly non-portable. Why would anyone inflict that pain upon themselves?

didibus06:06:59

I have no experience with it. Is that just an issue with async JDBC kinda just sucking? Or the async-ness makes it inherently less portable?

seancorfield06:06:01

Only some DBs have drivers that support async.

seancorfield06:06:12

(and each of them are different)

seancorfield06:06:45

So when folks ask me to support async in clojure.java.jdbc or next.jdbc I'm like, yeah, send me a PR for portable async šŸ™‚

seancorfield06:06:46

PostgreSQL has a fairly well-established async driver I believe... and some (many?) PG users tend to assume the world is like PG šŸ™‚

didibus06:06:36

Hum... I mean. I can imagine how in 10 years, everything will just have moved to async IO, and it'll just be the new io standard

didibus06:06:12

And everything might be a little faster.

seancorfield06:06:12

Yup, and in ten years, I'll be happy to release jdbc.async šŸ™‚

didibus06:06:21

Haha šŸ‘Œ

didibus06:06:28

I've actually never used async IO in Java and/or Clojure yet teehi

didibus06:06:03

I wonder if the APIs for it are simple

didibus06:06:55

I'm hoping it's just like takes a callback which is a callable

didibus06:06:28

And you could just therefore give it a Clojure fn which puts to the channel when called back

didibus06:06:12

Actually that's the part I can't quite imagine for Loom

didibus06:06:11

Could it park and like... block the return, hum. I guess it could. So you'd basically be doing a blocking call, but the thread is a lightweight one. Ya that's nice

didibus06:06:38

I guess it could be leveraged by http servers and what not

seancorfield06:06:13

But, really, these are implementation details in the grand scale of things...

sveri06:06:10

async io just sounds like another horrible trap for hard to reproduce, non deterministic, bugs

didibus06:06:12

Ya, I mean, Java can already scale. So all it could further do is save you a few extra hosts

seancorfield06:06:21

If you look at this from a higher level, you can't tell how a lot of the low-level stuff actually operates, and that's a good thing.

seancorfield06:06:59

We should be focusing on higher-level stuff, not lower-level stuff.

šŸ‘ 4
didibus06:06:15

That's true.

didibus06:06:44

I think that would be the defining factor for Loom as well. If its invisible, then great. You'll save some memory and shave of a few milliseconds in context switches without having to do anything. But if it's like a whole buy in, redesign everything, etc. Then meh

āœ”ļø 4
didibus06:06:36

I wonder how many people use go blocks in Clojure (not ClojureScript). I've used thread, but never had a real need for go.

seancorfield06:06:47

We have a few things that are very heavy on async and rely on core.async

seancorfield06:06:19

But out of 80k+ lines it's not much

didibus06:06:03

Cool! I guess I did maybe have a use case for it recently. Had to process a millions+ csv file and make a request to an API for each row.

didibus06:06:19

And then aggregate back their results

seancorfield06:06:07

And that's probably not a good use case for it

didibus06:06:09

Used an executor for it. But I guess async IO and go could have worked

didibus06:06:39

Well.. actually no lll

seancorfield06:06:40

Frankly an executor is better

didibus06:06:59

Because its not like I can call the API with a million concurrent requests

didibus07:06:08

The fleet maybe handles a few hundreds max concurrent, of which other prod systems call into. That's the thing with real life, you always need throttling, back offs, circuit breakers, etc.

didibus07:06:33

Seriously, Executors are great. There's one for every use case too. That with just futures take you really far already

didibus07:06:18

I'm always sad when beginners just jump on core.async. And don't start with future, executor and pmap

Ahmed Hassan10:06:45

Where do I start from while trying to learn these?

Ahmed Hassan10:06:23

I think you meant java.util.concurrent stuff should be use prior to using core.async.

didibus19:06:23

I mean that core.async is a pretty complicated tool, which shines only for particular advanced scenarios

didibus19:06:42

And yes, learning future, promise with normal threads, agents, pmap and the java.uitl.concurrent stuff should happen first in my opinion

didibus19:06:55

They will be easier and take you quite far on their own

Ahmed Hassan21:06:30

@U0K064KQV Thanks. What are examples of particular advanced cases?

didibus22:06:58

There's two scenarios in my mind. The first one is when you want to wait on a number of things to complete, which could complete in any order, and want to process them as they complete.

didibus22:06:14

The second one is when you have a producer/consumer scenario

didibus22:06:36

Where you have multiple producers and multiple consumers

didibus22:06:11

But in either case, it can be possible to do it with futures and executors as well. So you need to first make sure that doing it with those would either not be as fast as you want, like limit the concurrency you actually need (very rare that you need a lot of concurrency), or because core.async would actually result in simpler code for the use case.

didibus22:06:10

That's why some people say: "use core.async when you want to trade maintainability for extreme concurrency"

Ahmed Hassan05:06:46

And does it shine in browser? There's Promesa library for promises too. Which works with both Clojure and ClojureScript.

didibus05:06:07

Oh,yes. I'm talking about Clojure JVM here

didibus05:06:00

In browser and NodeJs, you have a lot less options for concurrency, so core.async is more helpful more often there

didibus05:06:02

One challenge I've had with Promesa is that it's not using native ES6 promises.

didibus05:06:05

Hum... but you know, it seems they changed to native promise now

didibus05:06:35

That said, I'm not an expert on ClojureScript and don't use it actively. So I'm the wrong person to ask for it

didibus05:06:29

But I think in general core.async is more useful in it, and it's also less tricky to use, because of the single threaded nature of JS and the fact that most IO is non-blocking

Ahmed Hassan05:06:45

Question arising in my mind is where one should choose Promesa vs core.async. Kind of best scenarios and tradeoffs in ClojureScript and browser for both libraries.

didibus05:06:24

So, I think in general you should choose Promesa

didibus05:06:37

Or even just use built in promises with JS interop

didibus05:06:00

That's my intuition.

didibus05:06:40

The basic difference between core.async and Promesa isnthe underlying primitive

didibus05:06:55

Promesa uses promise, and core.async uses channel

didibus05:06:27

A promise is like a variable, its single element only. Where a channel is like a sequence, it's a stream of elements

didibus05:06:52

So core.async shines in scenarios where you have streams of concurrent things.

didibus05:06:14

And promise shines when you have a few one off tasks to run concurrently which only return 1 thing and are done

didibus05:06:20

Does that make sense?

didibus05:06:47

In Java though, executors can also be used for processing streams concurrently. And are often a simpler tool for that. But there is no such thing in ClojureScript. Similarly, core.async in ClojureScript has less gotchas.

Ahmed Hassan05:06:24

Great. @U0K064KQV Thanks. Now, I think it's better to start from Promesa and upgrade to core.async if needed.

didibus05:06:39

Yes, I believe so

didibus05:06:39

But in Clojure it's best to start with futures and promise from Clojure

didibus05:06:10

And then you can try promesa after

didibus07:06:29

What's an example use case you use go blocks for?

didibus07:06:26

@deleted-user By the way, you should also have a look at https://clojureverse.org/t/cloroutine-v1/3300 At this point, you might know more about the whole space of coroutines vs fibers vs green threads vs cps, etc. But apparently core.async aren't full generalized coroutines and cloroutine implements a full genral coroutine in Clojure.

didibus08:06:33

@deleted-user And for completeness since this is the beginner channel. In ClojureScript land, when targeting NodeJS, you don't have to worry about accidentally blocking the go-blocks, since Node only supports async non-blocking IO. Some people do prefer NodeJS over the JVM as their backend runtime and use ClojureScript for that reason instead.

clojurites_1912:06:03

Hi , My map is like {2004 [{:id 1, :name water, :attribute_name Color, :attribute_value blue} ,{:id 2, :name water, :attribute_name formula, :attribute_value H2O} ,{:id 3, :name water, :attribute_name State, :attribute_value liquid}] } i want to create following map like this {2004 [{:Color blue, :formula H2O ,:State liquid}]} Please guys help me

snurppa12:06:40

towards something like this?

(->>
  [{:id 1, :name "water", :attribute_name "Color", :attribute_value "blue"}
  ,{:id 2, :name "water",  :attribute_name "formula", :attribute_value "H2O"}
  ,{:id 3, :name "water",  :attribute_name "State", :attribute_value "liquid"}]
  (map #(hash-map (:attribute_name %) (:attribute_value %)))
  (into {}))
=> {"Color" "blue", "formula" "H2O", "State" "liquid"}

clojurites_1912:06:27

It worked .... thanks in ton šŸ™‚

Francisco12:06:09

Hello, has anyone used Cucumber with Clojure? If so, which command do I have to use to run a tagged Test Case?

admay13:06:40

Hey, does anyone have any resources on how to include, import, and wrap homegrown Java classes in Clojure projects? I'm going to start working on wrapping an object model that a colleague wrote in Clojure. The classes are pretty involved, they're used to model CME market data for live feeds, so I'm looking for some resources on how to import the jar from the Java project in my Clojure project and how to properly implement a wrapper so that we can work with it more idiomatically on the Clojure side. Any resources would be a huge help! Thank you! Edit: I'm reading through the following things for a primer as well 1. https://clojure.org/reference/java_interop 2. http://clojure-doc.org/articles/language/interop.html 3. https://www.braveclojure.com/java/ 4. https://www.reddit.com/r/Clojure/comments/aqy19n/questions_about_wrapping_a_java_library/

orestis14:06:31

If you can get the jar in the class path (differs based on your build tool, I guess?) then youā€™re off to the races, everything should work with the Java interop syntax.

orestis14:06:23

As for more idiomatic, most Clojure apis would like to deal with maps, collections and seqs - so are if you can write some wrappers that do this kind of translation.

johnj18:06:33

is there a macro to aesthetically improve this (foo value (:bar (baz x y))) ?

admay18:06:44

What's wrong with that?

johnj18:06:33

nothing, wanted to play with -> ->> and cousins šŸ˜‰

4
johnj18:06:51

I actually prefer the nested one showed above but almost every clojure I see in the wild is obsessed with -> ->>

noisesmith18:06:15

the straightforward translation via arrows is (-> x (bar y) (:bar) (->> (foo value)))

zane18:06:42

Or maybe with as->.

noisesmith18:06:16

or alternately (->> y (bar x) (:bar) (foo value)) - it really depends on what x and y are and what they mean

zane18:06:31

Speaking for myself, -> or ->> in the context of some other threading operator gives me the heebie jeebies.

noisesmith18:06:51

the other threading macros are designed to be nested under ->

johnj18:06:33

is this specific to -> or applies to ->> too?

noisesmith18:06:50

it's specific to ->, because all the other arrow macros take a value as the first arg

noisesmith18:06:01

so any other arrow macro can be used as a child of ->

johnj18:06:37

got it, thanks!

zane18:06:40

I think the fact that that doesn't work in reverse is part of the reason why I avoid doing it at all.

noisesmith19:06:19

but it never needs to work in reverse - with -> as the parent, all you need to do is fall out the end of another form, and you're back in a position where all the other threading types are available

noisesmith19:06:10

all that's required is that -> be used at the top with the other threading types inside it

zane19:06:15

I think I might need to see an example to understand this last bit.

noisesmith19:06:27

because of how arrow macros turn nesting into sequential forms, there's never a need for -> to be inside ->>

(quux (bar baz (dog foo cat)))

;; becomes

(-> foo
    (dog cat)
    (->> (bar baz))
    (quux))

noisesmith19:06:33

it might be helpful to try to come up with a case where it looks like -> needs to be inside ->> - in every case there's a trivial way to just put ->> inside ->, and fall out the end of ->>, instead

zane18:06:16

Oh yeah? That's interesting.

noisesmith18:06:20

that's why as-> takes a value first then a binding name for example

noisesmith18:06:56

and the semantics are simple - if -> is the parent, you just transition from the -> rule to the threading of the child threading macro

noisesmith18:06:59

line breaks help

johnj18:06:22

x and y are single values. For small compositions stuff I guess its personal preference but seem handy for much bigger comps

noisesmith18:06:26

@lockdown- everything is a value - what I mean is in (bar x y) are you conceptually doing something to x that passes forward, or are you doing something to y that passes forward, or is bar something that combines x and y as "peers"

johnj18:06:53

yeah, the latter

johnj18:06:21

x and y end with the call to bar

johnj18:06:33

is this specific to -> or applies to ->> too?

Dave Goodchild18:06:45

So are threading macros akin to the pipe operator in Elixir and easy to overuse?

johnj18:06:26

there is also comp which I like but don't see it used much

noisesmith18:06:34

@lockdown- in that case (->> (bar x y) (:bar) (foo value)) is how I'd do it, since neither x or y is conceptually carried forward to the next step

noisesmith18:06:01

@buddhamagnet they are not an operator, they are a syntax transform

noisesmith18:06:18

now that I look at the elixir docs yes - they directly do what |> does in elixir, perhaps a difference being that they are strictly a syntactic transform that happens before compilation, so they can do things that don't make syntactic sense

noisesmith18:06:50

user=> (->> (+ x y) (let [x 11 y 31]))
42

Dave Goodchild18:06:09

thanks for the clarification

noisesmith18:06:31

x and y are being used "illegally" here because they are syntactically outside the block that defines them, but it works because the form is rearranged before evaluation to something that is valid

noisesmith18:06:38

I don't think elixir allows this

noisesmith18:06:56

(also don't use code like that, it's to demonstrate that it's a syntax transform :D)

noisesmith18:06:24

they can be overused, but usually (when combined with good formatting) they make code clearer

Suni Masuno20:06:52

I have a string iso8601 date, and wanna add or subtract from the year ending up with an otherwise identical string. What's the best path there? I'm worried parse/format could get caught in funny formatting problems. String manip makes me feel guilty. Is there already a function for that somewhere? I see clj-time.core and clj-time.format in a few places... so maybe I should go learn those?

hiredman20:06:25

if it is really iso8601, the format is standard and well supported, it should be pretty straightforward

Suni Masuno20:06:43

I don't know if I've ever seen dates straightforward... -_- But yeah, ok, how would you do that?

deactivateduser20:06:38

I would unequivocally recommend clj-time - itā€™s great.

ghadi20:06:58

I would anti recommend clj-time because it's based on a deprecated library (Joda time)

ā˜ļø 4
seancorfield20:06:26

I'll second that and I'm (one of) clj-time's maintainer! šŸ™‚

hiredman20:06:46

user=> (str (.plusYears (java.time.ZonedDateTime/parse "2019-06-12T20:18:17.744-04:00" java.time.format.DateTimeFormatter/ISO_OFFSET_DATE_TIME) 5))
"2024-06-12T20:18:17.744-04:00"
user=>

ghadi20:06:58

^^^yes do that

hiredman20:06:11

the new time stuff built into java 7+ (if I recall, maybe 8+?) is very good

šŸ’Æ 8
seancorfield20:06:26

I'll second that and I'm (one of) clj-time's maintainer! šŸ™‚

deactivateduser20:06:22

@seancorfield have you considered re-implementing clj-time in terms of Javaā€™s built-in time classes?

deactivateduser20:06:48

Requiring beginners to learn Java interop simply to handle basic data types (i.e. dates / times) seems a bit naff.

seancorfield20:06:01

There was a very long discussion thread about that in an issue over several years -- and the consensus was that it would not be possible without breaking backward compatibility.

seancorfield20:06:25

clj-time's readme suggests looking at clojure.java-time if you want syntactic sugar.

seancorfield20:06:00

There's also a new cljc-based wrapper for Java Time that has a compatible JS implementation inside it. I have not looked at that yet.

deactivateduser20:06:03

Nice - was not aware of that until now.

seancorfield20:06:08

(the discussion took nearly two years!)

deactivateduser20:06:14

I wonder if clojars supports a deprecated flag, at the library level? That would be quite handy for situations like this.

seancorfield20:06:15

It does not, unfortunately.

seancorfield20:06:08

(although I guess updating the project's name/description which would end up in pom.xml and therefore display on Clojars might be a good idea)

deactivateduser21:06:23

Yeah - shame there isnā€™t something that build tools (`lein`, tools.deps, etc.) can consume.

seancorfield21:06:32

Yes, thank you! Launched recently at Clojure/North.

didibus22:06:01

By the way, beginners should learn Java interop, and they should do so relatively early on.

deactivateduser00:06:38

Sure, but I donā€™t think itā€™s controversial to state that being required to learn Java interop before learning about basic data types in Clojure is indeed too early.

deactivateduser00:06:11

Though perhaps one counter argument is that because the JVMā€™s date/time data types have been so fscked for so long, beginners should go through that hazing ritual as early as possibleā€¦ (Iā€™m not sure I buy that argument mind you, given how much Clojure sanitises the JVMā€™s behaviour in other areas)

didibus04:06:16

Ya, I'm not saying that's the first thing you want to learn. But you should learn interop early on. Clojure's date data type for example is java.util.Date

(type #inst "2019-01-01")
;> java.util.Date

didibus04:06:42

If you look at the Clojure standard library, you'll see that there are many omissions and holes for what would count as complete. These were not accidental. Whatever Java provided which was good enough and didn't need improving was left to Java interop, on purpose

didibus04:06:04

If you look at the rationale for Clojure you'll see: > because I wanted A Lisp for Functional Programming *symbiotic with an established Platform*

didibus04:06:09

Emphasis is mine

didibus04:06:35

That symbiosis is key to Clojure and rooted in its design choices

didibus04:06:57

As a beginner, or someone coming to Clojure, you have to be onboard with this.

didibus04:06:32

If you reject it, and reject Java and the JVM platform, Clojure will just be extra painful and will seem odd, incomplete, not batteries included.

didibus04:06:17

Also read the Languages And Platforms section

deactivateduser15:06:41

FWIW I no longer consider myself a beginner (7 years with Clojure) - just here to help other beginners out.

deactivateduser15:06:46

And Java for a bit over a decade before that, so well aware of the strengths and weaknesses of the underlying platform. I just donā€™t think that arguments defending Clojureā€™s uneven sanitisation of the JVM hold much water (beyond the usual realities of finite time).

didibus03:06:43

How do you interpret the rationale? I always interpreted it as not trying to abstract away the JVM, but be symbiotic with it, as an explicit design goal.

didibus03:06:13

So, basically accepting that trying to not be leaky is too problematic. And accepting the leaky abstraction, but in a controlled and purposeful way was better

didibus03:06:28

Specifically this part: Language built for platform vs language ported-to platform Many new languages still take 'Language as platform' approach When ported, have platform-on-platform issues Memory management, type-system, threading issues Library duplication If original language based on C, some extension libraries written in C donā€™t come over

didibus03:06:17

If Clojure had it's own date time library in core, it seems it would fall prey to the "library duplication"

deactivateduser17:06:41

Weā€™re talking about fundamental data types here, and I donā€™t think itā€™s appropriate to require beginners to detour via Java interop if theyā€™re at that basic level of learning about the language.

deactivateduser17:06:00

And itā€™s not like Clojure doesnā€™t already sanitise other basic Java data types (e.g. numerics, data structures).

didibus18:06:02

Ya that's fair. I was talking in a broader sense.

didibus22:06:28

Java interop is not like an esoteric thing that you almost never reach for. It's one of the main component of Clojure

didibus22:06:21

Wrappers over it should only be used when they add additional value. Like make it considerably easier to use, provide additional functionality on top, etc.

didibus22:06:44

Or if you need a common abstraction between dialects like Clojure and ClojureScript

johnj22:06:29

Or learn Java and interop will come naturally šŸ˜‰

Alex Sumner22:06:17

This is my first post here, so hello all. Talking of Java interop. I was looking at the examples from the Java Interop chapter of Programming Clojure and playing around with them. I came across something I don't understand. One example given is the following:

edwaraco22:06:22

Hi šŸ‘‹ I'm trying to use the following lib https://github.com/mbuczko/embodie in a sample project. However, I got an error about:

1. Unhandled java.io.FileNotFoundException
   Could not locate embodie/core__init.class, embodie/core.clj or
   embodie/core.cljc on classpath.
My project.clj contains the following info:
(defproject hello_world_embody "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url ""
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url ""}
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [defunkt/embodie "1.0.0"]]
  :repl-options {:init-ns hello-world-embody.core}
  :main ^:skip-aot hello-world-embody.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})
Someone knows what my problem is? Thanks!

noisesmith22:06:21

did you restart your repl after adding the dependency?

edwaraco22:06:28

In addition, I got the same error when I ran lein run

noisesmith22:06:48

I can reproduce the error in a fresh project, it seems like they broke their build(?)

noisesmith22:06:07

I just looked in the 1.0.0 jar - it contains no clojure code or class files, and the pom file describing its deps pulls in no embodie projects

noisesmith22:06:21

they need to fix their release, they built it wrong

edwaraco22:06:15

Thanks for the help!

noisesmith22:06:34

lein caches your deps into $HOME/.m2/repository/ by default, and you can look up the jar files there. A good editor can open a jar file as if it were a directory - they are just zip files with some extra rules (fixed trivial typos)

šŸ‘€ 4
edwaraco15:06:36

@noisesmith Thank you. Sincerely I didn't keep in mind to look up the jar content That's a good idea šŸ™‚

Alex Sumner22:06:16

trying again, haven't got the hang of entering new lines in Slack... This is my first post here, so hello all. Talking of Java interop. I was looking at the examples from the Java Interop chapter of Programming Clojure and playing around with them. I came across something I don't understand. One example given is along the lines of the following: (defn sax_parse [source handler] (.. SAXParserFactory newInstance newSAXParser (parse (InputSource. (StringReader. source)) handler))) that works if called with an XML string as source and something extending org.xml.sax.helpers.DefaultHandler as handler. If I expand the .. member access threading macro I get: (defn sax_parse [source handler] (. (. (. SAXParserFactory newInstance) newSAXParser) (parse (InputSource. (StringReader. source)) handler))) which also works. But if I try to rewrite the last line using the -> thread-first macro then whether I try: (defn sax_parse [source handler] (.. SAXParserFactory newInstance newSAXParser (-> source StringReader. InputSource. (parse handler)))) or: (defn sax_parse [source handler] (. (. (. SAXParserFactory newInstance) newSAXParser) (-> source StringReader. InputSource. (parse handler)))) then it doesn't work. The defn can't be evaluated, I get Syntax error (ClassNotFoundException) compiling at (test.clj:16:3). StringReader. It seems to be taking StringReader. as a class name. Can anyone tell me why (please)?

lilactown22:06:45

try wrapping it in parens

lilactown22:06:57

(-> source (StringReader.) etc.

noisesmith22:06:14

yeah, StringReader. without parens will not read properly

Alex Sumner22:06:47

OK, thanks, but if I try: (defn sax_parse [source handler] (. (. (. SAXParserFactory newInstance) newSAXParser) (-> source (StringReader.) InputSource. (parse handler)))) I get a different error: Syntax error (IllegalArgumentException) compiling new at (test.clj:17:17). No matching ctor found for class java.io.StringReader

noisesmith22:06:48

or you can use (new StringReader) which is what (StringReader.) expands to when read

Alex Sumner22:06:38

What's confusing me is that (macroexpand '(-> source StringReader. InputSource. (parse handler)))

Alex Sumner22:06:50

(macroexpand '(-> source StringReader. InputSource. (parse handler)))gives

noisesmith22:06:39

also this would be much easier to read with sugared . forms - so (.newSAXParser (SAXParserFactory/newInstance) ...)`

Alex Sumner23:06:16

<sorry keep forgetting about shit enter> What's confusing me is that (macroexpand '(-> source StringReader. InputSource. (parse handler))) gives (parse (InputSource. (StringReader. source)) handler) which is what worked in the first place?

noisesmith23:06:18

yes, that's actually weird - that should work even without the parens actually

noisesmith23:06:29

are you sure you are evaluating that code in the right environment?

Alex Sumner23:06:28

I think so, everything else works. I'm just using C-c C-e in Emacs CIDER

seancorfield23:06:44

@alex.sumner you might want to look at Slack's formatting with triple-backticks so you get

code that looks like this

Alex Sumner23:06:10

Yes, that would be better! Thanks.

seancorfield23:06:29

And under Preferences > Advanced, check the option that pressing enter inside triple-backticks does not send the message. That way you can type three backticks, press enter, type lots of code with newlines, then three more backticks (and then enter will send the message).

Alex Sumner23:06:36

OK, so like this? `

seancorfield23:06:56

You need that preferences change too.

ghadi23:06:12

` to start

ghadi23:06:17

and ` to end

ghadi23:06:07

triple-backslash CODE CODE CODE triple-backslash

noisesmith23:06:03

you can also use control-enter to make a new line without sending a message

Alex Sumner23:06:51

OK, now we're getting somewhere
Still don't know about the problem with ->
but getting somewhere with Slack!

šŸŽ‰ 4
seancorfield23:06:18

I think in

(defn sax_parse [source handler]
 (. (. (. SAXParserFactory newInstance) newSAXParser)
    (-> source (StringReader.) InputSource. (parse handler))))
that first line is a problem. I think
(.newSAXParser (SAXParserFactory/newInstance))
makes part of it more readable but you still have that outer (. which would seem to be trying to process the whole (-> ...) form as a method call?

noisesmith23:06:52

oh yeah, that is an issue

seancorfield23:06:07

Oh, .parse is a method on the parser created?

ghadi23:06:24

the issue is the latter

ghadi23:06:10

(defn sax_parse [source handler]
 (. (. (. SAXParserFactory newInstance) newSAXParser)
    (-> source (StringReader.) InputSource. (parse handler))))
parse handler is in the innermost form accidentally. but anyways this is impossible to read and would never appear in practice

seancorfield23:06:00

(defn sax-parse [source handler]
  (.parse (.newSAXParser (SAXParserFactory/newInstance))
          (-> source (StringReader.) (InputSource.))
          handler))
Is that what you're looking for?

seancorfield23:06:20

(with kebab-case name instead of snake_case BTW for idiomatic Clojure)

Alex Sumner23:06:38

What I actually wanted to try was

(defn sax_parse [source handler]
  (.. SAXParserFactory newInstance newSAXParser
      (-> source StringReader. InputSource. (parse handler))))

ghadi23:06:47

(let [factory (SAXParserFactory/newInstance)
      parser (.newParser factory)
.....

āž• 4
ghadi23:06:12

naming things will bring some clarity, too

seancorfield23:06:54

Mixing .. and -> can be pretty confusing to read (and, as we see here, often confusing to write).

ghadi23:06:02

(defn sax-parse [source handler]
  (let [factory (SAXParserFactory/newInstance)
        parser (.newParser factory)
        input (-> source (StringReader.) (InputSource.))]
    (.parse parser input handler)))

ghadi23:06:19

(whatever the method signature is for parse, I can't remember)

ghadi23:06:05

.. is good for method chains in Java:

Foo.getOrder().getLineItem(2).getPrice()

(.. Foo getOrder (getLineItem 2) getPrice)

Alex Sumner23:06:12

OK, thanks. I am still worried that

(A (B))
works but
(A (C))
does not, when
(macroexpand '(C))
evaluates to
(B)
Where A is either
.. SAXParserFactory newInstance newSAXParser
or its macroexpansion `

Alex Sumner23:06:29

. (. (. SAXParserFactory newInstance) newSAXParser)

ghadi23:06:46

(hit up arrow to edit the last thing you wrote, btw)

Alex Sumner23:06:42

B is

parse (InputSource. (StringReader. source)) handler
and C is
-> source StringReader. InputSource. (parse handler)

ghadi23:06:55

(-> source StringReader. InputSource. (parse handler))
doesn't work because the last form becomes:
(parse PREVIOUS handler)

ghadi23:06:13

where you need

ghadi23:06:20

(.parse PREVIOUS handler)

ghadi23:06:25

note the .

ghadi23:06:42

(assuming that parse is a java method)

noisesmith23:06:06

this is a difference between .. and other forms

Alex Sumner23:06:12

Yes parse is a method

ghadi23:06:37

so without the leading . it thinks it's either a clojure function or a local in a let-binding

seancorfield23:06:10

@ghadi But he has an outer (. parser (-> ,,,)) form in the above

ghadi23:06:22

SAXParser.parse takes an input and a handler, so if you want to translate this to the "fancy style" https://clojurians.slack.com/archives/C053AK3F9/p1560381542398400

ghadi23:06:29

it would become:

ghadi23:06:34

(defn sax-parse
  [source handler]
  (let [input (-> source (StringReader.) (InputSource.))]
    (.. (SAXParserFactory/newInstance)
        (newParser)   ;;  no parens necessary here, but I'm including them
        (parse input handler))))

ghadi23:06:57

sorry, edited a typo

Alex Sumner23:06:25

OK thanks. That looks better. I am still confused as to why

(defn sax_parse [source handler]
 (.. SAXParserFactory newInstance newSAXParser
     (parse (InputSource. (StringReader. source)) handler)))
does work however, and it does (and it is basically what's in the Programming Clojure example)

noisesmith23:06:19

I think it could have to do with .. being expanded before -> ?

noisesmith23:06:56

if you think about it - .. can't be clever about the transform it does, if it finds a form starting with -> it would need to find a method named -> right?

ghadi23:06:46

there are two differences between that and what I pasted: 1) the first argument to parse (the input thingy) is inlined

ghadi23:06:36

2) (.. SAXParserFactory newInstance) is equivalent to (. SAXParserFactory newInstance) which is equivalent to (SAXParserFactory/newInstance)

ghadi23:06:31

in practice I never see the first two styles for calling a static method -- always (Class/staticmethod)

seancorfield23:06:55

If it helps

user=> (macroexpand-all '(.. SAXParserFactory newInstance newSAXParser (-> source StringReader. InputSource. (parse handler))))
(. (. (. SAXParserFactory newInstance) newSAXParser) (parse (new InputSource (new StringReader source)) handler))
user=> (macroexpand '(.. SAXParserFactory newInstance newSAXParser (-> source StringReader. InputSource. (parse handler))))
(. (. (. SAXParserFactory newInstance) newSAXParser) (-> source StringReader. InputSource. (parse handler)))
user=> 
I think the . special form is treating the (-> ,,,) form as a literal form to turn into a method call?

noisesmith23:06:44

right, that's my theory as well - it can't be clever about expanding its method forms, because it doesn't expect them to be expandable, it expects method names to turn into method invocations

seancorfield23:06:43

I wish there was a way to ask the compiler what it turns (. object (-> arg-1 (method arg-2))) into...

ghadi23:06:14

but the middle style (. Class methodName) is the "final" macroexpansion that the compile operates on

ghadi23:06:26

user=> (macroexpand '(java.util.Objects/nonNull a b c))
(. java.util.Objects nonNull a b c)

noisesmith23:06:02

I think this is an issue too

user=> (macroexpand '(.. foo (-> x (bar y) (someMethod))))
(. foo (-> x (bar y) (someMethod)))
- I don't expect . to be clever about discovering someMethod

seancorfield23:06:19

user=> (. "hello" (-> "l" (replace "L")))
Execution error (IllegalArgumentException) at user/eval163 (REPL:1).
No matching method __GT_ found taking 2 args for class java.lang.String
user=> (. "hello" (replace "l" "L"))
"heLLo"
user=> 
The . form doesn't know what to do with ->

āž• 4
noisesmith23:06:28

and it definitely shouldn't try to invoke the initial symbol in a form

ghadi23:06:49

(. "hello" (-> "l" (replace "L"))) doesn't trigger macroexpansion

ghadi23:06:08

on the arguments, that is

noisesmith23:06:24

yeah, there's no sane way it could, for the operator position at least

seancorfield23:06:31

So it turns it into (.-> "hello" "l' (replace "L")) which is munged to (.__GT_ "hello" "l" (replace "L"))

seancorfield23:06:42

So that's why you can't mix .. and -> like this.

ghadi23:06:49

i don't think it does yes, misread you Sean

ghadi23:06:40

those are the parsed forms for lists beginning with .

seancorfield23:06:50

Yup. And going back to .. to be more like @alex.sumnerā€™s original code

user=> (.. "HELLO" toLowerCase (-> "l" (replace "L")))
Execution error (IllegalArgumentException) at user/eval170 (REPL:1).
No matching method __GT_ found taking 2 args for class java.lang.String
user=> (.. "HELLO" toLowerCase (replace "l" "L"))
"heLLo"
user=> 

seancorfield23:06:43

(because you get (.__GT_ (.toLowerCase "HELLO") "l" (replace "L")) because -> is munged to __GT_ in JVM code)

ghadi23:06:52

extra dot at the beginning of the last line^ reading skillz no good right now

seancorfield23:06:55

Thank you for the question/puzzle @alex.sumner -- it's been a great learning experience for us old 'uns too!

Alex Sumner23:06:22

OK, thanks all of you! So, to check my understanding, you can't put macros inside

.
forms (and therefore inside
..
forms either) because they won't be expanded? I was working on the basis that if your macro A expands to B then anything with A in it iis equivalent to the same thing with A replaced by B. That's clearly not always the case though.

noisesmith23:06:48

you can, but not in the method position

noisesmith23:06:19

because it intentionally doesn't expand that position - you don't expect it to look up parse and replace it right?

noisesmith23:06:22

it's used as is

noisesmith23:06:41

you can safely use -> to construct args to one of the methods

Alex Sumner23:06:25

OK, thanks again.

noisesmith23:06:26

this all makes me wonder if explicit message passing OO isn't a lot more sane, I'm sure it doesn't optimize as nicely as what the JVM does, but it would make higher order code much simpler to construct

seancorfield23:06:53

Going back to your original code @alex.sumner this should work:

(defn sax-parse [source handler]
  (.. SAXParserFactory newInstance newSAXParser
      (parse (-> source StringReader. InputSource.) handler)))

seancorfield23:06:22

(but I haven't tested it)