clojure

New to Clojure? Try the #beginners channel. Official docs: https://clojure.org/ Searchable message archives: https://clojurians-log.clojureverse.org/
Yehonathan Sharvit 2021-04-28T07:39:45.106600Z

A question related to using Clojure’s multimethod at scale. Multimethod inlcudes a caching mechanism. In my code I called a multimethod with lots of different values (around 20 millions) and it caused a memory leak. Is there a way to disable multimethod caching or to limit the cache size? Is it advised not to use multi methods when there are too many different calls?

Ben Sless 2021-04-28T07:49:47.106800Z

I'm not sure if it's possible with multimethods but I think https://github.com/camsaul/methodical supports passing a custom cache

p-himik 2021-04-28T07:51:19.107100Z

You can call (.reset multi-fn) on each call. But it's quite a bit strange that, seemingly, MultiFn caches all values that end up in calling the default method.

Ben Sless 2021-04-28T08:00:49.107300Z

Happens here if you're curious https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/MultiFn.java#L161

p-himik 2021-04-28T08:04:18.107900Z

And of course I'm wrong about using reset because it also reset the hierarchy. So a workaround would be to call defmethod again - it resets the cache as well.

Yehonathan Sharvit 2021-04-28T09:36:31.108500Z

My use case is different. The dispatch value is always the same. And the memory leak is not as easy to reproduce as in the case that your reported

p-himik 2021-04-28T09:38:00.108800Z

The dispatch value is the same in my case as well - it's :default.

Yehonathan Sharvit 2021-04-28T10:15:34.109Z

in your case the dispatch value is the by array

p-himik 2021-04-28T10:21:04.109200Z

I guess there are two perspectives. :) > The dispatch value is always the same "Same" as in identical? or equal??

p-himik 2021-04-28T10:31:29.109400Z

Although, judging by the Java code, you would still have to be hitting the :default branch for the issue to surface.

p-himik 2021-04-28T10:34:33.109800Z

Another guess that I can hazard without having a minimal reproducible example is that your objects can be equal? without having their hashes be equal. It would mean wrong hash implementation, and can cause all sorts of problems in general, not just memory leaks when using defmulti.

Yehonathan Sharvit 2021-04-28T12:19:10.110100Z

Actually, my case was exactly the same as yours but not on purpose. It took me lots of time to understand it. Here is my buggy code

(defmulti foo :a)
(defmethod foo :default [_ _] nil)

(foo {} (Math/random))

Yehonathan Sharvit 2021-04-28T12:21:49.110500Z

My intent is to dispatch on the valu associated to :a in the first arg.

(defmulti foo (fn [x y]
                (:a x)))

Yehonathan Sharvit 2021-04-28T12:22:05.110700Z

But what I wrote was equivalent to this:

Yehonathan Sharvit 2021-04-28T12:22:10.110900Z

(defmulti foo (fn [x y]
                (:a x y)))

Yehonathan Sharvit 2021-04-28T12:22:57.111100Z

As a consequence, each time I called foo with a different second arg, I increased the cache of the multi method

👍 2
Yehonathan Sharvit 2021-04-28T12:41:25.112Z

It drove me crazy

Yehonathan Sharvit 2021-04-28T12:41:47.112600Z

Because my code behaved as I expected in term of functionallity

Yehonathan Sharvit 2021-04-28T12:42:30.113400Z

Anyway, I think that the cache size of the multi method should have a limit

borkdude 2021-04-28T12:44:44.115700Z

I want to import some dynamic var from another namespace but I also want to allow users to define a var with the same name, without breaking (it's on them to rename their own vars in order to be able to access the one from the prelude). E.g. my prelude is

(require '[babashka.tasks :refer [*task*]])
but the user's program, which will be appended, may already have a var named *task* and I don't want to crash their program with
(def *task* {})
IllegalStateException
I can work around this using:
(intern *ns* '*task* ...)
but then the *task* var will not hold the value of the dynamic binding of babashka.tasks/*task*. What can I do?

borkdude 2021-04-28T13:02:55.116400Z

I feel it could be useful if clojure had something like a proxy-var, which just proxied everything to another var. This would also solve the potemkin/import-vars stuff.

alexmiller 2021-04-28T13:16:41.117Z

Or you could just not ever do that

😂 1
alexmiller 2021-04-28T13:17:20.117600Z

Somehow I’ve managed to avoid it in 10 years of writing Clojure

borkdude 2021-04-28T13:19:42.119900Z

potemkin/import-vars was just an additional benefit that could be solved at the same time, but not the main issue (FWIW: I have never needed potemkin/import-vars myself, not a big fan, but the problem it addresses could be solved better if clojure supported "forwarding" to other vars). Let me try to explain my use case. This is a config-like program, where I want to be able to expose *task* without breaking existing configs. It is not a typical every day Clojure program, but more a framework/DSL-like setup. Not common.

Aron 2021-04-28T13:21:49.120300Z

There is a logical error here 🙂 And I say this with the utmost respect, hoping you don't mind it. https://en.wikipedia.org/wiki/Denying_the_antecedent

borkdude 2021-04-28T13:23:26.121200Z

I guess I could hack around it by making the dynvar part of clojure.core, but I'm not a fan of that either. https://github.com/clojure/clojure/blob/b1b88dd25373a86e41310a525a21b497799dbbf2/src/jvm/clojure/lang/Namespace.java#L87

bronsa 2021-04-28T13:25:16.123400Z

why do you need to refer *task* instead of b.t/*task* ?

borkdude 2021-04-28T13:25:30.123800Z

Maybe a better way is to make everything opt-in (or namespaced) but that will create some more boilerplate.

vlaaad 2021-04-28T13:25:35.124Z

maybe it’s fair to require user programs to not define *task* for this system? what’s the problem you are trying to solve?

borkdude 2021-04-28T13:26:09.124600Z

> maybe it’s fair to require user programs to not define `*task*` for this system? that is a fair restriction if you can control the future

borkdude 2021-04-28T13:26:50.125300Z

there might already configs around that are using *task* for something else today

bronsa 2021-04-28T13:27:04.125700Z

if they do define it it's on them to use an alias instead of referring

vlaaad 2021-04-28T13:27:21.126400Z

I realised I don’t know enough about the context where this is defined to give advice 😄

bronsa 2021-04-28T13:27:24.126600Z

you can also :rename {*task* *bs-task*}

borkdude 2021-04-28T13:28:45.127600Z

What I've done so far is this:

(when-not (resolve 'clojure)
  ;; we don't use refer so users can override this
  (intern *ns* 'clojure babashka.tasks/clojure))

(when-not (resolve 'shell)
  (intern *ns* 'shell babashka.tasks/shell))
This works for normal functions. clojure and shell are functions users can just access without any additional boilerplate. I want to do the same for a dynamic var.

bronsa 2021-04-28T13:30:15.128Z

not a fan ¯\(ツ)

➕ 1
bronsa 2021-04-28T13:30:19.128200Z

is a prefix that bad?

borkdude 2021-04-28T13:30:43.129Z

yes, in the sense that that prefix might also be already taken by the user

vlaaad 2021-04-28T13:30:58.129600Z

what problem are you trying to solve?

borkdude 2021-04-28T13:31:07.130Z

e.g. I had tasks as the default prefix, but I reverted this because a user already had taken this :)

borkdude 2021-04-28T13:31:25.130400Z

@vlaaad I am making a task runner / Makefile like system where people can define tasks using a DSL

Aron 2021-04-28T13:31:43.130700Z

oh, like gulp and grunt in js? 😄

NoahTheDuke 2021-04-28T13:33:48.131400Z

i think requiring users to change existing code is perfectly reasonable, given the relatively age of babashka's task runner feature

borkdude 2021-04-28T13:37:29.133900Z

@nbtheduke The problem is more general: even when stable, it might introduce more of these things in the future.

borkdude 2021-04-28T13:38:42.134400Z

I guess the problem is similar to [clojure.test :refer :all] and clojure.test introducing new things in the future which breaks certain programs when upgrading clojure

👍 2
borkdude 2021-04-28T13:41:51.134900Z

and if clojure doesn't see this breakage as problematic, maybe I shouldn't either?

borkdude 2021-04-28T13:42:55.135400Z

@vlaaad to summarize: the problem I'm trying to solve is: avoid breakage like with the above example

borkdude 2021-04-28T13:43:54.136200Z

but maybe my programs shouldn't try to be "don't break"-holier than clojure

NoahTheDuke 2021-04-28T13:44:26.137200Z

i realize one of the niceties of babashka is that a lot of stuff is imported by default, but could you make this one explicit?

borkdude 2021-04-28T13:45:26.138100Z

nothing is imported by default, except in the user namespace it pre-defines a couple of aliases to be used on the command line. e.g. (json/generate-string ...)

vlaaad 2021-04-28T13:46:21.138900Z

shouldn’t this also be done with default aliases?

borkdude 2021-04-28T13:46:51.139200Z

@vlaaad what do you mean by this?

vlaaad 2021-04-28T13:47:27.139900Z

instead of referring *task* you can add default alias to babashka.tasks

borkdude 2021-04-28T13:47:59.140400Z

@vlaaad even that was breaking some user's program where the user had already chosen the alias tasks

borkdude 2021-04-28T13:48:38.140900Z

we could just make everything explicit via a required [babashka.tasks :refer [shell clojure *task*]] instead.

borkdude 2021-04-28T13:49:00.141100Z

or :refer :all at the risk of the user

borkdude 2021-04-28T13:56:45.141700Z

I guess we could keep supporting normal fns like previous but additional stuff must be explicitly imported. Perhaps task or current-task could also be a normal fn which derefs *task* which is not uncommon.

tvaughan 2021-04-28T14:06:06.142300Z

Perhaps of interest, https://github.com/apenwarr/redo

borkdude 2021-04-28T14:06:56.142600Z

Did you also comment this in the Mach repo?

borkdude 2021-04-28T14:07:05.142800Z

Then I've probably seen it

Jim Newton 2021-04-28T15:43:50.146500Z

In clojure, fn allows me to define a multi-funciton, i.e. a function with several possible lambda lists. at call-time, the most appropriate function is selected by matching the arguments to the appropriate lambda list. Is the mechanism of selection documented somewhere? For example, certain sequences of lambda lists are not allowed. Case in point [a b] and [a [b c]] are not allowed simultaneously. What else is allowed and not allowed?

(defn foo 
  ([a [b c]] a)
  ([a b] (list a b)))
gives the following error
Syntax error compiling fn* at (clojure-rte:localhost:54179(clj)*:133:23).
Can't have 2 overloads with same arity

borkdude 2021-04-28T15:44:37.148100Z

The logic is pretty simple basic: it's only based on arg count, regardless of destructuring.

borkdude 2021-04-28T15:44:48.148700Z

And there is a rule around varargs.

2021-04-28T15:44:51.149Z

@jimka.issy [a b] and [a [b c]] are both 2 args fns.

Jim Newton 2021-04-28T15:44:53.149100Z

really? only argument count?

2021-04-28T15:45:34.150200Z

yes. arg count only.

Jim Newton 2021-04-28T15:45:48.150600Z

@potetm, yes I understand why this case fails. But is this the only such failing case? that’s my question. according to @borkdude that’s the only selection criteria, the arity

ghadi 2021-04-28T15:46:12.151Z

you should macroexpand the function to see what is happening:

ghadi 2021-04-28T15:46:38.151700Z

(macroexpand '(fn ([a [b c]] a)
                  ([a b] (list a b))))

->

(fn* ([a p__140] (clojure.core/let [[b c] p__140] a)) ([a b] (list a b)))

borkdude 2021-04-28T15:46:41.151800Z

@jimka.issy

user=> (macroexpand '(fn [[a b]]))
(fn* ([p__138] (clojure.core/let [[a b] p__138])))
exactly

ghadi 2021-04-28T15:47:37.152300Z

term of art here is "function with multiple arities"

ghadi 2021-04-28T15:47:53.152600Z

lambda lists

Jim Newton 2021-04-28T15:48:35.152900Z

fn* doesn’t seem to be documented in https://clojuredocs.org/

ghadi 2021-04-28T15:49:05.153300Z

fn* is a special form in the compiler

ghadi 2021-04-28T15:49:15.153700Z

it's the familiar fn minus destructuring

ghadi 2021-04-28T15:49:35.154Z

http://clojuredocs.org is very useful, but non-authoritative

Jim Newton 2021-04-28T15:50:18.154700Z

@ghadi, there seems to be some case about varargs, is the “term of art” really multiple arities even in the case of varargs?

ghadi 2021-04-28T15:50:39.155300Z

varargs is a type of arity

ghadi 2021-04-28T15:51:06.155900Z

you can have fixed arities + a vararg arity (which must be > than the length of the longest fixed arity)

ghadi 2021-04-28T15:51:20.156200Z

[a b] [a b c] [a b c & more]

ghadi 2021-04-28T15:52:07.157500Z

for the varargs arity, under the hood there is a mechanism that rolls up extra args and presents it to the arity as a seq bound to "more"

1
Jim Newton 2021-04-28T15:52:12.157800Z

ahhh, now we are getting some rules. If http://clojuredocs.org is not authoritative, where is the authority?

ghadi 2021-04-28T15:53:31.158Z

https://clojure.org/reference/special_forms#fn

1
Jim Newton 2021-04-28T15:54:54.158900Z

The `IFn` interface defines an `invoke()` function that is overloaded with arity ranging from 0-20. A single fn object can implement one or more invoke methods, and thus be overloaded on arity. One and only one overload can itself be variadic, by specifying the ampersand followed by a single rest-param. Such a variadic entry point, when called with arguments that exceed the positional params, collects them in a seq which is bound to, or destructured by, the rest param. If the supplied args do not exceed the positional params, the rest param will be `nil`. [taken from http://clojure.org]

Jim Newton 2021-04-29T07:56:25.217400Z

indeed it is. and a good description IMO

ghadi 2021-04-28T15:56:50.159400Z

destructuring is orthogonal to any of the above

ghadi 2021-04-28T15:57:17.160100Z

purely macro sugar to tear apart the arguments

2021-04-28T15:57:31.160500Z

it is a copy-paste from http://clojure.org, isn’t it?

Jim Newton 2021-04-28T15:57:34.160800Z

that destructuring is orthogonal makes since, yes, just two feature that work well together

Jim Newton 2021-04-28T15:58:56.161900Z

BTW what is the difference between the following two?

(defn foo 
  ([a b c] a)
  ([^Boolean a b] (list a b)))
vs the following
(defn foo 
  ([a b c] a)
  ([a b] (list a b)))

borkdude 2021-04-28T15:59:21.162300Z

In that example: in practice nothing, since the type hint is only relevant to Java interop here

wotbrew 2021-04-28T15:59:29.162400Z

Doesn't this represent a potential DOS vulnerability that a lot of people might not be aware of? Presumably the cache entries are not weak-interned, so you could use this to consume all available memory by providing many unique inputs that may happen to be used as dispatch vals? I'm sure many large unique inputs will cause pollution of caches all over the place not just multi-methods in many apps but I would still sleep easier if I knew :default matches were not cached.

borkdude 2021-04-28T15:59:47.163100Z

e.g. ^String a (.length a)

Jim Newton 2021-04-28T15:59:56.163400Z

does the compile compile a special branch for the case that a is a Boolean? and another for when it is not a Boolean?

borkdude 2021-04-28T16:00:29.164100Z

no, the type hint is just leveraged when doing interop and in other cases, just not used

✔️ 1
borkdude 2021-04-28T16:00:53.164400Z

there are other hints which can prevent boxed math

Jim Newton 2021-04-28T16:08:17.166Z

someone mentioned above that lambda-list is not a term used in the clojure community. what is the correct word for the vector which specifies the parameters of a function and their semantics wrt position, destructuring, and optionality?

borkdude 2021-04-28T16:09:02.166700Z

@jimka.issy Although not entirely accurate, a Clojure decompiler could be educating to see how the Clojure compiler turns s-expressions into bytecode in these cases https://github.com/clojure-goes-fast/clj-java-decompiler

borkdude 2021-04-28T16:09:32.167400Z

arglists is a term commonly used and this is also the name in the metadata on the var

Jim Newton 2021-04-28T16:09:55.167800Z

and what is the list of arguments called, if arglist has a different meaning?

Jim Newton 2021-04-28T16:10:51.168700Z

((fn [a b c] …) 1 2 3) for me [a b c] is the lambda list and (1 2 3) is the arglist

borkdude 2021-04-28T16:11:24.168900Z

arguments?

Jim Newton 2021-04-28T16:11:38.169200Z

arglist != list of arguments ???? curious

borkdude 2021-04-28T16:12:48.169800Z

user=> (defn foo [a b])
#'user/foo
user=> (meta #'foo)
{:arglists ([a b]), :line 1, ...

Jim Newton 2021-04-28T16:12:59.170300Z

A few years back I used one lisp which called (a b c) the formal parameter list and (1 2 3) the arg list

Jim Newton 2021-04-29T07:58:12.217700Z

I thought the term lambda-list came from lambda calculus. but an associate of mine who teaches lambda calculus says he’s never heard of the term.

borkdude 2021-04-28T16:13:00.170400Z

don't get caught up on names, just accept them, they won't ever change :)

wotbrew 2021-04-28T16:13:35.170600Z

Not sure how easy it'd be to exploit with most apps but I bet a bunch have multi-methods that take user input as a dispatch val (e.g string "type" json field) assuming that hitting :default comes with no risk.

ghadi 2021-04-28T16:14:02.170900Z

Arglist

ghadi 2021-04-28T16:14:08.171200Z

Argvec

ghadi 2021-04-28T16:14:22.171800Z

It’s a vector, but often called the arglist

Jim Newton 2021-04-28T16:42:13.173200Z

what are the type annotations called in a definition like this: (defn [^Boolean a b] …) are they called type hints, or type annotations or what?

2021-04-28T16:43:00.173700Z

they are not annotations, annotations are a bytecode feature

2021-04-28T16:43:17.174Z

(at least I'd like to keep that term specific)

2021-04-28T16:43:30.174200Z

type hint is the term I've seen

alexmiller 2021-04-28T17:07:20.174400Z

they are type hints

alexmiller 2021-04-28T17:07:38.174800Z

the only type hints that affect the actual compilation of the function signature are ^long and ^double

Jim Newton 2021-04-29T07:59:36.217900Z

@alexmiller, you said ^long and not ^Long. Do I understand that correctly?

Jim Newton 2021-04-29T08:00:13.218100Z

If I provide a ^double type hint, will the compiler compile a special path for double and another path for everything else?

Jim Newton 2021-04-29T08:01:47.218300Z

I’m giving a talk at ELS 2021, I’m going to mention some clojure things, and it will be very easy for me to claim something that’s not true in passing.

borkdude 2021-04-29T08:06:15.218800Z

@jimka.issy You can check yourself like this: https://gist.github.com/borkdude/d1bf6a20650862c38ee43c0656d72e39 Note that a decompiler isn't always accurate but it gives some insights.

borkdude 2021-04-29T08:07:37.219Z

It seems like what you are saying is true: it has one method specialized for the double and one for Object

Jim Newton 2021-04-29T08:09:06.219200Z

interesting so it does compile a specialized code path for double.

Jim Newton 2021-04-29T08:09:26.219400Z

cool

alexmiller 2021-04-29T12:46:37.245900Z

Yes

alexmiller 2021-04-28T17:07:53.175Z

otherwise they are always Object

vncz 2021-04-28T17:10:11.175300Z

Oh interesting. Is there a technical reason for this?

em 2021-04-28T17:11:23.175500Z

For more details and official documentation of this behavior: https://clojure.org/reference/java_interop#primitives

2021-04-28T17:13:37.175700Z

see also the definition of IFn - the specializations are only on long and double args, otherwise the sig is always Object https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/IFn.java

tvaughan 2021-04-28T17:15:40.176100Z

Sorry, I don't know what the Mach repo is

vncz 2021-04-28T17:20:19.176300Z

Aha that clarifies

borkdude 2021-04-28T21:20:31.177900Z

Why is there an assymetry between assoc! and conj!? (conj!) returns a transient vector (assoc!) does not return a transient map (conj! .. 1 2 3) is not varargs (assoc! (transient {}) :a 1 :b 2) is varargs

alexmiller 2021-04-28T21:59:39.178200Z

I am surprised if assoc! does not return a transient map

alexmiller 2021-04-28T21:59:51.178500Z

how could it work if not?

alexmiller 2021-04-28T22:00:35.178700Z

assoc! matches assoc

alexmiller 2021-04-28T22:00:43.179Z

I think there is a ticket to make conj! match conj

bronsa 2021-04-28T22:03:45.179500Z

he means the 0-arities of conj/conj! are defined

bronsa 2021-04-28T22:03:52.179800Z

while of assoc/assoc! aren't

alexmiller 2021-04-28T22:03:56.180Z

https://ask.clojure.org/index.php/2370/make-conj-assoc-dissoc-transient-versions-handle-similarly?show=2370#q2370 is one but I'm pretty sure there may be other tickets

alexmiller 2021-04-28T22:04:13.180400Z

I know there are some with other proposals that would interact with this one

alexmiller 2021-04-28T22:05:45.181Z

ah, sorry - I see you meant specifically those, I think that's covered in this ticket

alexmiller 2021-04-28T22:06:20.181300Z

I think the answer to the original question is: there is no good reason

ghadi 2021-04-28T22:20:23.183200Z

Calling conj! with no arguments is meaningful for its usage as a reducing function

ghadi 2021-04-28T22:20:57.184Z

transduce calls that arity when no init provided

ghadi 2021-04-28T22:22:22.185800Z

there is no such kv reducing context that calls the no-arity, so i suspect that there was no pressure in giving assoc! the same treatment

alexmiller 2021-04-28T22:24:49.186200Z

that ask.clojure issue has 0 votes, feel free to vote if you'd like to see it move up the priority list!

borkdude 2021-04-28T22:37:32.186500Z

voted thanks.

alexmiller 2021-04-28T22:48:35.190500Z

Jira has now force migrated people to their “new issue view” which inexplicably does not contain vote information. All the jira jiras about it are closed and seem to suggest it is available but perhaps only on “next gen” projects, not “classic” projects. To migrate, you have to create a new project, presumably with a different project id, which would break all existing links. Cool.

2
2021-04-28T22:57:39.191400Z

"All the jira jiras ..." 🙂

alexmiller 2021-04-28T22:59:33.192100Z

I’m just teeing you up to ask how I voted for that jira jira

2021-04-28T22:59:51.192700Z

"How did you vote for that jira jira, Alex?"

alexmiller 2021-04-28T22:59:53.192900Z

Because they didn’t use the new ui

alexmiller 2021-04-28T23:00:02.193300Z

Because it sucks

❤️ 1
2021-04-28T23:00:40.194Z

Sounds like a case of "normally we would eat our own dog food here, but in this case our own dog food wasn't good enough."

alexmiller 2021-04-28T23:01:07.194700Z

Eating your own dog food and holding your nose or something

alexmiller 2021-04-28T23:04:56.197Z

I mean like, I get why software is hard. I really get it. But this new ui has been like 2 years in the making and it just moves some fields to the right and got rid of actually useful things (afaict). I dunno.

alexmiller 2021-04-28T23:06:53.198Z

Clubhouse (the Clojure project thing, not the audio social media thing) is pretty cool

seancorfield 2021-04-28T23:28:26.198800Z

I guess it’s a good job we now have http://ask.clojure.org as a proxy for Jira so we can still vote on things by proxy :rolling_on_the_floor_laughing:

😂 1
seancorfield 2021-04-28T23:36:05.199100Z

FWIW, if I’m logged into Jira, I can still see the voting button for Clojure issues:

seancorfield 2021-04-28T23:36:24.199300Z

(That’s CLJ-2556)

alexmiller 2021-04-28T23:50:59.199500Z

oh, I might have just missed that completely

alexmiller 2021-04-28T23:51:59.199700Z

oh, I was on one with no votes there's no number so it's easy to miss