Fork me on GitHub
#clojure
<
2024-05-22
>
Saket04:05:53

Hey folks, is there a generally accepted guide on idiomatic clojure patterns? I want to understand when and where to use clojure constructs, their anti-patterns etc.

seancorfield05:05:38

https://guide.clojure.style/ is pretty low-level but covers a few idiomatic constructs. There aren't really many "pattern" level guidelines for Clojure... On http://clojure.org there are some cautions in the sequences/collections sections I think. There's Sierra's old series of Clojure Do's and Don'ts which is mostly still applicable: https://stuartsierra.com/tag/dos-and-donts

seancorfield05:05:13

You might find https://clojure.org/guides/faq useful as well?

❤️ 1
craftybones07:05:19

@U1Z5X06NP’s blog has some useful stuff. There’s a talk also : https://www.youtube.com/watch?v=hYSxlN_d2-g&amp;t=1s

❤️ 1
respatialized12:05:04

https://elementsofclojure.com/ I enjoyed reading Elements of Clojure for a more philosophical take on the topic, but it definitely feels like one of those texts that you have to reread periodically to re-grasp its point of view

dvingo14:05:28

another good one, only about when to use which features for polymorphism https://www.amazon.com/Clojure-Polymorphism-Paul-Stadig-ebook/dp/B01LB7NU34

kuzmin_m05:05:27

My colleague have written a very strange code. There are two problems. The compiler interprets a form (xyz/.x obj) just like (.x obj) , and it does the same way inside reify. I.e. the compiler just uses (name symbol) even the symbol is fully qualified. Could the compiler throws on this code?

(ns foo)

(defprotocol Foo
  (x [this]))

(ns bar
  (:require
    [foo]))

(let [f (reify foo/Foo
          (x [_] :ok))]
  [(foo/.x f)  
   (xyz/.x f)]) ;; any ns works
;; => [:ok :ok]

(let [f (reify foo/Foo
          (foo/x [_] :ok))]
  (foo/x f))
;; => :ok

(let [f (reify foo/Foo
          (xyz/x [_] :ok))] ;; any ns works
  (foo/x f))
;; => :ok
Tested on clojure “1.12.0-alpha5” and “1.11.0"

medianjoe08:05:29

The behavior changed substantially in the latest versions of 1.12.0-alpha. Behavior as of 1.12.0-alpha11: defining

(defprotocol Foo (x [self]))
after this...
(reify Foo
  (x [self]
    (not-a-ns/.x self)))
throws with message No such namespace: not-a-ns;
(reify Foo
  (x [self]
     (clojure.core/.x self)))
throws with message No such var: clojure.core/.x; and finally
(let [a-foo (reify Foo
              (x [self]
                (println "hello")
                (Class/.x self)))]
  (x a-foo))
warns-on-reflection that call to method x on java.lang.Class can't be resolved (no such method) and when evaluating looks up both calls to .x correctly (in a-foo's class the first time and in Class the second time). However, the error message when Class/.x isn't found at runtime is No matching method x found taking 0 args for class user$eval11210$reify__11211 , which incorrectly makes it look like the method was looked up in a-foo 's class. E: formatting.

clojure 1
phill09:05:33

This will be a huge breaking change for the agencies around the globe (and possibly elsewhere) that have been communicating via messages encoded in nonsense qualifications of dot-form symbols.

😂 1
Noah Bogart12:05:19

that is code that shouldn't work but does by accident and will break in 1.12. i recommend you get your coworker to stop writing code like that

👍 1
😁 1
Alex Miller (Clojure team)13:05:20

The way this “worked” previously is that the dotted form was macroexpanded and the namespace part of the symbol was ignored in that process. There are lots of macros in the wild that are sloppy about quoting method symbols and make things like this (we recently fixed with-open). In 1.12 that symbol namespace is no longer ignored as that is now syntax for a qualified instance method.

Alex Miller (Clojure team)13:05:51

Re “incorrectly makes it look like the method was looked up in a-foo’s class”, I think you are misreading that - it’s trying to invoke instance method x in Class and the the class it’s reporting is that of the target object (self).

Alex Miller (Clojure team)13:05:02

So I think that is correct

Alex Miller (Clojure team)13:05:11

There are really two errors at that point - self is not a Class and Class does not have an X method

Alex Miller (Clojure team)13:05:56

It’s reporting the latter, but maybe the former is more relevant

medianjoe15:05:31

The issue is that the message is that what's actually reported is that the method couldn't be found in the target's class, which is plainly not where the method is looked up in the first place. Take for instance: (Object/.add (java.util.ArrayList.) 1) which reports the rather nonsensical

; Execution error (IllegalArgumentException) at user/eval7745 (REPL:26).
; No matching method add found taking 1 args for class java.util.ArrayList
whereas (java.util.List/.add (java.util.ArrayList.) 1) works fine.

Alex Miller (Clojure team)15:05:23

agreed that this could be better

Alex Miller (Clojure team)16:05:32

we'll log an issue for this and try to get it fixed before 1.12 release

medianjoe16:05:52

What's interesting to note is that due to Java methods effectively only matching by name, the following is a thing:

(load-java
 ["package user; class DefinitelyNotAList { public boolean add(Object x) { return true; } }"])
nil
clj꞉user꞉> 
(doto (java.util.ArrayList.) (user.DefinitelyNotAList/.add 1))
[1]

medianjoe16:05:22

I imagine the semantic we're looking for here is "find method in target class that overrides a matching one in ns class", so isn't the fix as simple as emitting a cast before lookup?

Alex Miller (Clojure team)16:05:34

should already have one

Alex Miller (Clojure team)16:05:10

which version of Clojure are you on?

Alex Miller (Clojure team)16:05:56

In latest, I see:

user=> (doto (java.util.ArrayList.) (user.DefinitelyNotAList/.add 1))
Execution error (ClassCastException) at user/eval7327 (REPL:1).
java.util.ArrayList cannot be cast to user.DefinitelyNotAList

Alex Miller (Clojure team)16:05:45

the error above is in the reporting - it is looking for the add method in the java.util.List class, just reporting the class of the target type

medianjoe16:05:33

I'm on 1.12.11-alpha BTW:

(definterface IDefinitelyNotAList (^boolean add [^Object x]) (^boolean add [^Object x ^Integer y]))
user.IDefinitelyNotAList
cl:user>
(doto (java.util.ArrayList.) (user.IDefinitelyNotAList/.add 1))
; Execution error (ClassCastException) at user/eval7778 (REPL:72).
; java.util.ArrayList cannot be cast to user.IDefinitelyNotAList

clj꞉user꞉> (deftype DefinitelyNotAListAgain [] IDefinitelyNotAList)
user.DefinitelyNotAListAgain
clj꞉user꞉> 
(DefinitelyNotAListAgain/.add (java.util.ArrayList.) 1) 
; Execution error (ClassCastException) at user/eval7789 (REPL:10).
; java.util.ArrayList cannot be cast to user.IDefinitelyNotAList
guess that teaches me not to involve my janky pastime stuff in this

Noah Bogart16:05:39

hey @mikfjaqburcmqtymxb, is load-java just for the example or is this something you've written? And if you wrote it, would you be able to share it?

medianjoe16:05:16

something that I wrote for fun and the first thing that I thought of to make up a class with a .add method which isn't a j.u.List really, I should've defined an interface lol

👍 1
medianjoe16:05:38

oh wow, it's a bug on my part

p-himik16:05:57

@UEENNMX0T Looks very similar to https://github.com/tailrecursion/javastar, so something that like that is already public. :) But it's old and depends on some things you actually don't need to implement such compilation, especially if you wrap the functionality in a class on your own, so you can implement the same thing as what javastar does, in a similar way, in around 50 lines.

medianjoe17:05:02

FWIW, the problem there was apparently caused by my implementation happily compiling a non-`public` class, i.e. this works as intended:

(load-java
 ["package user; 
   public class A { 
     public boolean add(Object x) {
       return true; 
     } 
   }"])
nil
clj꞉user꞉> 
(user.A/.add (java.util.ArrayList.) 1)
; Execution error (ClassCastException) at user/eval5788 (REPL:156).
; java.util.ArrayList cannot be cast to user.A

Noah Bogart17:05:05

You should post load-java some place! it would be cool to compare it with other java loading ideas such as javastar

vemv18:05:58

Anyone can comment on :aot :all vs :aot [my.main.ns] ? Would seem similar for a vanilla-structured project, perhaps :allwould seem nicer to me as it would be more comprehensive and deterministic? i.e. don't leave un-AOTed namespaces. (Please take as a given that I need AOT in production - it's there for reasons)

vemv18:05:19

There's this passage https://github.com/technomancy/leiningen/blob/24fb93936133bd7fc30c393c127e9e69bb5f2392/sample.project.clj#L272-L273 which isn't super specific, I'd also be curious about additional considerations.

hiredman18:05:07

the class files compilation produces are sensitive to the order in which code is loaded, so if the order in which you load code for aot compilation is different from the order in which you load code at runtime you can have problems

hiredman18:05:58

and I believe :aot :all in lein doesn't try to determine dependency orderings between namespaces, so it is very easy to end up with stuff happening in a different order

vemv18:05:29

Interesting What kind of code is sensitive to ordering nuances?

hiredman18:05:37

the corresponding stuff in tools-build builds a dependenecy graph and topo sorts it to determine load order for aot compilaion, but I don't believe that is infallible either

👍 1
hiredman18:05:27

I forget, interopy stuff like protocols and deftypes can get weird, I think there is a call to eval in tools.logging that can go sideways

hiredman18:05:10

a big part of the reason aot compilation is sensitive to ordering doesn't have anything to do with the code itself, but the way clojure.core/compile is implemented

hiredman18:05:41

it does effectively a forced reload of the namespace you pass it, so if you aot compile by (run! compile nses) which nses are not in dependency order you can end up with weird double loading / compilation happening

hiredman18:05:54

a quick dirty solution is to create a dummy file that requires all your namespaces (in any particular order) and load that file with *compile-files* bound to true

hiredman18:05:10

the potential issue with that is because it avoids the forced reload, anything that is already loaded won't get aot compiled, so if you have a user.clj or complicated tooling that loads stuff before you get a chance to load the dummy file, it breaks

1
vemv18:05:28

Thanks! I wouldn't get too AOP (ahead of problems) with AOT - at the moment I just wanted to understand the tradeoffs. Using tools-build would seem a good improvement - I enjoy Lein for many aspects but here I'd enjoy something precise and modern.

phill22:05:24

Since AOT is viral, it's usually sufficient (if you want AOT, that is) to list only your program's main-entrypoint ns for AOT'ing and invoke "lein uberjar" to trigger the compilation, which will naturally get the namespaces in an OK order by virtue of Clojure fulfilling the main ns form by loading the referenced things.

1
Nundrum22:05:39

I'm looking for something to help with talking to Bluetooth devices. First from a Linux desktop, later from a Raspberry Pi. There's clearly no Clojure lib for this. But even looking at Java libs it's very confusing. com.github.hypfvieh/bluez-dbus seems like the best candidate I can find, but I can't find any documentation at all to go with it. TinyB's repo has been archived. Have I missed something?

hiredman22:05:35

if you are only trying to do really simple things with bluetooth devices (like read simple values from BLE services running on a microcontroller), you can get by with shelling out to bluetoothctl

Nundrum22:05:26

I'm hoping to do a bit more than that.

respatialized23:05:56

https://gist.github.com/SingingBush/50c3fba41ec0642a73b842e8d5802e9d kinda surprised something hasn't emerged out of Android but the bluetooth support in Android might be at the OS/Kernel level rather than in Java

respatialized23:05:00

JNI is probably the only way

respatialized23:05:14

there is this, which I'm not sure if you saw already, but it did have a release last year and an example application: https://github.com/weliem/blessed-bluez

hiredman23:05:19

two things: 1. the new java native stuff is way better than jni 2. bluez does everything over dbus so you don't need jni to do bluetooth stuff but likely need it for doing dbus client stuff

hiredman23:05:29

I haven't done it, and when I tried it was with sbcl not clojure, but the wall I ran up against was the native dbus library I was trying to use (in order to interact with bluez over dbus) was sd-bus (systemd's dbus client library) and sd-bus's interface has a lot of preprocessor macros, which are painful(impossible?) to deal with in an ffi context

😬 1
Nundrum00:05:00

@UFTRLDZEW I saw that but hadn't dug in yet because it's not in Maven central. It's in some other repo. And then dinner happened. And now patch night is happening. So ... tomorrow 😄

Nundrum00:05:45

I've seen there might be NodeJS support, so I'm wondering if nbb could do the job. But I haven't worked with Node very much so that'll be a can of worms.

Nundrum00:05:46

It seems like Python has great support for BT (which makes this all the more disappointing) so maybe Basilisp is worth a look. At least I'm familiar with Python.

respatialized12:05:06

you could also try libpython-clj but that's a pretty heavyweight dependency, especially if your eventual goal is to do this on a Raspberry Pi. Dunno if Lua/Fennel might also be an effective solution for the necessary C interop