Fork me on GitHub
#beginners
<
2020-12-02
>
st3fan01:12:04

I forgot who recommended claypoole to me, but that is a nice library if you easily want to do a bunch of stuff in parallel without fancy apis

3
st3fan02:12:48

What is the proper way to loop over a lazy collection to “do” something .. i need to loop over a list and swap! an atom.

st3fan02:12:00

Is it doseq ?

noisesmith07:12:41

doseq if you need bindings and an arbitrary body, run! if you have a function that should be called for each element in order

zach02:12:39

Would y’all have advice for choosing libraries for building a web app with clojure? I am facing an issue where I am searching for a solution to some problem in a web server, and find a site pointing to a library, but the library hasn’t had any updates for 10 years. It’s hard to tell if the library is extremely stable (and with clojure, this seems likely) or if it’s outdated.

zach02:12:53

I am also facing an issue with limited documentation. I am currently working to use compojure, which I think just has a slim wiki for documentation, which leads me to searching the web for how to do something with compojure, but much of the results are also 8-10 years old.

zach02:12:43

I don’t mean this as a complaint, it’s beautiful open source software and I know it’s people behind it all-- more just seeing if there’s some good rules of thumb experienced folks use when picking libraries or building up their stack.

seancorfield03:12:12

@U011DMQ8DSS Which library is that?

seancorfield03:12:58

For almost all our web apps, we use Ring, the "standard" Jetty adapter, Compojure for routing, and then Component for managing start/stop and dependencies. We have one app using Bidi for routing, and one app using Netty directly via Java interop (because it has to support http://socket.io on the server).

seancorfield03:12:45

It's certainly true that a lot of Clojure libraries have pretty minimal documentation. Clojure was originally aimed at experienced developers so there's a lot assumed in most documentation and often very little in the way of examples or any sort of "cookbook". As more developers -- and less-experienced developers -- have come to Clojure, some maintainers and projects are making more of an effort to provide more comprehensive documentation but it's all a bit hit or miss...

zach03:12:03

I was following advice (that might’ve been from you!) that I read on clojureverse, to start as simply as possible with compojure and ring and I quite like them, and how straightforward everything feels.

seancorfield03:12:43

Yup, very likely my recommendation.

seancorfield03:12:56

What is the library that is ten years old?

zach03:12:34

My issue came when I was trying to persist some session data using the ring-defaults and some middleware, and it worked kinda…and when I read some docs i was pointed to https://github.com/kremers/sandbar, which was 10 years old.

zach03:12:54

Similar libraries that pop up during my web crawling is deprecated lib-noir, or the 5 yo https://github.com/cemerick/friend , and it made me realize I hadn’t developed a good sense on when a library was basically done and stable, and when it was out-of-date.

seancorfield03:12:22

Never heard of sandbar. I try to avoid session data in general tho' but using cookie storage should be reasonable for basic usage (and anything that can't be done that way probably shouldn't be using session data IMO).

zach03:12:49

ah, good to know!

zach03:12:14

I saw you had a usermanager repo for learning, i should take a look at that cos it has basically the same stack i’m using (though i’d never heard of component before today!)

seancorfield03:12:36

Buddy is the other commonly-referenced auth library for Clojure and it's slightly more up-to-date than Friend but I don't know much about either. We had a requirement for OAuth2 and needed to write our own Identity Server for... reasons... So we have an Auth Server, a Login Server, and our apps all separate.

seancorfield04:12:38

In theory, anyone could request a client ID/secret from us (registering an app with us) and then use standard OAuth2 client libraries against our system 🙂 One day, maybe we'll test that theory...

😁 3
Clark Urzo02:12:10

So I'm using deps.edn with shadow-cljs but I can't seem to get a useful REPL out of it

Clark Urzo02:12:17

Here is my deps.edn file

{:paths   ["src/clj" "test/clj" "src/cljs" "test/cljs"]
 :deps    {org.clojure/clojure {:mvn/version "1.10.1"}}
 :aliases {:shadow-cljs {:extra-deps {thheller/shadow-cljs {:mvn/version "2.11.8"}
                                      binaryage/devtools {:mvn/version "1.0.2"}
                                      proto-repl {:mvn/version "0.3.1"}
                                      reagent {:mvn/version "0.8.1"}}
                         :main-opts  ["-m" "shadow.cljs.devtools.cli"]}}}

Clark Urzo02:12:54

And my shadow-cljs.edn file

{:deps {:aliases [:shadow-cljs]}

 :nrepl        {:port 3333}
 :builds       {:app {:target :browser
                      :output-dir "public/js"
                      :asset-path "/js"

                      :modules {:main {:entries [app.core]}}
                      :devtools {:http-root "public"
                                 :http-port 3000}}}}

dpsutton02:12:29

For looping over a collection laziness doesn’t really implicate it. The real question is if you need the result from doing whatever or just run a function for each thing in the collection

dpsutton02:12:22

Clark what’s your editor? Or just a repl at the command line?

Clark Urzo02:12:36

command line

Clark Urzo02:12:17

there's a note in the shadow-cljs docs that says aliases aren't applied when connecting to a running server but i don't know how to fix that

dpsutton02:12:46

What are you trying? There’s extensive documentation. (Oh I see you’ve seen these)

Clark Urzo02:12:20

I want to have a cljs repl that's connected to the browser

dpsutton02:12:39

Another thing you could try is the repl api. Just start the clojure repl with your alias and then use the dev tools api for shadow to start your build

Clark Urzo02:12:10

Mmm, not sure how to do that. I'm just doing clj -A:shadow-cljs watch app

Clark Urzo02:12:47

which I'm assuming starts a regular clojure repl instead of a cljs one

dpsutton02:12:41

do you get a repl prompt when running that?

Clark Urzo02:12:43

I feel like I'm doing several things wrong here. For instance, I'm still using lein repl :connect localhost:3333 to connect to the nREPL instance

Clark Urzo03:12:10

@dpsutton not really, but it does say it's started an nREPL server on the port I gave it

dpsutton03:12:34

ok. that's a good start. there's probably a webserver running. do you see any information about that?

Clark Urzo03:12:14

This is the output

> clj -A:shadow-cljs watch app
shadow-cljs - HTTP server available at 
shadow-cljs - server version: 2.11.8 running at 
shadow-cljs - nREPL server started on port 3333
shadow-cljs - watching build :app
[:app] Configuring build.
[:app] Compiling ...
[:app] Build completed. (175 files, 0 compiled, 0 warnings, 3.36s)

Clark Urzo03:12:33

which is the typical shadow-cljs output

dpsutton03:12:40

cool. open up http://localhost:3000 in a browser

Clark Urzo03:12:09

Yup, it works. The page renders. It's just that, if I go to the dev console even if there's shadow-cljs: ready! it doesn't seem to recognise (js/alert)

Clark Urzo03:12:19

So i'm assuming it means the repl wasn't set up properly

Clark Urzo03:12:45

Then when I connect to the nREPL port via lein repl :connect localhost:3333 I'm plopped into a Clojure REPL, not CLJS, so I'm assuming I'm not connected to the right server

Clark Urzo03:12:23

If I do (js/alert with the build window open it should work, right? An alert should pop up?

Clark Urzo03:12:36

Oh wait, so the browser console isn't connected to the nREPL instance after all. It's just a regular browser console.

Clark Urzo03:12:10

That's why (js/alert) wasn't working. (also, do you want to do this in a thread?)

Clark Urzo03:12:16

(thanks btw)

dpsutton03:12:11

of course. you're welcome. what's left to do?

dpsutton03:12:21

i imagine in your terminal you now have a cljs repl

Clark Urzo03:12:18

Not yet. It's still purely a Clojure REPL.

Clark Urzo03:12:36

I'm not sure how this all works out under the hood. Is it because I'm using clj to start the nREPL server?

dpsutton03:12:01

do you have a prompt where you can evaluate things?

Clark Urzo03:12:41

If I connect to it via lein repl, yes

Clark Urzo03:12:16

but otherwise, calling my clj command doesn't give me a REPL

dpsutton03:12:38

ok i misunderstood. i thought there was a repl

dpsutton03:12:08

run shadow-cljs cljs-repl app from another terminal

🎯 3
dpsutton03:12:21

which will connect a client to the running watch

Clark Urzo03:12:43

ohhh wow I can't believe it was that simple

Clark Urzo03:12:09

so to connect to a running cljs repl you need to use shadow-cljs instead of clj , got it

Clark Urzo03:12:30

It works, I can send commands to the browser now!

dpsutton03:12:34

awesome. yes shadow runs a server process that can also watch the build files and keep recompiling. then you connect to this and you're good to go

clj 3
Clark Urzo03:12:06

Thanks @dpsutton it works now!

Mattias10:12:47

Is there a state of the art way of accomplishing a Clojure notebook, a la Jupyter? I’m going to demo some things, mainly usage of rest APIs, and would like something like that. Pointers?

delaguardo10:12:02

those two are great and I used them both in different context during presenting clojure to colleagues

Michaël Salihi10:12:22

Yes, Gorilla and Maria are great! Otherwise, I recently found this library too. https://github.com/clojupyter/clojupyter It can be useful if you are already comfortable with Jupyter. 👍

Stuart10:12:22

I've found clojupyter to work great. Easy to set up (on linux at least).

Mattias10:12:56

Fantastic, thanks! Obviously the net is full of stuff, but having a recommendation (not from an algorithm...) makes all the difference. 😄👍

👌 3
practicalli-johnny13:12:51

https://github.com/scicloj/notespace is a relatively new notebook for Clojure and is very simple to use and can also incorporate vega graphics (which is fast becoming the defacto approach) If you want a multi-language (written in Clojure but supports lots of other language journals), try https://nextjournal.com/ Its an amazing project and seems very powerful. You may be interested in the https://scicloj.github.io/ community, lots of discussions on a wide range of data science related topics.

Lisbeth Ammitzbøll Hansen11:12:27

I am trying to call create-user multiple times - based on a map input - like this: (and then calling (get) to extract :id from the map of users just generated) test-user-ids (map #(get % :id :customer_id) (map #(create-user %) (gen-bodies 2 "user" customer-id)) ) But create-user function does not actually get called (and hence users not created) before I do this: _ (println "test-user-ids : " test-user-ids) I have also tried with (fn) instead of #(), but with the same result : test-user-ids (map (fn [userbody] (get (create-user-with-time-travel userbody) :id)) (gen-bodies 2 "user" customer-id) ) I would be very happy if you could explain whats happening here?

noisesmith18:12:01

map is lazy, and not designed for side effects but for creating values, you can replace map with run! if you don't need the return values, or mapv if you do. Also, (map f (map g l)) can be replaced with (map (comp f g) l)

noisesmith18:12:32

(or (run! (comp f g) l) , (mapv (com f g) l) etc.)

noisesmith18:12:28

also, for all f, where f is a function and not a method, #(f %) can be replaced with f - in your case #(create-user %) can be replaced with create-user

Lisbeth Ammitzbøll Hansen08:12:41

Thanks a lot for your answer - it was a great help :thumbsup:

rmxm11:12:10

Hey, I am trying to copy data(`io/copy`) from ZipInputStream more particularly from ZipEntry to BufferedWriter. I am getting something like so: No method in multimethod 'do-copy' for dispatch value: [java.util.zip.ZipEntry .BufferedWriter]

rmxm11:12:32

I think I am hitting a problem with multimethod dispatch and it has to be exact (inheritance ignored).

hiredman11:12:33

A zipentry is not an inputsream

hiredman11:12:39

If I recall that is not the correct way to use a zipentry, so you may want to check out the docs and maybe find some examples

rmxm11:12:16

yes you are right, i should use zipstream for io/copy and .getNextEntry to move the cursor between

Ben Sless15:12:22

Is there a way to override the JAVA_CMD for clj?

manutter5115:12:18

For what OS and/or shell?

manutter5115:12:56

I’m a bit rusty in bash, but I think you should be able to do JAVA_CMD=/some/other/java clj ...

Ben Sless15:12:32

Not sure this will work is java executable is found before

JAVA_CMD=$(type -p java)
set -e
if [[ ! -n "$JAVA_CMD" ]]; then
  if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
    JAVA_CMD="$JAVA_HOME/bin/java"
  else
    >&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
    exit 1
  fi
fi

Alex Miller (Clojure team)15:12:25

yeah, that won't work. set JAVA_HOME

Ben Sless15:12:56

that doesn't work either

Ben Sless15:12:04

JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 clj
Clojure 1.10.1
user=> (System/getProperty "java.version")
"1.8.0_275"

Ben Sless15:12:46

my Bash isn't perfect either but I think the JAVA_HOME test is only reachable if JAVA_CMD isn't set

Ben Sless15:12:58

and JAVA_CMD is set if a java executable is found

manutter5115:12:07

how about this: PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH clj

manutter5115:12:53

straight-up hacking at this point 😉

Ben Sless15:12:08

That's just cheating 🙂 I updated to a newer version of cli tools let's see

Alex Miller (Clojure team)15:12:14

well either set the java you want on your PATH, or set none and use JAVA_HOME

Alex Miller (Clojure team)15:12:10

jenv will work with the former

Ben Sless15:12:14

It would be nice if it could be configured from the command line without resorting to that. It would only require checking if JAVA_CMD is set before setting it for the firs time. Anyway for now I'll use jenv

Alex Miller (Clojure team)15:12:41

how is setting JAVA_CMD different than setting PATH or JAVA_HOME?

Alex Miller (Clojure team)16:12:10

those all feel like identical things configured in the same way from the command line

Alex Miller (Clojure team)16:12:04

I'm not actually disagreeing with your suggestion, just trying to probe the assumptions a bit

manutter5116:12:01

I think he’s referring to the section in the clojure script that overwrites JAVA_CMD, so you lose whatever you set on the command line.

manutter5116:12:59

That’s the code snippet he posted above.

Ben Sless16:12:31

My rationale is I want to configure different projects to use different versions of the JVM. I can do it with jenv, but what I've been doing with lein is create wrapper scripts where I set in them the JAVA_CMD then use dir-locals in emacs to change the run command for lein

Ben Sless16:12:58

But it looks like jenv will be a better option, all things considered

Alex Miller (Clojure team)16:12:25

we have a ticket for this suggestion already, I'm trying to vet whether having yet one more degree of freedom is necessary

Ben Sless16:12:59

I am biased towards more degrees of freedom. On a practical side: Doesn't require extra tooling: more convenient (less setup), one less dependency. Doesn't require messing around with PATH: safer?

Alex Miller (Clojure team)17:12:23

more degrees of freedom is inherently more complex, so I'm biased against it :)

manutter5115:12:06

or you could look at jenv or something similar to more easily switch between java versions

Alex Miller (Clojure team)15:12:49

clj uses JAVA_HOME if you have that set

Noah Bogart16:12:14

I'm having trouble getting a project made with deps.edn to work. i have a src/advent folder with a core.clj file inside. it has a (defn -main ...) function where I do some work. when I run clj -M at the root directory (where deps.edn is) I am dropped into the repl. how do I execute the project like running lein run?

dpsutton16:12:16

if there's a -main function in namespace advent.core you should be able to do clj -M -m advent.core

dpsutton16:12:40

also, the new -X will be helpful for running arbitrary functions without having to put a -main in each file as you progress through advent

Noah Bogart16:12:24

is it possible to create an alias to handle that for me?

Noah Bogart16:12:48

yeah, I thought about -X, but I want to be able to say clj -M ... 2 and then it runs day 2's code

Noah Bogart16:12:03

nevermind, I have figured out the alias: :main {:main-opts ["-m" "advent.core"]}

Noah Bogart16:12:43

thank you so much for the help

dpsutton16:12:57

absolutely. you can acomplish the same with -X as well. the difference is largely -M is tied to a function that must be named -main and gets string args whereas -X can be any function and its options will be passed as edn, which may or may not be better for your use case

Noah Bogart16:12:50

interesting, that's good to know

Noah Bogart16:12:30

gotta pass in the args like -X blah '{:day 1}', right?

Noah Bogart16:12:23

ope, found it. idk why im' struggling so much to read this page, lol

Noah Bogart16:12:24

clj -X:my-fn '[:my :data]' 789

Jeff Evans17:12:57

what am I doing wrong?

user=> (let [nums ["1" "2" "3"]]
          (map Integer/parseInt nums))
Syntax error compiling at (REPL:2:11).
Unable to find static field: parseInt in class java.lang.Integer

user=> (Integer/parseInt "1")
1

bronsa17:12:10

Integer/parseInt is not a function, you can't use it as a value

bronsa17:12:21

#(Integer/parseInt %)

noisesmith18:12:29

@jeffrey.wayne.evans for a little more context, the JVM model is that methods are not values, they can't be placed on the stack or held in a data structure, a clojure function is an object with an invoke method that the clojure compiler looks for, and #() is a terse way to create one to call a specific method

noisesmith18:12:20

there's a classic essay where someone says "on the JVM methods are slaves to objects and cannot act freely, they must be accompanied by some object at all times", something like that - "kingdom of nouns" I think

😂 3
Noah Bogart21:12:31

good reference, that's a classic

Jeff Evans18:12:41

that makes sense. also probably explains why I can’t do (type Integer/parseInt) (which is what I’d normally do in this situation)

noisesmith18:12:36

right, it's not a "thing" - it's some action known to a thing

hiredman18:12:48

somewhat confusingly the a symbol like Integer/parseInt (a namespace symbol) might be interpreted in a number of different ways

hiredman18:12:16

(Integer/parseInt "1") is invoking the parseInt static method on Integer

hiredman18:12:59

Math/PI is a reference to a static field PI on the class Math

hiredman18:12:50

clojure.core/+ is the value of + defined in clojure.core

Jeff Evans18:12:14

yep, and I can do both type and source on that one

hiredman18:12:20

the latter two are "values", they can be passed around as arguments to functions, etc

hiredman18:12:29

Integer/parseInt only means something when it is literally the first element of a list like (Integer/parseInt "1")

Jeff Evans18:12:49

so is that a special form? or something else?

hiredman18:12:05

it is sort of syntax sugar

delaguardo18:12:10

Integer/parseInt is a reference to static field parseInt of class Integer disreagards that Integer doesn’t have such field it is still useful to reference realy existing static fields in another classes

delaguardo18:12:52

exception thrown from the form (map Integer/parseInt ["1"]) should make it clear

hiredman18:12:58

user=> (macroexpand `(Integer/parseInt "1"))
(. java.lang.Integer parseInt "1")
user=>

hiredman18:12:01

(it isn't a macro, it just happens that macroexpansion de-sugars it)

😮 3
delaguardo18:12:27

btw, more or less complete guide can be found here - https://clojure.org/reference/java_interop

hiredman18:12:34

A long time ago (maybe pre-1.0) clojure had two special forms for interop . and new, but then various bits and bobs were added on top of those, so you almost never use those forms directly now. But it can be useful when explaining certain behaviors to show the . or new version.

hiredman18:12:45

and for some of the new forms, the meaning can be explained by transforming it in to the . or new version, but the compiler doesn't bother and works directly on the "sugared" form, so is that still syntax sugar?

noisesmith18:12:36

the interop sugar can also lead to potentially confusing exceptions to clojure's syntax rules

user=> (= Math/PI (Math/PI))
true

hiredman18:12:12

user=> (read-string "#{Math/PI (Math/PI)}")
#{(Math/PI) Math/PI}
user=> (eval (read-string "#{Math/PI (Math/PI)}"))
Execution error (IllegalArgumentException) at user/eval13 (REPL:1).
Duplicate key: 3.141592653589793
user=>

leif20:12:36

Is there any way to destructure atoms in functions in clojure?

leif20:12:44

Like, the same way you can destructure arrays and maps

noisesmith20:12:31

the only thing you can do to an atom in a binding context is deref it, you can deref on the right hand of a destructure

noisesmith20:12:29

so eg. if you have (def a (atom [1 2 3])) you can do (let [[a b c] @a] b)

noisesmith20:12:27

as for atoms inside other structures: don't, it's not useful

leif20:12:45

I see. So I would have to do something like:

(defn foo [{:keys [atm1 atm2]}]
  (let [{:keys [a b]} @atm1
        {:keys [c d]} @atm2]
    (+ a b c d)))

leif20:12:06

Oh, don't put atoms in data structures?

noisesmith20:12:06

yeah, atoms inside data structures is an antipattern

noisesmith20:12:47

I mean - there might be some case where it's the best option, but I can't recall seeing one

leif20:12:27

Okay, so if I have several variables for an applications state, rather than having a set of atoms, I should have one atom which contains a set, yes?

noisesmith20:12:36

and, for that matter, a nested destructure is usually better split to multiple lines in a let anyway - the performance at runtime is the same, and the code will be clearer

dpsutton20:12:57

a set of atoms sounds fundamentally unmanageable

noisesmith20:12:59

@leif yeah, the usual thing is a single atom holding a hash-map

dpsutton20:12:25

(if you meant that as a literal set instead of just colloquially "some atoms")

noisesmith20:12:38

right - the mutation of an atom can make it unreachable or disasterous to the internal structure of the set actually it works out because atoms are not compared for value, only by identity

leif20:12:39

Err.. I meant more of a map of atoms.

leif20:12:00

Like: {:file (atom "") :color (atom "")}

noisesmith20:12:22

right, the normal thing is (atom {:file "" :color ""})

leif20:12:22

But it sounds like instead I should do:

(atom {:file ""
       :color ""})

dpsutton20:12:26

i figured, just wanted to catch that in the case it was actually meant 🙂.

leif20:12:38

lol, fair, thanks.

leif20:12:53

Okay, so what should I do if I want one thing to reference another in the same data structure.

leif20:12:33

Like...say this:

{:items [_a_ _b_ _c_]
 :selected _c_}

dpsutton20:12:39

swap! takes a function that receives the atom's state and returns a new state

dpsutton20:12:21

and note, it's not possible in the general case to do that with sibling atoms. because one atom could change after the read and you'd be working on stale data

leif20:12:22

Right, I know about swap!, but if I'm making the whole map be an atom, it seems like I can't just rely on it?

dpsutton20:12:56

i don't know what you mean by "rely on it"

noisesmith20:12:09

having one atom makes coherent state easier to enforce, not harder

noisesmith20:12:21

you can set a "validation" function to fix or reject changes for example

dpsutton20:12:28

i think "possible" versus "impossible"

noisesmith20:12:51

you can't have a validator that looks at multiple atoms, you can have one that manages a single atom

noisesmith20:12:09

well - you could in an ad-hoc way but...

leif20:12:16

Like, if I had a map of atoms, I could do this:

(def state
  {:items [_a_ _b_ _c_]
   :current _c_})

(reset! (:current state) ...)
And now the _c_ in both places would be updated

leif20:12:36

And I didn't have to manually do any sort of dependency resolution to find other _c_s in the state.

noisesmith20:12:36

you can't reset a key

leif20:12:13

@noisesmith Right, I didn't reset the key, I dereferenced _c_ using a key.

noisesmith20:12:17

oh I misunderstood

noisesmith20:12:54

but I don't see how multiple atoms makes this any easier

leif20:12:42

I mean, if _c_ isn't an atom, I can't just call reset! on it, so I'd have to hunt down all of the _c_s in the data structure manually, no?

leif20:12:08

Like, if I'm making a multi-tab text editor, I can have all of my operations just work on the current tab, and so I can trust that all changes to that one tab's state will end up in the list of tab's state too, since they're literally the same object.

leif20:12:26

I guess put another way:

(def state
  {:items [_a_ _b_ _c_]
   :current _c_})
(reset! (:current state) _c*_)

;; Results in state turning into:
;;
;;{items [_a_ _b_ _c*_]
;; :current _c*_}

leif20:12:13

err..that's slightly wrong, actually:

dpsutton20:12:40

make "current" a path into items rather than a "copy"

noisesmith20:12:15

in this case I'd use an index, yes

leif20:12:39

(def state
  (let [c-box (atom _c_)]
    {:items [(atom _a_) (atom _b_) c-box]
     :current c-box}))
(reset! (:current state) _c*_)
;; Results in state turning into:
;;
;;(let [c-box (atom _c*_)]
;;  {items [(atom _a_) (atom _b_) c-box]
;;   :current (atom c-box)})

dpsutton20:12:40

or have current be the thing being edited and on commit updated items. couple ways to model this

phronmophobic20:12:59

using atoms would be closer to the OO way of doing things. there are UI libraries that help do this in a more functional way (eg. https://day8.github.io/re-frame/subscriptions/), but depending on your project, I'm not sure I would recommend rearchitecting it

leif20:12:04

@noisesmith Problem with using an index is that program logic that manipulates the :current state also has to be aware of :items. Which is not ideal.

leif20:12:42

@dpsutton Same with making :current a path.

noisesmith20:12:03

@leif this implies a larger transformation where there's likely one indexing hash from key to current value, and then at least one other structure describing state / arrangement

noisesmith20:12:23

but using multiple atoms is not how any normal clojure codebase does things

noisesmith20:12:03

clojure data structures are designed to hold immutable values, and atoms (though luckily not equal by value), are mutable

leif20:12:03

@smith.adriane So you're suggesting using subscriptions to watch for changes in _current_?

phronmophobic20:12:48

that would be a pretty big change, so I'm not sure I would recommend it without knowing more about your project and goals

leif20:12:56

Like, the transformation should be purely functional

leif20:12:14

But the code should only need to deal with a small part of the data structure.

noisesmith20:12:16

@leif lenses, as I've seen them are an abstraction over immutable values

leif20:12:20

And the rest should change with it.

phronmophobic20:12:50

a less invasive change is to pass down event handlers that can delegate changes back up the UI component hierarchy. this reference covers that technique https://reactjs.org/docs/lifting-state-up.html

noisesmith20:12:25

@leif my experience with clojure doesn't prove you're idea doesn't work, but I can say your design is "weird" for clojure

leif20:12:33

I mean, I know both C, Java, Haskell, and Racket have things like this, so I'm just assuming Clojure(Script) does too. But it sounds like people don't do it that much. 🙂

phronmophobic20:12:13

I'm pretty interested in functional solutions. I would love any links to references if you have them handy

noisesmith20:12:09

in fact I'm sure some variation of what you are talking about could work great, it's just not going to be familiar to experienced clojure users (and in a collaborative environment that's a risk)

leif20:12:28

@smith.adriane Right. Although that seems more like handling the view rather than the view-state. (Which I'm already doing. 😉 )

phronmophobic21:12:03

not sure I follow

leif20:12:46

Yes it is. 🙂

leif20:12:03

Just so we're clear, I'm not insisting on doing it one particular way.

leif20:12:11

Like, the things I want are:

leif20:12:26

1. A big immutable data structure for my applications state.

leif20:12:47

2. To have code that does operations on small parts of that state.

leif20:12:36

3. To have the state stay internally consistent, ideally provided by the language or system, so I don't have to write pub/sub code.

noisesmith23:12:25

right - this is why I suggested watchers: a function registered via add-watch (you can have as many as you like) can approve or reject changes to a single atom. This doesn't work when state crosses N atoms.

user=> (doc add-watch)
-------------------------
clojure.core/add-watch
([reference key fn])
  Adds a watch function to an agent/atom/var/ref reference. The watch
  fn must be a fn of 4 args: a key, the reference, its old-state, its
  new-state. Whenever the reference's state might have been changed,
  any registered watches will have their functions called. The watch fn
  will be called synchronously, on the agent's thread if an agent,
  before any pending sends if agent or ref. Note that an atom's or
  ref's state may have changed again prior to the fn call, so use
  old/new-state rather than derefing the reference. Note also that watch
  fns may be called from multiple threads simultaneously. Var watchers
  are triggered only by root binding changes, not thread-local
  set!s. Keys must be unique per reference, and can be used to remove
  the watch with remove-watch, but are otherwise considered opaque by
  the watch mechanism.

leif20:12:02

I'm happy to do it the idiomatic way in clojure, if there is one. 🙂

leif20:12:11

(Unless you think that's unreasonable?)

leif20:12:19

(If so I'm also always happy to learn more.)

leif20:12:26

I guess another way of putting it: I want to do DAG manipulation with clojure.

leif20:12:43

And right now the only way of doing it in clojure that I'm aware of is using atoms.

leif20:12:01

Everything else seems to be purely tree based.

leif20:12:54

But atoms are certainly more powerful than what I want, as they give you full graph manipulation. And I'm cool with the acyclic part.

phronmophobic20:12:02

I think re-frame's subscriptions is the most popular example of trying to accomplish this. om's cursors and https://github.com/hoplon/hoplon/https://github.com/hoplon/javelin I think are also in the same space

hiredman20:12:09

You might look at structuring your state more like a database (a flat set of tuples) instead of an in memory graph of objects

💯 3
hiredman20:12:30

So using something like datascript

leif20:12:52

@smith.adriane Ya, what I want is similar to what re-frame does. Taking a look at hoplin/javelin now.

leif20:12:44

@hiredman Fair. And I guess SQL like code does make it super easy to make indexes into keys splice well together. I'll take a look at datascript.

hiredman21:12:20

to be fair I like the flat database style approach a lot, but have never gone so far as to actually use datascript in a project, I just limp by rolling my own indices, sometimes using clojure.set/index

leif21:12:45

lol, fair.

leif21:12:12

I mean, my app's state is actually stored in the browsers local-storage, which is itself a flat key-value map...so it does fit.

Fra22:12:14

Hi, I am exercising on https://www.4clojure.com/problem/95 and I don’t understand why the last two forms are testing for false. To me those look binary trees, is there anything I am missing? Thanks

dpsutton22:12:03

(:a nil ()) i think its complaining that the "right" tree () does not have a value, left and right child. so i guess it depends on if () is a value in itself or a tree that is lacking values

dpsutton22:12:19

i don't understand why [1 [2 [3 [4 false nil] nil] nil] nil] is not a tree unless false is not a valid value. but i don't see any reason why. but i'm guessing the trees need to be homogenous? the first one is keywords and nils, the remaining ones are integer numbers and nils

👍 3
st3fan22:12:43

Is there a good alternative for (count (filter true? (map some-predicate? collection))) ?

st3fan22:12:28

this seems like a common thing

borkdude22:12:52

@francesco.losciale it seems that they take nil as the only valid leaf

👍 3
borkdude22:12:20

@st3fan (count (filter predicate? collection))?

st3fan22:12:47

This seems too obvious .. maybe i was over thinking this 🙂

dpsutton22:12:40

filter true? might be a bit of a foot gun in lots of code since it only recognizes the boolean true and not all truthy things (the complement of the set #{false nil})

hiredman22:12:27

identity is the usually thing to filter by

leif22:12:40

Okay, another question, is there a function like cljs.spec.alpha/keys but allows you to provide defaults?

leif22:12:40

Like, I want users of the data to be able to rely on the value being there, but the provider of the data can not include it and have the default there.

dpsutton22:12:32

can you give an example usage?

dpsutton22:12:19

this sounds like (merge user-provided-value defaults) but that's only toplevel. i think there are several "deep merge" variants in the wild depending on your particular use cases

leif23:12:44

@dpsutton You're right about the 'deep merge'. Something like this:

dpsutton23:12:38

and your question about spec was a bit confusing. spec should describe the shape/acceptable values. so if they are optional just make them optional in the spec. then merge the stuff in as needed. if you like you can have a second spec that will spec the fully-fleshed datastructure as well

leif23:12:46

(def defaults
  {:options {:color "blue"
             :size "big"
             :menu "top"}
  :items []})

(def curr-db
  (atom {:options {:color "yellow"}}))

(_deep-merge_ @curr-db defaults)
;=>
#_ {:options {:color "yellow"
              :size "big"
              :menu "top"}
    :items []})

leif23:12:37

Ya, I considered having two specs, but that seemed to be repeating myself.

leif23:12:54

Unless there's some sort of splice spec combinator, then I could make an ::items spec and put one in a spec in the :req portion and the other in a spec in the :opt portion.

leif23:12:12

Anyway, thank you all for your help so today. 🙂

parens 3
leif23:12:40

@dpsutton Ah, the reason I bundled the default question with spec is because I'd ideally like to associate some kind of 'default' with each spec.

dpsutton23:12:02

i think you can use conformers to do this but i'm not sure how wise it is

leif23:12:05

That way I can have my pseudo-types and default-values all in one place, rather than having to write down the name twice.

leif23:12:14

Understood.

noisesmith23:12:36

I'm wondering if conformers are relevant here? https://clojuredocs.org/clojure.spec.alpha/conformer

noisesmith23:12:00

my vague understanding is that a spec's conformer can be used to take apropriate data and make the "canonical" spec-conforming version out of it

dpsutton23:12:05

in the conformer i think you can turn strings into UUIDs and other such things so i imagine you can merge things into a map

noisesmith23:12:38

right - that was my thought too, but I haven't used conformers in anger so I'm slightly cautious recommending them here

leif23:12:01

Ah, and is this why it may not be wise?

leif23:12:07

(Or are there other issues too?)

dpsutton23:12:17

we used them at my last job for api shapes but there was a conforming and nonconforming dance to prevent ors and seqs from making them the different shapes

dpsutton23:12:33

and not often in that code so i really need some examples to mimic rather than do it from scratch

noisesmith23:12:41

the suggestion here is not to use conformers this way (and not to use specs to coerce data in general) https://stackoverflow.com/a/49056441

Alex Miller (Clojure team)23:12:26

using conformers this way is imo wrong and you're wanting something out of spec that it was not designed to provide

noisesmith23:12:39

@leif and as I read it, that means you should just make a function that converts the data, outside the spec system

noisesmith23:12:47

or what he said

leif23:12:47

Okay. So basically what I'm hearing is I should role my own DSL for this. 🙂

leif23:12:09

(If I want to write my spec and default in the same place)

noisesmith23:12:11

I think a small handful of functions would suffice, DSL seems a bit much

dpsutton23:12:18

the most straightforward thing seems to be make your merge function and then a spec that specs that?

leif23:12:09

@noisesmith Is it possible to use functions here without repeating yourself?

leif23:12:27

It seems like with functions I have to do something like:

(s/def ::color string?)
(s/def ::size string?)
(s/def ::options (s/keys :req-un [::color ::size]))

(def default-options
  {:color "red"
   :size "large"})

noisesmith23:12:31

clojure functions (when written / factored properly) don't tend to be very repetitive (beyond the kind of repetition that's desirable, for making intent clear)

noisesmith23:12:53

I don't consider that code especially repetitive

leif23:12:13

Note that I had to write color and size 3 times there. Each of which making sense, but I'm following a pattern.

noisesmith23:12:02

the pattern I'm seeing here is you are quite opinionated about how things should be structured, and clojure is too

leif23:12:22

Unless you can make a function like this (using pseudo code here)?:

(defn options [opts]
  (doseq [name, spec, default] in opts:
    (s/def `:~name spec)
   {fn []
     (into {}
           (for [name, spec, default] in opts:
              [name default]))))

noisesmith23:12:48

you'd need a macro for this I think? but yeah, things like this are possible

noisesmith23:12:59

I find they usually obfuscate more than the enable, but yes they work

leif23:12:22

> the pattern I'm seeing here is you are quite opinionated about how things should be structured, and clojure is too Eh, I'm less opinionated then I sound. I've been trying to be less aggressive as I talk, with only mixed success. Sorry about that. 🙂

dpsutton23:12:28

i think one of the design considerations going into spec 2 is to be a bit more manipulable like this.

leif23:12:52

Oh that would be nice.

dpsutton23:12:53

i've never used it so just going off a hazy memory of something i might have read, might have made up 🙂

noisesmith23:12:30

@leif nothing to apologize for, I see most of what I have to offer in this forum as "this is how we'd usually do this in clojure", and if you have a good idea that isn't what we usually do, there's not much more to say there

leif23:12:51

@noisesmith Makes sense. And that's why I'm asking here. Like, I can always go hack on a thing and make it work (for me), but I also want to absorb some of the community's knowledge. If that makes any sense?)

👍 3
leif23:12:39

But ya, y'all've been very helpful in giving me stuff to think about, so I really do appreciate it. ❤️

phronmophobic23:12:53

> if you have a good idea that isn't what we usually do, there's not much more to say there not sure I follow. it seems like being able to evaluate different approaches or adapt existing approaches when the use case demands it is a good thing

phronmophobic23:12:45

I think lisps (and clojure) are a great environment for experimenting with new ideas and designs

noisesmith23:12:02

@smith.adriane as in, I don't go to #beginners to introduce a novel app architecture, and if it works there's no question to answer? I'm trying to talk myself out of the pitfall of spending my time convincing someone to structure an app differently in a channel about learning a new language I guess

👍 3
phronmophobic23:12:53

I could see how, if you're new to clojure, but not new to programming, to not know which questions are which

phronmophobic00:12:30

clojure as a language and as a community has lots of opinions about how programs should be written. especially if you're bringing ideas and practices from another language/ecosystem, I could see how it might be hard to tell if an idea is bad practice, uncommon, or good practice (and it's simply unclear how to implement it because it's a new language)

leif23:12:11

Yup, that makes a lot of sense.