Fork me on GitHub
#beginners
<
2024-07-02
>
Melody04:07:29

This is just a snippet and I can share more code if it seems helpful but can anyone help me potentially debug this time based application? This is my first foray into using time in clojure at all and I at least got it to display the current time, but while the time will update in the REPL with this code, it does not update on the seesaw UI that I am using.

(ns op1.timer
  (:require [seesaw.core :as s])
  (:import [java.time Instant ZoneId]
           [java.time.format DateTimeFormatter]
           [java.util Timer TimerTask]))

(defn format-instant [instant]
  (let [formatter (DateTimeFormatter/ofPattern "yyyy-MM-dd HH:mm:ss")
        zoned-date-time (.atZone instant (ZoneId/systemDefault))]
    (.format formatter zoned-date-time)))

(defn create-updating-clock [] 
  (let [clock-label (s/label) 
        timer (Timer. true) 
        update-task (proxy [TimerTask] [] 
                      (run [] (let [formatted-time (format-instant (Instant/now))] 
                                (println "Updating clock to:" formatted-time) 
                                (s/invoke-later #(s/text! clock-label formatted-time)))))] 
    (.scheduleAtFixedRate timer update-task 0 1000) 
    clock-label))

(defn clock-panel []
  (let [clock (create-updating-clock)]
    (s/vertical-panel
     :items ["Current Time:" clock]
     :background :cyan)))
Among my other functions this code will compile and seems to be working, but the time is either not shown at all in the GUI or I can use (str clock) in the clock-panel in order to return a literal toString representation of the java object of the time label (at least I am pretty sure that is what it is). "Current Time:seesaw.core.proxy$javax.swing.JLabel$Tag$fd4071[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.0,border=,flags=8388608,maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon=,horizontalAlignment=LEADING,horizontalTextPosition=TRAILING,iconTextGap=4,labelFor=,text=,verticalAlignment=CENTER,verticalTextPosition=CENTER]" ^prints onto the GUI if I use str

Bob B04:07:46

invoke-later takes a body which it wraps in a function, so essentially the code as-is creates a function that returns the anonymous function (but doesn't invoke it). Deleting the # on thee (s/text! ....) call should bring some progress (based on a bit of local fiddling)

Melody22:07:52

I actually did try to remove the # but I forget what that did, I will try again. I am excited to try and get it working.

Melody00:07:05

(defn create-updating-clock [] 
  (let [clock-label (s/label) 
        timer (Timer. true) 
        update-task (proxy [TimerTask] [] 
                      (run [] (let [formatted-time (format-instant (Instant/now))] 
                                (println "Updating clock to:" formatted-time) 
                                (s/invoke-later (s/value! clock-label formatted-time)))))] 
    (.scheduleAtFixedRate timer update-task 0 1000) 
    clock-label))
This works!

Jim Newton06:07:27

I have a question about organizing a project. I have a library that i'm working on, you can find it here https://github.com/jimka2001/heavybool/tree/main/clojure The useful part of the library is pretty small, but the largest part of files in the repository are examples and testing files. What is the best way to present that to users? Currently it is organized with all the code in a common src directory. I was thinking of creating an examples directory either within src or parallel to src and test. Has anyone developed a similar library before. How do you distinguish between useful cod and example code?

Jim Newton06:07:28

In a slightly larger context. The clojure code is part of a larger research project. I'm developing the same code in several languages, Python, Scala, Common Lisp, and Clojure. One of the things I wish to present in the research is which sorts of things are easier in some languages and more difficult in others. Therefore the git repository has directories for each of these language implementations.

Ludger Solbach07:07:13

It separates src from test and also the different languages.

Jim Newton07:07:30

@U017HM6BG07 does that handle the issue of main source vs examples?

Jim Newton07:07:05

@U017HM6BG07 I'm not sure whether it is true, but the intro page https://maven.apache.org/what-is-maven.html says maven is for java based projects. My project is not limited to JVM-based languages.

adi08:07:43

examples in parallel with src and test makes more sense because examples can be potentially anything, requiring their own dependencies among other things ... keeping examples distinct from src will also sidestep potential build annoyances if one wants to exclude examples code from the published library

adi08:07:57

and generally, from a library user's perspective, I would use the library APIs from "somewhere else" (i.e. not from within the source repo's src directory)

Jim Newton08:07:01

@U051MHSEK, This relates to a question I just posted elsewhere (https://clojurians.slack.com/archives/C053AK3F9/p1719907735988699) If I make the examples directory parallel to src and test then I don't understand how namespace names and requires work. because src and test seem to work implicitly.

Jim Newton08:07:58

@U051MHSEK, how should example code require the library api, and how do I write tests for example code?

adi08:07:37

Given the reference to project.clj, I assume you are using Leiningen. The sample project file is a handy reference. I think this bit on configuring file system paths should help: https://github.com/technomancy/leiningen/blob/master/sample.project.clj#L308 Also have a look at the sections on aliases and profiles in that file.

adi08:07:49

So you could potentially have:

src
test
examples/examples
        /examples-tests
and namespace accordingly

mmer08:07:25

As a side question - why would you use leinigen rather than the deps.edn?

Jim Newton08:07:55

question about the project.clj file. This file does not seem to indicate which directory the project source code is found in. In my case it is in a subdirectory named src and the tests are in a subdirectory named test. with that setup, my source code and test code can require other files from the project with something like (ns ... (:require [name ...])) and I dont have to mention src as (ns ... (:require [src.name ...])) Why is this? Is the name src, special?

daveliepmann08:07:55

not sure why src is special but I believe the override is :source-paths https://github.com/technomancy/leiningen/blob/github/sample.project.clj#L311 (I don't know of a better "API doc" for leiningen)

daveliepmann08:07:54

hmm, also https://github.com/technomancy/leiningen/blob/24fb93936133bd7fc30c393c127e9e69bb5f2392/doc/MIXED_PROJECTS.md?plain=1#L23 > By default, Leiningen assumes your project only has Clojure source code under > src.

Jim Newton08:07:16

@U05092LD5 are you suggesting that if I include :source-paths ["src" "examples"] then I can just put the files in those directories without having to qualify namespace names with examples.this-and-that ?

daveliepmann08:07:40

sorry, I haven't used that option in years. sounds plausible though, worth a try

1
Ben Sless08:07:42

You might want to add the examples under the dev profile in lein such that they won't be distributed along with the library source

Jim Newton08:07:33

@UK0810AQ2 > You might want to add the examples under the dev profile in lein such that they won't be distributed along with the library source Sorry, I don't understand what you mean by dev profile

Jim Newton08:07:57

@UK0810AQ2 I'm happy to have the examples and tests distributed.

Jim Newton08:07:11

@U05092LD5 > :source-paths ["src" "examples"] This seems to work according to my initial experiments. However, quickdoc doesn't find the code now. I'm not sure if that's good or bad.

👍 1
Jim Newton09:07:01

Can someone help me understand what it means to say: yielding non-public def This comes from the docstring of defn- . What things am I supposed to be able to do with a non-public def? The function in question is I have a macro which is the public api, the macro expands to a call to a function. I don't particularly want that function to be public. however, I want the function to be callable from customer code which uses the macro. Is this a case where I should be using defn-?

Jim Newton09:07:57

apparently not. when I change defn to defn- my tests fail with the following message

[Geminiani:~/repos/heavybool/clojure] jimka% lein test
Syntax error (IllegalStateException) compiling heavy-bool/+forall- at (heavy_bool_test.clj:7:19).
var: #'heavy-bool/+forall- is not public

Full report at:
/var/folders/rf/5n_0m5j52x71hffg59b_y3kw0000gn/T/clojure-7572854053063970632.edn
Subprocess failed (exit code: 1)
[Geminiani:~/repos/heavybool/clojure] jimka% 

phill10:07:46

As you note, defn- is usually not worth the trouble. It seems to me that around here it's agreed that, as an author, you should clarify the path you want to encourage (by documenting it) but you should not waste time trying to keep out the foolish. So all you need to do is put a docstring on the "public" functions only. Your docs generator should then mention only those.

Ben Sless10:07:35

There is a gentlemanly agreement to put such functions in a .impl namespace and users are expected to be responsible enough to not use them

Jim Newton10:07:47

@UK0810AQ2 I haven't seen this before. You are suggesting that I make a sub-namespace heavy-bool.impl which contains that function. Then even if the call to the function appears in the macro expansion of user code, the user still does not need to require the ...impl namespace. Right?

Ed10:07:13

that's correct. Macro expansion will fully qualify the reference to the impl namespace, and so long as it's required already (presumably by the ns that defines the macro) it should call the correct function.

Ed10:07:38

I've also seen people use the naming convention of thing for the macro and thing* for the function and put them in the same ns.

Ed10:07:52

I think it depends on how large the surface area of your api is as to how comfortable you feel about it all being in one file vs a separate ns.

Jim Newton11:07:29

Ouch, I remember now the difficulty. If I create a namespace for the impl functions, some of those will need to call the functions in the main public namespace. So I'll have to namespaces which require each other. So those will need to be refactored out into yet another namespace defined as a function of the topology of the calling tree, which DOES NOT match the semantics of the main namespace. More trouble than it's worth, I think.

valerauko12:07:30

You can use protocols to work around circular dependencies

1
daveliepmann14:07:05

Protocols seem a bit heavyweight compared to, say, putting a comment or note in the docstring saying "use at your own risk"

Ben Sless15:07:36

Another option is to put the entire implementation in the impl namespace then have an api namespace that calls directly into that. Bit of code duplication but avoids circular dependencies

1
2
☝️ 1
1
growthesque13:07:36

what specifically is an accumulator? is it necessarily some sort of collection type which stores progress like a battery or can it be a literal too? for example in (reduce + 2 [1 2 3]) the 2 is supposedly an accumulator, but it's not a collection so I am not sure why it's an accumulator.

Bob B13:07:12

it can be an atomic value, it's the "result so far"

Ed13:07:49

it accumulates the results of each calculation. if you replace reduce with reductions you can see the intermediate values

growthesque13:07:56

I understand it conceptually, but not technically. for example in:

user=> (reductions + 2 [1 2 3])
(2 3 5 8)
which part is the accumulator? the '() or the 2?

growthesque13:07:44

both store state in a way

jpmonettas13:07:45

accumulator is normally the concept/name of the first argument of a reducing function. Your "2" is not the accumulator but the initial value for it

Ed13:07:48

(reduce + 2 [1 2 3]) is equivalent to the series of calculations:

(+ 2 1) ;; => accumulator: 2 input: 1 result: 3
  (+ 3 2) ;; => accumulator: 3 input: 2 result: 5
  (+ 5 3) ;; => accumulator: 5 input: 3 result: 8
... the accumulator is the first argument to the reducing function

growthesque13:07:13

i see, so anything that's the first argument of the reduction process (that is optionally intended to save state).

jpmonettas13:07:27

the accumulator concept is about the reducing funcitons, then functions like reduce or reductions allows you to provide an initial value for this "accumulator"

Ed13:07:56

I don't think it's optional, I think that's the only place reduce provides for you to store state between each calculation

Ed13:07:17

the same is true for loop or other recursive structures, but they provide more flexibility, allowing for more than one accumulator and extra work on the terminal case

jpmonettas13:07:34

even if accumulator is normally used I think it is not the best name when learning about the concept, since you can use reduce for example for finding the max of a collection of ints, which doesn't "accumulate" anything

dpsutton13:07:29

it accumulates the current max seen so far 🙂

☝️ 1
dpsutton13:07:14

(splitting hairs. But that really is how I think of it. Hard to remember if this was mystifying as a beginner but it’s my current mental model)

jpmonettas13:07:10

haha yeah, but for me accumulate means somehow a combination of values with the previous ones, which doesn't happen in the case of max

dpsutton13:07:38

it does combine them. it’s the output of (max biggest-so-far this-element)

growthesque13:07:00

(reductions) made it easier to understand - each step saves the state for the next iteration

Jim Newton13:07:53

I love this question: What precisely is an accumulator? Admittedly that's not exactly what the OP asked.

jpmonettas13:07:32

well the dictionary for accumulate says : • gather together or acquire an increasing number or quantity of • gradually gather or acquire (a resulting whole). • gather; build up.

Bob B13:07:50

yep... gradually acquiring the resulting whole that is the max

Ed13:07:53

> gradually gather or acquire (a resulting whole). sounds bang on ... accumulating the results of each intermediate step to finish with a resulting whole

Bob B13:07:54

or, the max of an ever-growing set

Ed13:07:33

only with something like map does the resulting collection grow in proportion to the input ...

Jim Newton13:07:59

If we use boolean-or to or together a sequence of 1's, then it is not gradual. How about, intermediate results in an iterative computation?

James Amberger13:07:51

while ‘ymmv,’ I would avoid the term “state” here. What I would do, and might do myself if I find the time, is write my own reduce function with the same semantics and sig as reduce

James Amberger14:07:48

then I/we can see how we feel about the word acc(umulator) (which after all is an arbitrary token in this context).

☝️ 2
phill21:07:44

This goes way back! To put a stake in the 1970s: The 8080 register that received the results of 8-bit math was register A, the "accumulator".

phill21:07:52

The Apple 2's 6502 chip had 3 registers - X, Y, and A "the accumulator"

1
Jim Newton08:07:21

wasn't 6502 motorolla, not apple?

phill11:07:33

@U010VP3UY9X or Mostek? But put it this way... the 6502's having 3 registers would not have impressed/offended me so much if it hadn't been soldered into the Apple II. The 6502 was a processor fit for a doorbell. (Edit: A 1980's doorbell, the kind where you pressed a button and it went 'ding!'... probably doorbells nowadays have 8-core Snapdragons and it's recommended to run them in a cluster)

Giu15:07:06

In Clojure for the brave page 112, author defines this function (defn tri* "Generates lazy sequence of triangular numbers" ([] (tri* 0 1)) ([sum n] (let [new-sum (+ sum n)] (cons new-sum (lazy-seq (tri* new-sum (inc n))))))) Next to this, author writes: The next expression calls tri*, actually creating the lazy sequence and binding it to tri: (def tri (tri*)) But I'm not understanding this. Why not use tri* directly? Or defining tri* as tri?

1
Ed15:07:00

The statement (def tri (tri*)) creates a global reference to the result of (tri*) called tri in the current namespace. Because that result is lazily calculated, not much work happens until the result is consumed (maybe with something like (take 5 tri). After that is called, some of the resulting lazy sequence will be calculated and cached, so the work is only done once, and next time the sequence is consumed the cached results can be reused.

1
Giu15:07:09

but it's not the same as (take 5 tri*) ??

jpmonettas15:07:26

Why not use tri* directlyyou will be calculating a different sequence every time, instead of calculating it once and reusing it > Or defining tri* as tri? you want different things, a function to create the lazy seq (tri* in this case) and a reference to the sequence itself (tri), so you can consume its elements from multiple places

Giu15:07:05

oh!! ok. I think I got it now

Giu15:07:40

this learning path is becoming difficult for me lol

Giu15:07:50

thanks to both

jpmonettas15:07:14

it is tricky until you get the hang of it

💯 1
Ed15:07:18

> but it's not the same as (take 5 tri*) ?? No. tri* is a function that you will need to call. It will produce the same answer as (take 5 (tri*)) but without the caching behaviour.

Ed15:07:47

> it is tricky until you get the hang of it I second this. There can be a lot to learn, but Clojure is the technology and community that have taught me the most about software, and I'm not sure where I'd be without it 😉

❤️ 1
1
yuhan19:07:27

For what it's worth, I don't think this is particularly idiomatic Clojure code - it reads somewhat like an attempt by a Haskell programmer where such recursive / lazy constructions are much more common. (Hint: have a look at clojure.core/reductions for this specific problem) Perhaps the author intended it as a sort of brain-teaser for a beginner to chew on and familiarize themselves with more general FP ways of thinking? I wouldn't worry about it too much :)

Sam Ritchie19:07:47

There is a recursive Fibonacci example with a def that blew my mind at the start of my journey, but these don’t come up often

Sam Ritchie19:07:01

Slash ever, unless you’re writing mathy code

James Amberger21:07:28

Read https://clojure.org/reference/vars, still not getting the difference between (def x) (def x 5) and (def x) (alter-var-root #'x (constantly 5)) The guide says “Supplying an initial value [to a def form] binds the root (even if it was already bound),” such that if I were forced to answer my own question based on what I know now I would say the only difference is the atomicity of alter-var-root.

1
didibus21:07:50

alter-var-root let's you do a transaction over the current value and the new value

didibus21:07:14

It synchronizes from reading the current value to updating it. def won't do that. So you would have to do:

(let [current-val x]
  (def (inc x)))
And this isn't thread safe

didibus21:07:39

def also does a lot more stuff. It'll check if a symbol exists in the current namespace, if not create it and bind it to a new var. If it already exist then it will change the root value of the currently bound var. But say you wanted to change the root value of a bar in a different namespace? With def it's quite tricky. You have to temporarily switch to the other NS, call def, then switch back. Alter-var-root let's you just change the value no matter where the var is interned.

didibus21:07:26

You'll most often see alter-var-root used when people want to wrap the current value with other things, or change the value of a var in another namespace. Where-as you'll see def used to redefine the value of a def in the same namespace to something else. Neither should really be used in production code, because def-ed vars are meant to be effectively immutable. You can mutate them in order to allow REPL redefinition and test monkey-patch and such. If you actually want a mutable global you should put an atom inside the def and mutate not the var but the atom within it.

James Amberger21:07:29

my use of alter-var-root has been limited to advice in https://mccue.dev/pages/12-7-22-clojure-web-primer and yeah I wouldn’t redef a var in production/non-repl code

didibus21:07:51

I see. The way they use it, you could probably also use def and it would be fine. That said some linters sometimes tell you that def shouldn't be used in non-top-level position, etc. There's no harm in using alter-var-root here as well.

James Amberger22:07:27

> The way they use it, you could probably also use def and it would be fine. Ah; that is part of motivation for this thread.

Jason Bullers22:07:07

What's the difference between defing a data structure and updating it via alter-var-root (which, if I understand right, is an atomic operation) and defing that data structure wrapped in an atom and using swap! to update it? From what I understand, the change would be visible across all threads in either case. Is it that CAS only done on atoms, so in case of contention between threads, you'd need the retries that atoms afford but vars don't?

phronmophobic22:07:31

I believe it's much easier to think about how they should be used rather than the specific mechanics. The specific mechanics are really quite tricky. vars are typically used as constants or thread locals. atoms are used for shared state via CAS. If you're interested in the nitty gritty, I highly recommend Java Concurrency in Practice, https://jcip.net/.

Jason Bullers22:07:42

Yeah, that's a great book. Took me a few tries to get through it. I certainly understand that vars tend to be used for constants or dynamic vars (thread local bindings), and atoms are for state that needs to be atomically updated. But while thinking about the atomic semantics of alter-var-root, I realized that I didn't really understand why that's the case, so I speculated that it could be because vars make no promises around contention. So like, in a threaded environment, if you represented a counter as a var, you could miss increments because there's no retry in the case that both threads read the same initial value, both increment, and both write it back

phronmophobic23:07:09

The alter-var-root is wrapped in synchronize, so I don't think you would miss increments, but I would also treat that as an implementation detail and avoid trying to rely on it. There are also some questions around visibility and performance which I would also avoid trying to rely on.

👍 1
phronmophobic23:07:03

I kind of think that's why the docs don't really give many specifics around exactly how they work and instead focus on their intended uses.

👍 1
Jason Bullers23:07:37

I see, so it's one of those "it may work, but we didn't design it for this" things. Vars have "just enough" semantics to do what's needed for their use case, and rather than promising more and potentially complicating their implementation to try and meet those guarantees, atoms were created to explicitly have those guarantees. And of course, as you mentioned above, even if it does work, it would be unexpected to not use an atom for state management.

seancorfield23:07:06

I think the only things we ever use alter-var-root for are REPL-based stuff and some test-based stuff. It never occurred to me that it might be safe enough for production code usage...

didibus00:07:58

Atom gives you the full gamut. You have reset!, swap!, compare-and-set!, reset-vals! and swap-vals! Var only gives you alter-var-root. So it is missing a lot of cases that you might encounter as part of having to perform updates to the data-structure. Also, vars are purely mutable to support certain "dev time" things. This is apparent because when compiling with direct-linking the Vars are erased. They're not even meant to be a real thing in production.

didibus00:07:54

Another thing is that atom performs optimistic locking. Where-as Var performs pessimistic locking.

phronmophobic00:07:36

So the jvm's volatile "always returns the most recent write from any thread" (Java Concurrency in Practice p.38). The alter-var-root happens within a synchronized block, so I guess if you were to rely on these details, then you could use it as a shared state reference. I would probably still avoid it, but it's interesting. @U0K064KQV makes some good points about other features that atoms expose that aren't available for vars.

Jason Bullers00:07:31

Very interesting about the erasure of vars and all the extra functions for atoms. I didn't know about the *-vals! ones before