Fork me on GitHub
#clojure
<
2022-12-20
>
Kimo08:12:04

I guess defmulti is stateful, since the value of (methods %) changes as you add & remove methods. Is there a way to subscribe to that state?

OknoLombarda08:12:41

It uses internal state of a https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/MultiFn.java#L58 to store methods, so I guess no, you can't subscribe to it. But you could poll the method table to see if it changed

Kimo09:12:39

(.-method-table %) returns an atom, so it seems like calling add-watch on that would work.

OknoLombarda13:12:30

It does, but in ClojureScript, not in Clojure

Martynas Maciulevičius09:12:21

What was the design decision when adding update-vals? I'm used to use

(->> []
     (map identity)
     (reduce conj #{}))
But update-vals expects the function to be in the end of its arg list and that always throws me off. Why this design choice? I know that we have as-> function but I don't like to use it. Does it mean that syntax of map and reduce is bad? Should their signatures also have the function in the last place? I don't understand this inconsistency for update-vals . Even in my own function signatures I like to send the actual data in the last place.

magnars09:12:36

Functions that work on maps take the map as their first argument, while functions that work on seqs take the seq as their last argument, for use with -> and ->> respectively.

☝️ 2
Martynas Maciulevičius09:12:40

Ah, alright. I see. assoc But well.. here we also see the input map as a sequential update problem, like map. i.e. you don't work on one element but on all of values.

simongray09:12:00

I don’t think it is about the kind of work, but about the structure of the chained data. You can chain operations on sequential data together with ->> and on associative data using -> .

Martynas Maciulevičius09:12:21

@U4P4NREBY this is what I want to do:

(->> [1 2 3 4 5]
     (map (juxt identity identity))
     (into {})
     (update-vals inc)) ;; <- error

; (err) Don't know how to create ISeq from: clojure.core$inc

magnars09:12:53

why not ?

(->> [1 2 3 4 5]
     (map (juxt identity inc))
     (into {}))

Martynas Maciulevičius09:12:21

The first part is completely arbitrary. I only want to show that update-vals is almost the same thing as map. It's an example.

magnars09:12:48

it's not, tho, since update-vals works on associative structures, and map works on sequential structures

Martynas Maciulevičius09:12:46

I tend to write this code more frequently than I'd want to write it:

(defn map-vals [f m]
  (->> m
       (map (fn [[k v]] [k (f v)]))
       (into {})))

(map-vals inc {:a 1 :b 2})

octahedrion10:12:15

(associative? (map (fn [x y] (clojure.lang.MapEntry. x y)) (range 8) (range 8)))
=> false
(update-vals (map (fn [x y] (clojure.lang.MapEntry. x y)) (range 8) (range 8)) inc)
=> {0 1, 1 2, 2 3, 3 4, 4 5, 5 6, 6 7, 7 8}

(associative? (seq {:x 5}))
=> false
(update-vals (seq {:x 5}) inc)
=> {:x 6}

Martynas Maciulevičius10:12:18

I don't understand what you mean. If I want to input a map and get a map as an output I do it. There is no guard against that. Of course map is not associative, because it produces a lazy-seq. But I want a {} as an output, not a list of tuples that I'd then put into a new map anyway. This was what my own map-vals function does. But I don't want to rewrite it again and again. Maybe I miss something. Or maybe I should overoptimize everyting to prevent {} -> {} transformations and only do it once.

magnars10:12:11

or you could take into consideration that -> is a better tool for working with maps than ->> and restructure your code to work with the grain, instead of against it

skylize13:12:30

Does not resolve your philosophical dispute with the design, but maybe something like this will help you?

(defn flip [f] (fn [y x] (f x y)))

(->> [1 2 3 4 5]
     (map (juxt identity identity))
     (into {})
     ((flip update-vals) inc))

; => {1 2, 2 3, 3 4, 4 5, 5 6}

Martynas Maciulevičius14:12:48

> Note that this is not the same as taking the primary operand last. This was the piece of philosophy that I was thinking about. I wanted the map to come last. But well.. oh well. For me update-vals operates on a sequence of items in a map, not on a map. This is... gray area for me. But alright, the top-level data structure is a map so what can I do.

Alex Miller (Clojure team)14:12:53

update-vals is map -> map so it's a collection function

Martynas Maciulevičius14:12:18

So why filter is not a collection function?

Alex Miller (Clojure team)14:12:48

it's seqable -> seqable

Alex Miller (Clojure team)14:12:55

seq functions often seqables, coerce them to seqs, operate on them as seqs, and return seqs (or occasionally seqables)

Alex Miller (Clojure team)14:12:33

examples: map, mapcat, keep, filter, remove, take, drop

Martynas Maciulevičius14:12:00

This is what I wrote in my larger comment because I read about it in clojure guide that you sent.

seqable -> seqable => last 
map -> map => first ;; also other data structures probably as well

Martynas Maciulevičius14:12:07

My original comment was about this updating because when I update parts of my data structure then I'm used to provide the data structure last as if it was a collection. Example of it would be clojure.walk/postwalk which takes fn and any form. So what I was thinking about was "if I update one value in a map then it's first operand" and "if I update more than in some kind of sequential way then it's last" But it doesn't matter as basically only data type matters.

octahedrion09:12:09

One thing about -> and ->> is that it's easier to switch into ->> from -> than the reverse:

(-> {:y 3}
  (assoc :x 7)
  (update-vals (partial * 8))
  (->>
    (mapcat reverse)
    (partition 2)
    (map vec)
    (into {}))
  (assoc :z 9)
  (conj [9 8]))
switching to -> from ->> is cumbersome:
(->> (range 8)
  (partition 2)
  (map vec)
  (into {})
  ->
  (->>
    (->
      (assoc :y 7)
      (update-vals (partial * 10))))
  (map reverse)
  (map vec)
  (into {})) 
unless anyone knows a better way (using only -> and ->>) ? Not that it's ever really needed but it's an interesting exercise

Martynas Maciulevičius09:12:06

I haven't been running into a problem like this because you work on concrete data structures when you use -> and then you move to lists when you use ->>. So you're rewriting your code in both cases but yes, it's harder to produce concrete data structures from lists. Also your code is too long. I think try to not chain more than ~5 of these ->> operations in one code chunk. Also doubly nested ->> and ->... why didn't you use as->?

octahedrion10:12:30

(it was only an example @U028ART884X! Contrived for discussion purposes, and yes I know about as-> that's why I specified -> and ->> only)

Mayur Pandey10:12:04

I am trying to create a GCP function using Java 11. Cloud Function type: pub-sub. Uploaded a standalone jar generated using lein uberjar I am getting below error while deploying my function. Does anyone got any idea ?

Exception in thread "main" java.lang.ExceptionInInitializerError at clojure.lang.Namespace.<init>(Namespace.java:34) at clojure.lang.Namespace.findOrCreate(Namespace.java:176) at clojure.lang.Var.internPrivate(Var.java:156) at com.hello.GCPClojureHandler.<clinit>(Unknown Source) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490) at com.google.cloud.functions.invoker.BackgroundFunctionExecutor.forClass(BackgroundFunctionExecutor.java:122) at com.google.cloud.functions.invoker.BackgroundFunctionExecutor.forClass(BackgroundFunctionExecutor.java:115) at com.google.cloud.functions.invoker.runner.Invoker.startServer(Invoker.java:247) at com.google.cloud.functions.invoker.runner.Invoker.main(Invoker.java:121) Caused by: java.io.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath. at clojure.lang.RT.load(RT.java:462) at clojure.lang.RT.load(RT.java:424) at clojure.lang.RT.<clinit>(RT.java:338) ... 12 more

kwladyka10:12:20

I don’t know the answer, but it is worth to see if you can use Cloud Run instead of functions

kwladyka10:12:45

At least this is my choice, but it can not fit it all cases

Mayur Pandey12:12:36

Thank you @U0WL6FA77. Will explore that as well. 🙂

Ed12:12:46

That error looks a bit like clojure core isn't included .. if you look in the jar you've generated, can you find the clojure/core__init.class file?

Ed12:12:12

I'm not sure I remember what lein does if you don't have a version of clojure as a dependency. Maybe it works fine if you lein repl or whatever, but doesn't include it as a dep when lein uberjaring??

Mayur Pandey12:12:38

init class in present ➜ gcp-test jar tf ./target/gcp-test-0.1.0-SNAPSHOT-standalone.jar | grep clojure/core__init.class clojure/core__init.class

Mayur Pandey12:12:42

https://sparkofreason.github.io/jvm-clojure-google-cloud-function/ this article mentions that it may be due to class loading. But I am not sure what I need to do exactly.

Ed12:12:09

have you got :gen-class on the namespace that you're using as an entry point?

Mayur Pandey12:12:52

yes

(gen-class :name "com.hello.GCPClojureHandler"
  :implements [com.google.cloud.functions.BackgroundFunction]
  :import [com.google.cloud.functions.Context])
and in project.clj aot is set
:profiles {:uberjar {:aot :all}}

Ed13:12:19

and if you run that jar on your machine with something like

java -cp path/to/uber.jar com.hello.GCPClojureHandler
does it work? (I guess you may need to add a -main function or something?)

Mayur Pandey13:12:28

yes Error: Main method not found in class com.hello.GCPClojureHandler, please define the main method as: public static void main(String[] args) or a JavaFX application class must extend javafx.application.Application

Ed13:12:44

I have to admit to having done way more aws lambda than gcp cloud functions ... but I don't remember having to do anything more than correctly implementing the interface ...

Ed13:12:41

maybe the best thing to do is to follow the advice from that article and implement a small java shim?

Mayur Pandey13:12:37

Yeah, I am going to try that. Thank you so much 🙂

Ed13:12:07

or maybe define clojure.main as the entry point? if you can pass an arg to it to tell it which ns to load?

👍 1
Ed13:12:30

I'm not so familiar with what you can specify to the gcp cloud function settings

Eric Scott14:12:33

I just spent a bit of time tracking down what turned out to be a syntax error in my deps.edn file, which manifested as:

"Execution error (ClassCastException) at clojure.tools.build.tasks.write-pom/to-dep$fn (write_pom.clj:44).\nclass clojure.lang.PersistentVector cannot be cast to class clojure.lang.Named (clojure.lang.PersistentVector and clojure.lang.Named are in unnamed module of loader 'app')\n",
It surprised me that there wasn't some kind of spec violation flagged instead. The build code does in fact check the basis against specs in various places, but here, as far as I can tell, is the entire contents of the specification:
(ns clojure.tools.build.api.specs
  (:require [clojure.spec.alpha :as s]))

(s/def ::lib qualified-ident?)
(s/def ::path string?)
(s/def ::paths (s/coll-of string?))
That's it? Is there some other place where well-formed deps.edn specifications are nailed down in more detail?

Alex Miller (Clojure team)14:12:52

that's tools.build specs, not deps.edn

Eric Scott14:12:45

Ah. Am I correct in the understanding that the thing that needs to be well-formed is the basis, derived from the various layers of deps.edn files?

Eric Scott14:12:45

Ah! That's what I was looking for! Thanks!

Alex Miller (Clojure team)14:12:17

if you have an example of what your error was happy to consider improving spec or error if it makes sense (but maybe move this to #C6QH853H8 or #C02B5GHQWP4)

Eric Scott14:12:05

In retrospect it was a dumb error.

{:deps {<library> {:mvn/version ... :exclusions [[nested-too-deep]]}, ...}...}

Alex Miller (Clojure team)14:12:58

the Clojure CLI does this already but perhaps it is not being checked in this case via tools.build

👍 1
Alex Miller (Clojure team)14:12:42

I think clj-kondo does validate deps.edn files too, or it can, so you could ask that in #CHY97NXE2

Eric Scott14:12:12

I'll do that. Thanks.

lispyclouds14:12:06

@UB3R8UYA1 if you have clojure-lsp setup, that can warn you when you have a deps.edn open in real time:

Eric Scott14:12:58

Does somebody already publish a tool or an alias that validates your deps.edn file against deps/specs.clj?

Alex Miller (Clojure team)14:12:00

as I said above, the Clojure CLI should be doing this, but perhaps it is not

lread15:12:37

#CHY97NXE2 detects common errors in deps.edn

Eric Scott15:12:17

This would be the proper command, right?

clojure -M:kondo --lint deps.edn
This flags syntax errors, but the actual spec violation discussed in the thread above goes unnoticed.

lread15:12:33

Ya, if you have a :kondo alias setup in your deps.edn, but folks typically use the clj-kondo binary. So: clj-kondo --lint deps.edn And lotsa people enjoy editor integration, so you can get lint warnings/errors as you type. If you are using clojure-lsp, it includes clj-kondo linting.

Eric Scott15:12:12

Yeah, personally I hate that. It's like having your mother look over your shoulder while you're trying to think.

Eric Scott15:12:50

I didn't realize the binary was so different from the aliased version.

lread15:12:55

One person's nagging mother is another person's helpful friend. simple_smile

lread15:12:27

Huge time saver for me.

Eric Scott15:12:36

We clearly have different mothers 🙂

simple_smile 1
lread15:12:31

Yes, there is no one right answer, and different opinions and ways of working are totally fine.

lread15:12:54

BTW, the binary and aliased version should be equivalent. The jvm launched kondo will just take a little longer to startup.

Eric Scott17:12:01

Just as a follow-up, I wound up adding this function to my copy of @U04V70XH6’s build.clj file:

(:require 
            ...
            [clojure.spec.alpha :as spec]
             [clojure.tools.deps.specs :as deps-specs]
            ))

(defn validate-deps
  "Throws an `ex-info` of type `::invalid-deps`, or returns `opts` unchanged"
  [opts]
  (println "Validating deps.edn...")
  (let [deps (-> "deps.edn" (slurp) (read-string))
        ]
    (when (not (spec/valid? ::deps-specs/deps-map deps))
      (throw (ex-info "Invalid deps.edn"
                        {:type ::invalid-deps.edn
                         ::spec/problems (-> (spec/explain-data ::deps-specs/deps-map deps)
                                             ::spec/problems
                                             )
                         })))
    (println "deps.edn conforms to clojure.tools.deps.specs")
    opts))

seancorfield17:12:11

Or perhaps just (spec/assert ::deps-specs/deps-map deps) instead of that whole when expression? And it will also throw if your deps.edn is not valid EDN (I'm hoping read-string is referred from clojure.edn and isn't the core read-string function?). But I'm in the "editor integration" crowd with Calva/LSP/Kondo checking my deps.edn files as I edit them -- we have 175 of them in our monorepo!

🙏 1