beginners

Getting started with Clojure/ClojureScript? Welcome! Also try: https://ask.clojure.org. Check out resources at https://gist.github.com/yogthos/be323be0361c589570a6da4ccc85f58f.
st3fan 2020-12-02T01:40:04.169900Z

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

➕ 1
Alex 2020-12-02T01:46:24.170Z

Thanks!

st3fan 2020-12-02T02:01:48.170800Z

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.

st3fan 2020-12-02T02:03:00.171Z

Is it doseq ?

zach 2020-12-02T02:47:39.174200Z

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.

2020-12-02T02:51:10.175700Z

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

dpsutton 2020-12-02T02:51:29.176400Z

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

zach 2020-12-02T02:51:53.176500Z

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.

2020-12-02T02:52:17.177100Z

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"]}}}

dpsutton 2020-12-02T02:52:22.177400Z

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

2020-12-02T02:52:36.177700Z

command line

zach 2020-12-02T02:52:43.178Z

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.

2020-12-02T02:53:17.179Z

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

dpsutton 2020-12-02T02:53:46.179500Z

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

2020-12-02T02:53:54.179800Z

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}}}}

2020-12-02T02:54:20.181Z

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

dpsutton 2020-12-02T02:54:39.181900Z

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

2020-12-02T02:56:10.182500Z

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

2020-12-02T02:56:47.182900Z

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

dpsutton 2020-12-02T02:58:41.183400Z

do you get a repl prompt when running that?

2020-12-02T02:59:43.184Z

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

2020-12-02T03:00:10.184500Z

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

dpsutton 2020-12-02T03:00:34.185Z

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

2020-12-02T03:01:14.185400Z

This is the output

> clj -A:shadow-cljs watch app
shadow-cljs - HTTP server available at <http://localhost:3000>
shadow-cljs - server version: 2.11.8 running at <http://localhost:9630>
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)

2020-12-02T03:01:33.185900Z

which is the typical shadow-cljs output

dpsutton 2020-12-02T03:01:40.186400Z

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

2020-12-02T03:03:09.187300Z

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)

2020-12-02T03:03:19.187600Z

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

2020-12-02T03:04:45.188800Z

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

2020-12-02T03:05:23.189600Z

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

2020-12-02T03:06:36.190300Z

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

2020-12-02T03:07:10.190900Z

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

2020-12-02T03:07:16.191100Z

(thanks btw)

dpsutton 2020-12-02T03:09:11.191200Z

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

dpsutton 2020-12-02T03:09:21.191400Z

i imagine in your terminal you now have a cljs repl

2020-12-02T03:10:18.191600Z

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

2020-12-02T03:10:36.191800Z

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

dpsutton 2020-12-02T03:13:01.192Z

do you have a prompt where you can evaluate things?

2020-12-02T03:14:41.192200Z

If I connect to it via lein repl, yes

2020-12-02T03:15:16.192400Z

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

dpsutton 2020-12-02T03:15:38.192600Z

ok i misunderstood. i thought there was a repl

dpsutton 2020-12-02T03:17:08.192800Z

run shadow-cljs cljs-repl app from another terminal

🎯 1
dpsutton 2020-12-02T03:17:21.193Z

which will connect a client to the running watch

2020-12-02T03:18:43.193200Z

ohhh wow I can't believe it was that simple

2020-12-02T03:19:09.193400Z

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

2020-12-02T03:19:30.193600Z

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

2020-12-02T03:20:06.194Z

Thanks @dpsutton it works now!

dpsutton 2020-12-02T03:23:34.194200Z

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

1
seancorfield 2020-12-02T03:40:12.194500Z

@webmaster601 Which library is that?

seancorfield 2020-12-02T03:41:58.194700Z

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).

seancorfield 2020-12-02T03:45:45.194900Z

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...

zach 2020-12-02T03:47:03.195100Z

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.

seancorfield 2020-12-02T03:47:43.195300Z

Yup, very likely my recommendation.

seancorfield 2020-12-02T03:47:56.195500Z

What is the library that is ten years old?

zach 2020-12-02T03:48:34.195700Z

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.

zach 2020-12-02T03:52:54.196200Z

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.

seancorfield 2020-12-02T03:54:22.196500Z

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).

zach 2020-12-02T03:55:49.196700Z

ah, good to know!

zach 2020-12-02T03:56:14.196900Z

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!)

seancorfield 2020-12-02T03:58:36.197100Z

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.

seancorfield 2020-12-02T04:00:38.197300Z

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...

😁 1
mbjarland 2020-12-02T06:55:38.198Z

And you are supposed to be able to call the constructor from within the ns where the class is defined? As in (ClassName. Args)? I must have had something else broken with my setup then...

2020-12-02T07:05:32.198600Z

I think you need to declare :factory name and then call that factory function

2020-12-02T07:06:41.198800Z

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

2020-12-02T10:05:47.202600Z

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?

2020-12-02T10:22:02.202900Z

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

2020-12-02T10:30:22.203100Z

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. 👍

2020-12-02T10:36:22.203400Z

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

2020-12-02T10:38:56.205200Z

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

👌 1
Lisbeth Ammitzbøll Hansen 2020-12-02T11:03:27.205700Z

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?

Lisbeth Ammitzbøll Hansen 2020-12-03T08:27:41.382900Z

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

rmxm 2020-12-02T11:37:10.207400Z

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 <http://java.io|java.io>.BufferedWriter]

rmxm 2020-12-02T11:39:32.208700Z

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

2020-12-02T11:42:33.209400Z

A zipentry is not an inputsream

2020-12-02T11:43:39.211100Z

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

rmxm 2020-12-02T11:49:16.212200Z

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

practicalli-john 2020-12-02T13:21:51.212900Z

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.

Ben Sless 2020-12-02T15:36:22.215300Z

Is there a way to override the JAVA_CMD for clj?

2020-12-02T15:39:18.215600Z

For what OS and/or shell?

Ben Sless 2020-12-02T15:39:34.215900Z

linux/bash

2020-12-02T15:41:56.217100Z

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

2020-12-02T15:43:06.217600Z

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

Ben Sless 2020-12-02T15:43:32.217900Z

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" ]] &amp;&amp; [[ -x "$JAVA_HOME/bin/java" ]]; then
    JAVA_CMD="$JAVA_HOME/bin/java"
  else
    &gt;&amp;2 echo "Couldn't find 'java'. Please set JAVA_HOME."
    exit 1
  fi
fi

alexmiller 2020-12-02T15:43:49.218300Z

clj uses JAVA_HOME if you have that set

alexmiller 2020-12-02T15:44:25.218400Z

yeah, that won't work. set JAVA_HOME

Ben Sless 2020-12-02T15:44:56.218600Z

that doesn't work either

Ben Sless 2020-12-02T15:45:04.218800Z

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

Ben Sless 2020-12-02T15:45:46.219Z

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

Ben Sless 2020-12-02T15:45:58.219200Z

and JAVA_CMD is set if a java executable is found

2020-12-02T15:47:07.219400Z

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

2020-12-02T15:47:53.219600Z

straight-up hacking at this point 😉

Ben Sless 2020-12-02T15:48:08.219800Z

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

alexmiller 2020-12-02T15:48:14.220Z

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

alexmiller 2020-12-02T15:49:10.220200Z

jenv will work with the former

Ben Sless 2020-12-02T15:51:14.220400Z

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

alexmiller 2020-12-02T15:59:41.220600Z

how is setting JAVA_CMD different than setting PATH or JAVA_HOME?

alexmiller 2020-12-02T16:00:10.220800Z

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

alexmiller 2020-12-02T16:04:04.221Z

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

2020-12-02T16:09:01.221200Z

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.

2020-12-02T16:09:59.221400Z

That’s the code snippet he posted above.

NoahTheDuke 2020-12-02T16:17:14.223500Z

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?

dpsutton 2020-12-02T16:19:16.224600Z

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

dpsutton 2020-12-02T16:20:40.225400Z

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

NoahTheDuke 2020-12-02T16:21:24.226300Z

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

NoahTheDuke 2020-12-02T16:21:48.227Z

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

NoahTheDuke 2020-12-02T16:23:03.227600Z

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

dpsutton 2020-12-02T16:23:05.227800Z

of course. make an alias with :main-opts (https://clojure.org/reference/deps_and_cli#_main_execution)

🎉 1
NoahTheDuke 2020-12-02T16:23:43.228200Z

thank you so much for the help

dpsutton 2020-12-02T16:24:57.229500Z

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

NoahTheDuke 2020-12-02T16:28:50.229800Z

interesting, that's good to know

NoahTheDuke 2020-12-02T16:29:30.230600Z

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

NoahTheDuke 2020-12-02T16:30:23.230900Z

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

NoahTheDuke 2020-12-02T16:30:24.231100Z

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

Ben Sless 2020-12-02T16:33:31.231200Z

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 Sless 2020-12-02T16:34:58.231400Z

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

alexmiller 2020-12-02T16:38:25.231600Z

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

Ben Sless 2020-12-02T16:50:59.232100Z

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?

alexmiller 2020-12-02T17:02:23.232400Z

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

2020-12-02T17:38:57.232700Z

what am I doing wrong?

user=&gt; (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=&gt; (Integer/parseInt "1")
1

bronsa 2020-12-02T17:41:10.233100Z

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

bronsa 2020-12-02T17:41:21.233400Z

#(Integer/parseInt %)

2020-12-02T17:45:13.233600Z

thanks

2020-12-02T18:10:01.234Z

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)

2020-12-02T18:10:32.234200Z

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

2020-12-02T18:11:28.234500Z

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

2020-12-02T18:16:29.236100Z

@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

2020-12-02T18:17:20.237100Z

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

😂 1
2020-12-02T18:17:41.237700Z

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

2020-12-02T18:18:36.238600Z

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

2020-12-02T18:18:48.239100Z

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

2020-12-02T18:19:16.239600Z

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

2020-12-02T18:19:59.240500Z

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

2020-12-02T18:20:50.241300Z

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

2020-12-02T18:21:14.241700Z

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

2020-12-02T18:21:20.242Z

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

2020-12-02T18:22:29.243Z

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

2020-12-02T18:23:49.244200Z

so is that a special form? or something else?

2020-12-02T18:24:05.244800Z

it is sort of syntax sugar

2020-12-02T18:24:10.245Z

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

2020-12-02T18:24:52.245800Z

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

2020-12-02T18:24:58.246100Z

user=&gt; (macroexpand `(Integer/parseInt "1"))
(. java.lang.Integer parseInt "1")
user=&gt;

2020-12-02T18:26:01.246700Z

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

😮 1
2020-12-02T18:27:27.247100Z

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

2020-12-02T18:32:34.249700Z

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.

2020-12-02T18:33:45.250800Z

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?

2020-12-02T18:41:36.251600Z

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

user=&gt; (= Math/PI (Math/PI))
true

2020-12-02T18:45:12.251900Z

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

2020-12-02T20:19:36.256400Z

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

2020-12-02T20:19:44.256700Z

Like, the same way you can destructure arrays and maps

2020-12-02T20:19:58.256900Z

(Like this, but for atoms https://clojure.org/guides/destructuring)

2020-12-02T20:20:31.257300Z

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

2020-12-02T20:21:29.259100Z

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

2020-12-02T20:22:27.260100Z

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

2020-12-02T20:22:45.260500Z

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)))

2020-12-02T20:22:47.260700Z

?

2020-12-02T20:23:06.261200Z

Oh, don't put atoms in data structures?

2020-12-02T20:23:06.261300Z

yeah, atoms inside data structures is an antipattern

2020-12-02T20:23:47.262300Z

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

2020-12-02T20:24:27.263600Z

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?

2020-12-02T20:24:36.263800Z

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

dpsutton 2020-12-02T20:24:57.264400Z

a set of atoms sounds fundamentally unmanageable

2020-12-02T20:24:59.264500Z

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

dpsutton 2020-12-02T20:25:25.265100Z

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

2020-12-02T20:25:38.265600Z

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

2020-12-02T20:25:39.265700Z

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

2020-12-02T20:26:00.266400Z

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

2020-12-02T20:26:22.267600Z

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

2020-12-02T20:26:22.267700Z

But it sounds like instead I should do:

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

dpsutton 2020-12-02T20:26:26.267800Z

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

2020-12-02T20:26:38.268200Z

lol, fair, thanks.

2020-12-02T20:26:53.268800Z

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

2020-12-02T20:27:33.269800Z

Like...say this:

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

dpsutton 2020-12-02T20:27:39.270100Z

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

dpsutton 2020-12-02T20:28:21.271300Z

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

2020-12-02T20:28:22.271400Z

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?

dpsutton 2020-12-02T20:28:56.271900Z

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

2020-12-02T20:29:09.272500Z

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

2020-12-02T20:29:21.273100Z

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

dpsutton 2020-12-02T20:29:28.273500Z

i think "possible" versus "impossible"

2020-12-02T20:29:51.274300Z

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

2020-12-02T20:30:09.274800Z

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

2020-12-02T20:30:16.275Z

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

2020-12-02T20:30:36.275700Z

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

2020-12-02T20:30:36.275800Z

you can't reset a key

2020-12-02T20:31:13.276700Z

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

2020-12-02T20:31:17.276900Z

oh I misunderstood

2020-12-02T20:31:54.277400Z

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

2020-12-02T20:32:42.278400Z

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?

2020-12-02T20:34:08.280600Z

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.

2020-12-02T20:35:26.282300Z

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*_}

2020-12-02T20:36:13.282800Z

err..that's slightly wrong, actually:

dpsutton 2020-12-02T20:37:40.284600Z

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

2020-12-02T20:38:15.286200Z

in this case I'd use an index, yes

2020-12-02T20:38:39.287300Z

(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)})

dpsutton 2020-12-02T20:38:40.287400Z

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

phronmophobic 2020-12-02T20:38:59.288Z

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

2020-12-02T20:40:04.289100Z

@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.

2020-12-02T20:40:42.290100Z

@dpsutton Same with making :current a path.

2020-12-02T20:41:03.290800Z

@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

2020-12-02T20:41:23.291700Z

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

2020-12-02T20:42:03.292900Z

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

2020-12-02T20:42:03.293Z

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

2020-12-02T20:42:46.293800Z

@noisesmith Think lenses.

phronmophobic 2020-12-02T20:42:48.294Z

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

2020-12-02T20:42:56.294400Z

Like, the transformation should be purely functional

2020-12-02T20:43:14.295400Z

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

2020-12-02T20:43:16.295600Z

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

2020-12-02T20:43:20.295900Z

And the rest should change with it.

phronmophobic 2020-12-02T20:43:50.296600Z

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

2020-12-02T20:44:25.297300Z

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

2020-12-02T20:44:55.297500Z

@noisesmith Fair.

2020-12-02T20:45:33.298700Z

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. 🙂

2020-12-02T20:46:09.299700Z

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)

2020-12-02T20:46:28.300200Z

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

2020-12-02T20:46:46.300400Z

Yes it is. 🙂

2020-12-02T20:47:03.300800Z

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

2020-12-02T20:47:11.301100Z

Like, the things I want are:

phronmophobic 2020-12-02T20:47:13.301400Z

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

2020-12-02T20:47:26.301800Z

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

2020-12-02T20:47:47.302300Z

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

2020-12-02T20:48:36.303200Z

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.

2020-12-02T20:49:02.303700Z

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

2020-12-02T20:49:11.303900Z

(Unless you think that's unreasonable?)

2020-12-02T20:49:19.304200Z

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

2020-12-02T20:50:26.304900Z

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

2020-12-02T20:50:43.305500Z

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

2020-12-02T20:51:01.305900Z

Everything else seems to be purely tree based.

2020-12-02T20:51:54.307100Z

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.

phronmophobic 2020-12-02T20:53:02.309200Z

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

2020-12-02T20:53:09.309500Z

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

💯 1
2020-12-02T20:53:30.310300Z

So using something like datascript

2020-12-02T20:54:52.311200Z

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

2020-12-02T20:56:44.312400Z

@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.

2020-12-02T21:02:20.312700Z

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

NoahTheDuke 2020-12-02T21:03:31.313Z

good reference, that's a classic

2020-12-02T21:03:45.313200Z

lol, fair.

2020-12-02T21:04:12.313400Z

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.

phronmophobic 2020-12-02T21:07:03.313600Z

not sure I follow

gibi 2020-12-02T22:25:14.314900Z

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

st3fan 2020-12-02T22:26:43.315800Z

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

st3fan 2020-12-02T22:27:28.316100Z

this seems like a common thing

borkdude 2020-12-02T22:28:52.317100Z

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

👍 1
dpsutton 2020-12-02T22:29:03.317200Z

(: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

dpsutton 2020-12-02T22:31:19.318300Z

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

👍 1
borkdude 2020-12-02T22:31:20.318500Z

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

dpsutton 2020-12-02T22:32:40.319500Z

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})

st3fan 2020-12-02T22:36:47.319800Z

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

2020-12-02T22:39:27.320200Z

identity is the usually thing to filter by

2020-12-02T22:53:40.321800Z

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

2020-12-02T22:54:40.322900Z

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.

dpsutton 2020-12-02T22:58:32.323100Z

can you give an example usage?

dpsutton 2020-12-02T22:59:19.323900Z

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

2020-12-02T23:07:44.324400Z

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

dpsutton 2020-12-02T23:09:38.327600Z

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

2020-12-02T23:10:46.328900Z

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

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

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

2020-12-02T23:11:37.329500Z

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

2020-12-02T23:12:54.330900Z

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.

2020-12-02T23:13:12.331200Z

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

1
2020-12-02T23:31:25.331700Z

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=&gt; (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.

2020-12-02T23:35:40.332900Z

@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.

dpsutton 2020-12-02T23:36:02.333600Z

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

2020-12-02T23:36:05.333800Z

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

2020-12-02T23:36:14.334Z

Understood.

2020-12-02T23:36:36.334600Z

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

2020-12-02T23:37:00.335600Z

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

dpsutton 2020-12-02T23:37:05.335800Z

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

2020-12-02T23:37:38.336400Z

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

2020-12-02T23:38:01.337Z

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

2020-12-02T23:38:07.337400Z

(Or are there other issues too?)

dpsutton 2020-12-02T23:38:17.337700Z

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

dpsutton 2020-12-02T23:38:33.338300Z

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

2020-12-02T23:38:41.338500Z

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

alexmiller 2020-12-02T23:39:26.339500Z

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

2020-12-02T23:39:39.340Z

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

2020-12-02T23:39:47.340300Z

or what he said

2020-12-02T23:39:47.340400Z

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

2020-12-02T23:40:09.341200Z

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

2020-12-02T23:40:11.341500Z

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

dpsutton 2020-12-02T23:40:18.341700Z

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

2020-12-02T23:41:09.342300Z

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

2020-12-02T23:42:27.344800Z

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"})

2020-12-02T23:42:31.345Z

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)

2020-12-02T23:42:53.345700Z

I don't consider that code especially repetitive

2020-12-02T23:43:13.346200Z

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

2020-12-02T23:47:02.350600Z

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

2020-12-02T23:47:22.351200Z

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]))))

2020-12-02T23:47:48.351900Z

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

2020-12-02T23:47:59.352400Z

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

2020-12-02T23:48:22.353500Z

> 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. 🙂

dpsutton 2020-12-02T23:48:28.353600Z

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

2020-12-02T23:48:52.354400Z

Oh that would be nice.

dpsutton 2020-12-02T23:48:53.354500Z

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

2020-12-02T23:49:30.355100Z

@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

dpsutton 2020-12-02T23:49:56.355500Z

well put

2020-12-02T23:50:51.356800Z

@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?)

👍 1
2020-12-02T23:51:39.357900Z

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

phronmophobic 2020-12-02T23:51:53.358300Z

> 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

phronmophobic 2020-12-02T23:54:45.360800Z

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

2020-12-02T23:56:02.361800Z

@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

👍 1
phronmophobic 2020-12-02T23:57:00.362Z

makes sense

2020-12-02T23:59:11.362400Z

Yup, that makes a lot of sense.

phronmophobic 2020-12-02T23:59:53.362500Z

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