Fork me on GitHub
#clojure
<
2023-09-23
>
jasonjckn00:09:56

has someone devised a way to add ‘middleware’ or ‘advice’ to function definitions in a manner that doesn’t scare you and you’d want to keep it in your non-dev builds. i’m running into a lot of use cases where i’d like to instrument certain functions with telemetry, metrics, logging, extra specs, etc, ideally through a simple piece of metadata ^:with-telemetry , or perhaps a new defn macro. alter-var-root and defadvice both are not very good abstractions for this IMO , i know there are libraries like that.

Nazral00:09:22

For what it's worth, I've done something like what you describe rewriting my own defn inspired by the original one

👍 1
Nazral00:09:06

But it's not polished enough for prod, among other things because it does quite some extra stuff and slows down function execution (not massively, but once you start making lots of call it quickly adds up and becomes noticeable)

jasonjckn00:09:53

yah, exactly, it’s not hard to write /a/ solutions here, but i’m somewhat cautious, i want performance, composability and several characteristics.

jasonjckn00:09:00

i use this in dev builds, very emacs-like https://github.com/thunknyc/richelieu

jasonjckn00:09:50

i think the pluggable ‘middleware’ here should run at macro expansion time of ‘defn’ or a defn-like macro, and alter function definition if applicable

nivekuil00:09:30

not immediately helpful to you, but my more complex programs are more pathom than clojure and its plugin system addresses all these concerns

jasonjckn00:09:26

i really like pathom/eql but i’m stuck on REST and GraphQL at work , due to corporate constraints

😣 1
Noah Bogart01:09:41

We have a defn-with-tracing macro that calls defn normally and then calls alter-var-root to wrap the original function in a binding call that sets some telemetry dynamic vars. Works well for us

👍 1
p-himik04:09:17

A library that I know of, its approach seems reasonable: https://github.com/galdre/morphe

gratitude-thank-you 1
oyakushev06:09:58

Counter-opinion: if you control the code that you want to cover with extra functionality, then nothing beats a macro that wraps the body. Direct, explicit, flexible. You'll appreciate it the moment when you'll need a slightly more complicated behavior (e.g. "write to this metric if successful or to this if failed"). I guess it won't work for adding extra specs, but for everything that pertains to function execution I prefer macros within the body rather than outside.

👍 1
vemv07:09:26

https://github.com/donut-party/hooked is a recent take from a good source. I haven't gotten my hands to it yet, though

p-himik07:09:04

It's just a home-cooked multimethod with a docstring and a schema, fewer than 40 loc.

👀 1
vemv07:09:08

Alternatively, maybe one can use (ab)use Component/Integrant components, or Pedestal-like interceptors to add extra functionality. I'm thinking of Pedestal interceptors most of all (they might be available in other modern frameworks) The idea is that instead of using f as a vanilla function, you inject f into your system / interceptor chain, whatever, then you could transform it at will. Pros: just data Cons: most likely you'd lose some ide features

Hendrik06:09:38

How can I set jvm properties with clojure cli. I run this with java java -DFoo=Bar -jar my.jar . How do I have to inoke clojure? I tried this in several alternatives without success: clojure -M -m namespace.to.main -J-DFoo=Bar What am I missing here?

oyakushev06:09:55

You have to put the property arguments first.

clojure -J-DFoo=Bar -M -m namespace.to.main

Hendrik06:09:16

That did the trick. Thank you very much.

👍 1
dgr17:09:23

I just noticed that defonce does not take a docstring as does def. Is there any particular reason for that, or just a mistaken omission?

Alex Miller (Clojure team)17:09:06

It’s surprisingly complicated, and there is a ticket for it :)

dgr17:09:04

Wow, OK. I’ll go look for it. Inquiring minds want to know and all that. I figured that I couldn’t be the first person to notice. 🙂

seancorfield18:09:10

@U7BEY9U10 You can use metadata to add a docstring:

user=> (defonce ^{:doc "I'm a docstring!"} foo :bar)
#'user/foo
user=> (doc foo)
-------------------------
user/foo
  I'm a docstring!
nil

dgr20:09:03

@U04V70XH6, thanks. I figured that was also possible and was my next step.

grounded_sage20:09:07

What is the easiest way to start a webview with Clojure on desktop?

phronmophobic20:09:45

It really depends on what you mean by "start a webview". You can look at how portal launches a browser: • https://github.com/djblue/portal/blob/master/src/portal/runtime/browser.cljchttps://github.com/djblue/portal/blob/master/src/portal/runtime/jvm/launcher.clj There are also a few options that are typically used for testing like: • selenium • https://github.com/clj-commons/etaoin • playwright.js (accessible via cljs)

grounded_sage20:09:08

Not for testing. For running an app. I'm exploring options outside of the standard Electron/Tauri. I would prefer to stay completely JVM.

phronmophobic20:09:32

Would the full app be running in the browser or would the browser just be a subcomponent?

grounded_sage20:09:50

I'm interested in sub component but right now full app is all I require.

grounded_sage20:09:59

Looks like Portal has figured it out

phronmophobic20:09:34

It's also worth noting that there's not really a difference between driving a web browser for testing or driving a web browser for other reasons.

grounded_sage20:09:22

Ah.. that is a good point

grounded_sage20:09:45

Portal is using Electron?

phronmophobic20:09:20

But yea, I would probably start with looking at what portal is doing.

phronmophobic20:09:35

I think portal has multiple options, one of them being electron. I think the default option is just to launch chrome and have it open a URL pointing to a locally hosted app.

grounded_sage20:09:39

I think for user testing I probably just want to do this. But I still want to figure out a nicer webview launching from java at some point 🙂

phronmophobic20:09:13

If you're willing to put in the work, you can use something like the Chromium Embedded Framework. It's quite a beast, but it's basically what every commercial product that embeds a web browser uses. I wrote some bindings a while back, but the API is very low level at the moment, https://github.com/phronmophobic/clj-cef.

phronmophobic20:09:00

It's quite capable though. Here's an example:

grounded_sage20:09:25

I did see this but got a bit overwhelmed by the code. But alright... this does seem like the best option.

grounded_sage20:09:47

I was already looking at learning some C and JNA for a different webview situation but realised it was a yak shave

phronmophobic20:09:00

Yea, it is a bit intense, but I'm unaware of any alternative.

grounded_sage20:09:53

I've had my eyes on this for a while. Even though it is only partially open. https://ultralig.ht/

phronmophobic20:09:04

I know cljfx has a webview component, but I think it fails to run many modern websites.

grounded_sage20:09:32

It's also not a full web browser. It's a complete strip down to the basics and built up from the bottom focused on performance.

grounded_sage20:09:53

the cljfx one is fairly broken from my experience

grounded_sage20:09:52

That ultra light one is actually the one I was trying to figure out JNA for today.

phronmophobic20:09:05

If you do decide to check out clj-cef or another cef wrapper, I'd be happy to offer advice, but it is quite a slog.

grounded_sage20:09:12

I just want a webview that has it's own context. Why is this so hard 😅

grounded_sage20:09:44

Or at least work in this space

jpmonettas14:09:03

what about a JavaFX webview? I see cljfx mentioned on the thread but not sure if the limitations you are talking about are referring to cljfx or JavaFX webview itself

grounded_sage14:09:11

It’s not that good from my experience

jpmonettas14:09:46

but out of curiosity, why? just in case I have to use it in the future 😛

grounded_sage14:09:28

You can try it with cljfx there is a webview demo. It’s quite a janky experience. Using cljfx/JavaFX just for a webview that I’m not entirely happy with isn’t worth it and I would prefer a better alternative

jpmonettas14:09:54

I just tried with a plain javafx web view and did some googling and opening random pages, even youtube, it didn't look bad, but I probably see what you say, that it felt a little slower, not sure what is that about, shouldn't it just be webkit?

grounded_sage15:09:59

It is apparently WebKit and not far behind in updates. I’m not sure what is happening and what others experience of it is. I’ve also considered CEF but tbh it would be nice to have something simple, small, closer to Clojure and composable.

jasonjckn23:09:19

If you AOT (def x (merge {..} {..})) , does the generated byte code store merged or unmerged value?

hiredman23:09:46

Unmerged

👍 1
hiredman23:09:55

the compiler doesn't know that merge doesn't do side effects, doesn't know that you have redefined what clojure.core/merge is

👍 1
jasonjckn23:09:11

thanks! and presumably (let [x$ (merge ...))] (def x x$)) stores merged value

jasonjckn00:09:37

oh wow that's counter intuitive

jasonjckn00:09:55

(let [x$ (merge ...))] (defn [] x$))
^ this is surely merged no?

hiredman00:09:36

I guess it is not entirely clear to me what you are asking

hiredman00:09:34

But no, the compiler always generates code that runs to produce the effects and values, aot compilation just saves that same code off to the side on disk

jasonjckn00:09:11

if you disassemble the byte code

(let [x$ (+ 1 2)] (defn foo [] x$)
(A) you'd only see the value 3 in the disassembly
(let [x$ (+ 1 2)] (def foo x$)  
(B) what about here ?
(def x (+ 1 2))
(C) what about here ?

jasonjckn00:09:22

A is 3 , B is .. , C is no 3

hiredman00:09:12

I am not at my computer but I am pretty sure you are dead wrong about what you would see in the bytecode for A

seancorfield00:09:43

(the Clojure compiler may evaluate simple constant expressions during compilation?)

hiredman00:09:50

Compiling A will generate at least two classes

jasonjckn00:09:10

@U04V70XH6 hah yah, tricky compiler, pretend they're strings being concatenated

ghadi00:09:14

hiredman is correct about the compiler generating the code for the effects, not their results

hiredman00:09:40

The nature of the first depends on what context the expression is compiled in, but it will have the code for the let

jasonjckn00:09:01

i'm assuming everything is top level , not sure if that's all the context you need

seancorfield00:09:43

@U0J3J79FE top-level expressions are compiled and they are evaluated each time the ns (or .class file) is loaded.

> cat src/jason/example.clj
(ns jason.example
  (:gen-class))

(let [x$ (do (println "x$") (merge {:a 1} {:b 2}))]
  (def x x$))
x$ will be printed when tests run, when the ns is compiled...
> clojure -T:build ci

Running tests in #{"test"}
x$

Testing jason.example-test

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.

Copying source...

Compiling jason.example...
x$

Building JAR...
and then again when the AOT-compiled JAR is executed:
> java -jar target/net.clojars.jason/example-0.1.0-SNAPSHOT.jar
x$

hiredman00:09:35

The compiler doesn't do any constant folding or partial evaluation

hiredman00:09:14

It does very little implicit optimization at all, it is all left to the jvm

jasonjckn00:09:10

there's no world where the class file that corresponds with function 'foo' has anything but 3 inside the byte code So one of these two things must be true, so the class associated with function 'foo' isn't generated at AOT time, or it is but never executed, because then its regenerated when you java -jar

jasonjckn00:09:17

am i tracking at all?

hiredman00:09:37

No, I don't know why you think there is no world

jasonjckn00:09:02

ok, i guess i have to unwind a deep faulty assumption I have

hiredman00:09:30

The name in the let is treated as being closed over by the fn

hiredman00:09:55

So the name in the let is assigned to an instance field on the fns class

👍 1
jasonjckn00:09:51

that makes sense

hiredman00:09:50

The compilation of the let emits code that loads the two numbers on the stack the adds them, then stores them in a method local that corresponds to the let binding, then loads the value from that method local and passes it to the constructor of the fns class

👍 1
seancorfield00:09:38

@U0J3J79FE Here's what javap shows for (part of) your code after AOT compilation:

0: lconst_1
       1: ldc2_w        #12                 // long 2l
       4: invokestatic  #19                 // Method clojure/lang/Numbers.add:(JJ)J
So there's the (+ 1 2) Clojure expression.

gratitude-thank-you 1
jasonjckn00:09:18

This for (A) presuming, awesome, thanks for all the great answers

seancorfield00:09:01

Right, I wanted to confirm what was actually in the bytecode after compilation of a minimal example.

jasonjckn00:09:21

definitely had the wrong mental model there, in retrospect very reasonable

jasonjckn00:09:44

That relegates 'sexp compile time optimization' strictly to defmacro usage, which is a sound model

seancorfield00:09:23

The main thing folks "miss" when trying to form a mental model is that top-level expressions -- even when compiled -- still all run every time that ns is loaded. Which means they run during compilation (which has to load the ns to compile it) and it happens when the (compiled) program is run too.

👍 1