Fork me on GitHub
#clojure
<
2022-09-11
>
Brian Beckman00:09:47

Hello! I want to write clojure.specs for namespaced keywords that are bound to variables, like nskw below. Here is what I tried:

(ns asr)
(require '[clojure.spec.alpha :as s])
(defn sdef [nskw pred]
  (println (= nskw ::tunit))
  (s/def nskw pred))
(sdef ::tunit list?)
Here is what I get: > true > asr/nskw Here is what I want: > true > asr/tunit As an aside, the reason I want this is that I am translating another specification language (ASDL — ancient :)) into clojure.spec. I want to translate the ASDL automatically because the ASDL specs are evolving and I don’t want to maintain the parallel clojure.specs manually. More asides: I want clojure.spec for test-generation. I want to generate large volumes of test cases that conform to the clojure.specs (and to the ASDL) for beating the hell out of a compiler backend. The specs are for the compiler intermediate representation, a tree-like Abstract Semantics Representation (asr). The compiler is lpython (https://lpython.org) Any hints for me?

walterl06:09:27

s/def is a macro, which turns symbol keys (the first arg) into https://github.com/clojure/spec.alpha/blob/13bf36628eb02904155d0bf0d140f591783c51af/src/main/clojure/clojure/spec/alpha.clj#L355. That means you'll need a macro for what you want to do.

thumbnail08:09:02

Or use eval to create/execute the s/def form

👍 1
Brian Beckman12:09:12

@UJY23QLS1 great to read the source. for the pointer.

Brian Beckman12:09:52

@UHJH8MG6S eval is probably the easiest solution in the short-run, and for my collaborators (who are not clojurians) to read. macrology is fun, but I expect they won't go to the effort of understanding or maintaining macros.

roklenarcic12:09:26

I have a namespace with 5 functions that all have same first two parameters, both being a kind of an options map. Usually they are called one after another, so it seems silly to always specify two options maps, especially when if user were to specify a different options map in each call it would be a bug. So I wonder how to simplify this so I wouldn’t have to repeatedly provide these options maps. Now in OOP this would be quite simple, create a class that takes 2 maps in constructor and then add 5 functions as methods without the 2 params. In Clojure I can make a Protocol, then a Record and have that extend the protocol, but that seems quite an overkill for this. But another way would be to have a function that is a “constructor” and it returns a map of functions that close over the two maps. But I never see this pattern in Clojure code. What other options are there?

p-himik12:09:58

If the calling pattern is always exactly or almost exactly the same, I would simply extract that calling pattern. So instead of a user having to call (f m1 m2) and (g m1 m2) right after, they would call (x m1 m2) where (defn x [m1 m2] (g m1 m2) (f m1 m2)). If the calling patterns are different between use cases, I'd still probably tend to use maps explicitly. But if you really don't want to do that, dynamic bindings is another viable option. And, of course, what you have described are still viable options themselves - what to choose depends on the exact use cases. Just note that you won't be able to completely prevent a user from calling g with different parameters than those that were passed to f - and not that you should, so that's a good thing.

roklenarcic12:09:46

hm… yeah I don’t see big savings there

roklenarcic12:09:03

the calling patterns are different enough that I cannot premake combinations

phill12:09:43

You could have the caller bundle the two maps into one, then use the threading macro (-> twomaps (f 42) (g) ...)

💯 1
phill12:09:51

in essence, it is your OO pattern, but without the O's

💯 1
roklenarcic12:09:02

Hm I’ll think about it, thanks

skylize13:09:34

Sounds to me like a job for partial.

(defn foo [a b] ... )
(defn bar [a b c] ... )

(let [foo-mn (partial foo m n)
      bar-mn (partial bar m n)]
  (foom-mn)
  (bar-mn z))

👍 1
dpsutton14:09:42

you could have a “constructor” function that takes options maps and returns a reifyd instanced that provides a protocol of the functions you care about

didibus17:09:20

Only have one options map. Either merge them, or just have an options map of options map:

(-> {:x-options x-options
     :y-options y-options}
    (fn1)
    (fn2 extra-arg)
    (fn3))
Or
(-> (merge x-options y-options}
    (fn1)
    (fn2 extra-arg)
    (fn3))
And you can have a constructor for that map so clients don't have to know how the functions want it:
(defn make-xy-options 
  [x-options y-options]
  ;; Either merge or combine
  (merge x-options y-options))
  ;; Or combine
  ;; {:x-options x-options
  ;;  :y-options y-options})
So all together it looks like:
(defn make-xy-options
  [x-options y-options]
  {:x-options x-options
   :y-options y-options})
 
(defn fn1
  [xy-options]
   ...)
(defn fn2
  [xy-options extra-arg]
  ...)
(defn fn3
  [xy-options]
  ...)
 
(-> (make-xy-options x-options y-options)
    (fn1)
    (fn2 extra-arg)
    (fn3))
Keep in mind in order to allow threading they all have to return the xy-options. But they don't have too, just if they return something else you can't thread them. If they do side-effects only you could still thread them with doto instead. But otherwise you would call them without threading:
(let [xy-options
      (make-xy-options x-options
                       y-options)
       fn1-result (fn1 xy-options)
       fn2-result (fn2 xy-options     
                       extra-arg)
       fn3-result (fn3 xy-options)]
  (+ fn1-result 
     fn2-result
     fn3-result))

🎯 1
didibus18:09:45

This is a common idiom by the way. Everytime you have a set of functions all operating over the same data using the above pattern is common. Like @U0HG4EHMH said, it's OOP without the Os, which some people call it Data Oriented Programming DOP. Because we've replaced the Object with a Data-structure instead.

👍 1
roklenarcic18:09:47

I’ve seem this pattern before

chrisn16:09:09

Is there a way to disable locals clearing for a specific let statement?

Drew Verlee18:09:16

what do you mean by "clearing" in "locals clearing?"

bronsa18:09:47

no, why do you need that?

roklenarcic18:09:38

By locals clearing he means that local variables are set to nil whenever it is first possible to so so instead of being referenced for the whole scope

roklenarcic18:09:27

This is done to enable garbage collection

bronsa18:09:26

@UDRJMEFSN there is a clojure.compiler.disable-locals-clearing flag but it works for the whole execution, the only other way is to artificially keeping the local referenced, but of course this is not "disabling" clearing

chrisn18:09:50

Ok thanks not worth it ;-)

lvh22:09:44

I’m trying to understand tools.deps’ Tools abstraction. IIUC the idea is that tools that don’t use the classpath directly can be invokved and installed separate from any project. I would therefore expect that they’re the intended deployment mechanism for linters (e.g. clj-kondo, zprint), etc. However, because it works analogously to -X, it’s intended for tools that don’t write a main-style entrypoint. Is that mostly accurate? (I ask because zprint /feels/ like I should be invoking it -Tzprint.)

seancorfield22:09:39

More and more tools are adding exec entry points, often in addition to -main entry points, so they can be invoked via -T/`-X`. Since it's the newer way to do stuff, not all tools have "caught up" yet.

lvh00:09:51

thanks! any advice for how to use these in a company context? I imagine a babashka task that ensures all the tools are installed or something

didibus00:09:07

In company context you probably shouldn't use globally installed tools, instead declare aliases in the project directly for them, you can have project local tools that you run with -T:tool that similarly doesn't use the classpath, but don't need a global install. This makes sure everyone who pulls down the project gets the exact same set of tools and the same versions of them,

didibus00:09:34

You could also wrap zprint and company inside your tools.build and call them with -T:build zprint if you wanted as well.

seancorfield00:09:07

@U07QKGF9P Yeah, as far as use within a company context, I agree with @U0K064KQV -- put the tools/aliases/etc in your company project's repo / deps.edn / build.clj and use them that way, even if that means invoking them via their -main using something like https://clojure.github.io/tools.build/clojure.tools.build.api.html#var-process