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
Thanks!
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.
Is it doseq
?
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.
So I'm using deps.edn
with shadow-cljs
but I can't seem to get a useful REPL out of it
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
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.
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 what’s your editor? Or just a repl at the command line?
command line
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.
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
What are you trying? There’s extensive documentation. (Oh I see you’ve seen these)
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}}}}
I want to have a cljs repl that's connected to the browser
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
Mmm, not sure how to do that. I'm just doing clj -A:shadow-cljs watch app
which I'm assuming starts a regular clojure repl instead of a cljs one
do you get a repl prompt when running that?
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
@dpsutton not really, but it does say it's started an nREPL server on the port I gave it
ok. that's a good start. there's probably a webserver running. do you see any information about that?
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)
which is the typical shadow-cljs output
cool. open up http://localhost:3000 in a browser
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)
So i'm assuming it means the repl wasn't set up properly
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
If I do (js/alert
with the build window open it should work, right? An alert should pop up?
Oh wait, so the browser console isn't connected to the nREPL instance after all. It's just a regular browser console.
That's why (js/alert)
wasn't working.
(also, do you want to do this in a thread?)
(thanks btw)
of course. you're welcome. what's left to do?
i imagine in your terminal you now have a cljs repl
Not yet. It's still purely a Clojure REPL.
I'm not sure how this all works out under the hood. Is it because I'm using clj
to start the nREPL server?
do you have a prompt where you can evaluate things?
If I connect to it via lein repl
, yes
but otherwise, calling my clj
command doesn't give me a REPL
ok i misunderstood. i thought there was a repl
run shadow-cljs cljs-repl app
from another terminal
which will connect a client to the running watch
ohhh wow I can't believe it was that simple
so to connect to a running cljs repl you need to use shadow-cljs
instead of clj
, got it
It works, I can send commands to the browser now!
Thanks @dpsutton it works now!
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
@webmaster601 Which library is that?
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).
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...
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.
Yup, very likely my recommendation.
What is the library that is ten years old?
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.
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.
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).
ah, good to know!
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!)
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.
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...
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...
I think you need to declare :factory name
and then call that factory function
doseq
if you need bindings and an arbitrary body, run!
if you have a function that should be called for each element in order
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?
those two are great and I used them both in different context during presenting clojure to colleagues
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. 👍
I've found clojupyter to work great. Easy to set up (on linux at least).
Fantastic, thanks! Obviously the net is full of stuff, but having a recommendation (not from an algorithm...) makes all the difference. 😄👍
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?
Thanks a lot for your answer - it was a great help :thumbsup:
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]
I think I am hitting a problem with multimethod dispatch and it has to be exact (inheritance ignored).
A zipentry is not an inputsream
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
yes you are right, i should use zipstream for io/copy
and .getNextEntry
to move the cursor between
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.
Is there a way to override the JAVA_CMD
for clj?
For what OS and/or shell?
linux/bash
I’m a bit rusty in bash, but I think you should be able to do JAVA_CMD=/some/other/java clj ...
or you could look at jenv
or something similar to more easily switch between java versions
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
clj uses JAVA_HOME if you have that set
yeah, that won't work. set JAVA_HOME
that doesn't work either
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 clj
Clojure 1.10.1
user=> (System/getProperty "java.version")
"1.8.0_275"
my Bash isn't perfect either but I think the JAVA_HOME test is only reachable if JAVA_CMD isn't set
and JAVA_CMD is set if a java executable is found
how about this: PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
clj
straight-up hacking at this point 😉
That's just cheating 🙂 I updated to a newer version of cli tools let's see
well either set the java
you want on your PATH, or set none and use JAVA_HOME
jenv will work with the former
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
how is setting JAVA_CMD different than setting PATH or JAVA_HOME?
those all feel like identical things configured in the same way from the command line
I'm not actually disagreeing with your suggestion, just trying to probe the assumptions a bit
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.
That’s the code snippet he posted above.
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
?
if there's a -main
function in namespace advent.core
you should be able to do clj -M -m advent.core
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
is it possible to create an alias to handle that for me?
yeah, I thought about -X
, but I want to be able to say clj -M ... 2
and then it runs day 2's code
nevermind, I have figured out the alias: :main {:main-opts ["-m" "advent.core"]}
of course. make an alias with :main-opts
(https://clojure.org/reference/deps_and_cli#_main_execution)
thank you so much for the help
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
interesting, that's good to know
gotta pass in the args like -X blah '{:day 1}'
, right?
ope, found it. idk why im' struggling so much to read this page, lol
clj -X:my-fn '[:my :data]' 789
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
But it looks like jenv will be a better option, all things considered
we have a ticket for this suggestion already, I'm trying to vet whether having yet one more degree of freedom is necessary
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?
more degrees of freedom is inherently more complex, so I'm biased against it :)
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
Integer/parseInt
is not a function, you can't use it as a value
#(Integer/parseInt %)
thanks
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)
(or (run! (comp f g) l)
, (mapv (com f g) l)
etc.)
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
@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
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
that makes sense. also probably explains why I can’t do (type Integer/parseInt)
(which is what I’d normally do in this situation)
right, it's not a "thing" - it's some action known to a thing
http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html
somewhat confusingly the a symbol like Integer/parseInt
(a namespace symbol) might be interpreted in a number of different ways
(Integer/parseInt "1")
is invoking the parseInt static method on Integer
Math/PI
is a reference to a static field PI
on the class Math
clojure.core/+
is the value of +
defined in clojure.core
yep, and I can do both type
and source
on that one
the latter two are "values", they can be passed around as arguments to functions, etc
Integer/parseInt
only means something when it is literally the first element of a list like (Integer/parseInt "1")
so is that a special form? or something else?
it is sort of syntax sugar
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
exception thrown from the form (map Integer/parseInt ["1"])
should make it clear
user=> (macroexpand `(Integer/parseInt "1"))
(. java.lang.Integer parseInt "1")
user=>
(it isn't a macro, it just happens that macroexpansion de-sugars it)
btw, more or less complete guide can be found here - https://clojure.org/reference/java_interop
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.
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?
the interop sugar can also lead to potentially confusing exceptions to clojure's syntax rules
user=> (= Math/PI (Math/PI))
true
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=>
Is there any way to destructure atoms in functions in clojure?
Like, the same way you can destructure arrays and maps
(Like this, but for atoms https://clojure.org/guides/destructuring)
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
so eg. if you have (def a (atom [1 2 3]))
you can do (let [[a b c] @a] b)
as for atoms inside other structures: don't, it's not useful
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)))
?
Oh, don't put atoms in data structures?
yeah, atoms inside data structures is an antipattern
I mean - there might be some case where it's the best option, but I can't recall seeing one
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?
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
a set of atoms sounds fundamentally unmanageable
@leif yeah, the usual thing is a single atom holding a hash-map
(if you meant that as a literal set instead of just colloquially "some atoms")
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
Err.. I meant more of a map of atoms.
Like: {:file (atom "") :color (atom "")}
right, the normal thing is (atom {:file "" :color ""})
But it sounds like instead I should do:
(atom {:file ""
:color ""})
i figured, just wanted to catch that in the case it was actually meant 🙂.
lol, fair, thanks.
Okay, so what should I do if I want one thing to reference another in the same data structure.
Like...say this:
{:items [_a_ _b_ _c_]
:selected _c_}
swap!
takes a function that receives the atom's state and returns a new state
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
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?
i don't know what you mean by "rely on it"
having one atom makes coherent state easier to enforce, not harder
you can set a "validation" function to fix or reject changes for example
i think "possible" versus "impossible"
you can't have a validator that looks at multiple atoms, you can have one that manages a single atom
well - you could in an ad-hoc way but...
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 updatedAnd I didn't have to manually do any sort of dependency resolution to find other _c_
s in the state.
you can't reset a key
@noisesmith Right, I didn't reset the key, I dereferenced _c_
using a key.
oh I misunderstood
but I don't see how multiple atoms makes this any easier
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?
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.
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*_}
err..that's slightly wrong, actually:
make "current" a path into items rather than a "copy"
in this case I'd use an index, yes
(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)})
or have current be the thing being edited and on commit updated items. couple ways to model this
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
@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.
@dpsutton Same with making :current
a path.
@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
but using multiple atoms is not how any normal clojure codebase does things
clojure data structures are designed to hold immutable values, and atoms (though luckily not equal by value), are mutable
@smith.adriane So you're suggesting using subscriptions to watch for changes in _current_
?
@noisesmith Think lenses.
that would be a pretty big change, so I'm not sure I would recommend it without knowing more about your project and goals
Like, the transformation should be purely functional
But the code should only need to deal with a small part of the data structure.
@leif lenses, as I've seen them are an abstraction over immutable values
And the rest should change with it.
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
@leif my experience with clojure doesn't prove you're idea doesn't work, but I can say your design is "weird" for clojure
@noisesmith Fair.
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. 🙂
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)
@smith.adriane Right. Although that seems more like handling the view rather than the view-state. (Which I'm already doing. 😉 )
Yes it is. 🙂
Just so we're clear, I'm not insisting on doing it one particular way.
Like, the things I want are:
I'm pretty interested in functional solutions. I would love any links to references if you have them handy
1. A big immutable data structure for my applications state.
2. To have code that does operations on small parts of that state.
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.
I'm happy to do it the idiomatic way in clojure, if there is one. 🙂
(Unless you think that's unreasonable?)
(If so I'm also always happy to learn more.)
I guess another way of putting it: I want to do DAG manipulation with clojure.
And right now the only way of doing it in clojure that I'm aware of is using atoms.
Everything else seems to be purely tree based.
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.
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
You might look at structuring your state more like a database (a flat set of tuples) instead of an in memory graph of objects
So using something like datascript
@smith.adriane Ya, what I want is similar to what re-frame does. Taking a look at hoplin/javelin now.
@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.
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
good reference, that's a classic
lol, fair.
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.
not sure I follow
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
Is there a good alternative for (count (filter true? (map some-predicate? collection)))
?
this seems like a common thing
@francesco.losciale it seems that they take nil
as the only valid leaf
(: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
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
@st3fan (count (filter predicate? collection))
?
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}
)
This seems too obvious .. maybe i was over thinking this 🙂
identity is the usually thing to filter by
Okay, another question, is there a function like cljs.spec.alpha/keys
but allows you to provide defaults?
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.
can you give an example usage?
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
@dpsutton You're right about the 'deep merge'. Something like this:
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
(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 []})
Ya, I considered having two specs, but that seemed to be repeating myself.
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.
Anyway, thank you all for your help so today. 🙂
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.
@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.
i think you can use conformers to do this but i'm not sure how wise it is
That way I can have my pseudo-types and default-values all in one place, rather than having to write down the name twice.
Understood.
I'm wondering if conformers are relevant here? https://clojuredocs.org/clojure.spec.alpha/conformer
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
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
right - that was my thought too, but I haven't used conformers in anger so I'm slightly cautious recommending them here
Ah, and is this why it may not be wise?
(Or are there other issues too?)
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
and not often in that code so i really need some examples to mimic rather than do it from scratch
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
using conformers this way is imo wrong and you're wanting something out of spec that it was not designed to provide
@leif and as I read it, that means you should just make a function that converts the data, outside the spec system
or what he said
Okay. So basically what I'm hearing is I should role my own DSL for this. 🙂
(If I want to write my spec and default in the same place)
I think a small handful of functions would suffice, DSL seems a bit much
the most straightforward thing seems to be make your merge function and then a spec that specs that?
@noisesmith Is it possible to use functions here without repeating yourself?
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"})
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)
I don't consider that code especially repetitive
Note that I had to write color
and size
3 times there. Each of which making sense, but I'm following a pattern.
the pattern I'm seeing here is you are quite opinionated about how things should be structured, and clojure is too
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]))))
you'd need a macro for this I think? but yeah, things like this are possible
I find they usually obfuscate more than the enable, but yes they work
> 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. 🙂
i think one of the design considerations going into spec 2 is to be a bit more manipulable like this.
Oh that would be nice.
i've never used it so just going off a hazy memory of something i might have read, might have made up 🙂
@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
well put
@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?)
But ya, y'all've been very helpful in giving me stuff to think about, so I really do appreciate it. ❤️
> 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
I think lisps (and clojure) are a great environment for experimenting with new ideas and designs
@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
makes sense
Yup, that makes a lot of sense.
I could see how, if you're new to clojure, but not new to programming, to not know which questions are which