Question:
In my https://github.com/xadecimal/async-style library, I support canceling promises. When you cancel a promise, it not only instantly returns a CancellationError to everything that was waiting on it, but it also goes on to interrupt the async code that returned the promise. It does that by both calling thread interrupt and setting a cancelled flag.
Now to be honest, I've never much used cancellation myself. So I don't know how useful it is. I can see cancelling the waiting to be useful, for say "timing out". But would there be times when interrupting the execution actually annoying and not what people want? Or would that be what people want?
I also have utility methods like race for example, it waits for the first promise to return. Now as soon as one promise return, I go and cancel all the others and that will in-turn interrupt them all. Is this the behavior people prefer?
Timeout does the same, if it timeouts and stops waiting on the promise, it goes and cancels it.
Finally, I could even support propagating the cancelation, so if you cancel an outer promise, it goes and cancel all inner promises. Would that be a really awesome feature, or something surprising and annoying?
Regards
If I were to do something like that, I'd definitely implement some cancellation as well.
But I would not make race and other functions cancel whatever you pass to them automatically. Similar to how core.async doesn't close the source channels - those channel, as well as your promises, could be used in other places, and just the fact that you pass them to some function does not automatically make that function the owner of all those promises.
There could be an optional argument to all such functions that, when set, makes them behave the way you describe - they become the owners of the promises and might cancel them. But IMO this should not be the default behavior.
First, my thanks go to @didibus for https://github.com/xadecimal/async-style; it is a pleasure to use. Recently, I added async span support to my project https://github.com/steffan-westcott/clj-otel for several async libraries in the Clojure ecosystem, and used async-style in the core.async examples. Of the async libraries I've tried, I am most impressed with Missionary's handling of resource allocation when a failure/cancellation/race triggers, i.e. supervision trees. As an end-user, I would prefer an async library to enable coordinated resource deallocation, much like how Missionary does. The most straightforward explanation I know of on supervision trees is this by Missionary's creator: https://www.youtube.com/watch?v=xtTCdT6e9-0 Edit: Oops, wrong video, I meant to link this one: https://www.youtube.com/watch?v=tV-DoiGdUIo
Missionary's race interrupts other branches as soon as any branch succeeds. join interrupts other branches as soon as any branch fails. I like this default behaviour.
Thanks, I'll have to look at Missionary. I do think what @p-himik is saying is the risk introduced. You might not realize that something that's being used elsewhere is getting cancelled. And I think I can track the graph of what's waiting for what, and technically walk it and cancel everything. But it's more complicated and I don't know if it's worth the cost of walking over each time to only delete what's orphaned. Then there's also the question of what to do with "fire and forget" style, things that are running but without anything waiting for them. If the outer promise is cancelled should they be cancelled too, or does having them fired without any waiting means they should complete no matter what.
But I also feel like 99% of the time, for all standard use-cases, you'd want siblings cancelled inside a race, any, timeout and so on. And even I think you'd want that if you cancel the outer one, if it was waiting on an inner one it should propagate the cancel down.
I could have no propagation of cancellation propagation as default. So only what is explicitly cancelled is, no sibling, no dependent promises. And then I could introduce
(with-cancellation-context
...)
And every async/blocking/compute that are spawned inside it will all be linked to the same cancellation-context so if you cancel any of them they're all cancelled.I'm struggling to get into a "flow" using REPL-assisted development. It would be nice to have a process or heuristic similar to the red-green-refactor cycle of test-driven development. Does anyone have a short rule of thumb that I can experiment with? I'm currently working on an UNO card game that is played from the REPL, so maybe you could describe how you would build that with assistance from the REPL.
Possibly non-obvious: you can disable a "line" in a let with #_#_:
(let [a 10
#_#_b 11]
...)
It's a bit easier than commenting each of the two forms, and in the case of multi-line expressions easier than using ;.also see #caveman for how it organizes things into a user namespace so your code stays pure but you have basically global vars for development (maybe not the best paraphrase but do check it out).
That's what I do as well. And if I decide to leave things in (not cleaned up cause I find them useful to have in the future for quick eval in the REPL), I personally prefer using the reader ignore form #_ over comment .
Nice thing about #_ as well, is if you need to "comment out" some things in the middle of a line to eval like say you have:
(let [a (foo context)]
a)
And want to test out foo, you can do:
(let [a (foo {:user 1} #_context)]
a)
And now you can eval the let.In addition to all the previous advice, I also reach for https://github.com/AbhinavOmprakash/snitch from time to time. This will def all the local bindings of the form you are using in the same ns. It is less safe than doing your own let binding in a comment, but I still find it handy.
Part of the reason I live steamed advent of code is to show off how I use the repl. https://youtu.be/mrGKQkINqzE?si=Mkt39DsGa4WNCDpn
Start with the smallest possible thing, evaluate, accrete, repeat
@daveliepmann when I'm writing a function, how do I evaluate just part of the function? Do I just copy the form into an adjacent comment block and put some test data in?
For example:
(defn insert-at
"Inserts an element at index n, shifting the elements to the right"
[n x coll]
(let [left (take n coll)
right (conj (drop n coll) x)]
(concat left right)))
What if I wanted to eval (conj (drop n coll) x) separately?@potetm I just watched you solve some AoC. When you are testing small pieces of a function, it looks like you just copy them from the function into a comment, then replace the unbound symbols with real test data. Is that a fair description of your flow?
If you feel the need to evaluate part of a function, I'd say you haven't found the smallest possible thing yet. > What if I wanted to eval (conj (drop n coll) x) separately? Here's how I'd do it:
;; First bind your function vars in a let so that you can get your information
;; from your eval immediately.
(let [n 5
x :newval
coll [1 2 3 4 5 6 7 8]]
(let [left (take n coll)
right (conj (drop n coll) x)]
(concat left right)))
;; Then remove everything except the thing you care about.
(let [n 5
x :newval
coll [1 2 3 4 5 6 7 8]]
(conj (drop n coll) x))
;; => (:newval 6 7 8)Firstly, consider writing this function as a let form with what will be the params bound in the let, then hoist the let into a function after you’ve tried it with a variety of params.
And you might like cider-eval-sexp-up-to-point when you want to eval down to some point in the code you’re working on…
It also depends on what editor and plugin you use. I’m using vim with elin. There you have shortcuts to evaluate the current expression, current list or top list. https://liquidz.github.io/elin/#_ranges Then, check the 3.2.2. below for the “evaluate in context” where you can provide params as in let statement. There should be similar options in other editors/plugins.
I'd start with a map that represents the state of your game. Within that map you'd have a :deck key, something for players, etc. Then any top level fn that you add for your game, have it take that map as the first arg, and always return a new map representing the new game state.
Then you can add helper functions for generating a full deck, or for debugging purposes, a short deck so everything fits neatly in your repl window. Then calling one of your game fns at the repl is as simple as (my-game-fn {:deck (create-short-deck)} arg2 arg3)
If you want to reuse the same short deck or game state, just bind it as a temporary def in your repl
(def my-short-deck (create-short-deck))
(def my-game-state {:deck my-short-deck})
(my-game-fn my-game-state arg2 etc)Keep in mind, if you change namespaces, you'll lose access to those defs. What I do is stay in the user namespace and I have a helper function that requires all the namespaces that I'll need and assigns them each a well known and stable alias so I have full access to them from user
To get the red green system that you like, have a look at the docs for clojure.test, you should be able to run your unit tests from the repl as well.
@coltongoates Yeah that's generally how I do it. I want a little scratch area to just run whatever snippet comes to mind. The comment keeps it from running when the namespace loads.
When exploring a new domain or challenge I tend to start writing all my code within a rich comment form. I can quickly evaluate different design approaches and get instant feedback. I copy 'finished' code into respective test and src namespaces. If there were interesting design choices that were not taken I will keep tue code or move it to supporting documentation.
First thing first you need to make sure you have the right setup. You need an editor where you have a shortcut setup (a very easy one like Ctrl+Enter) where it'll evaluate either the firm under the cursor or the form before the cursor. So that you have a shortcut that can evaluate basically one form at a time easily. Then you need to also make sure your setup will evaluate the form in the context of the namespace where the form is defined (and not the namespace the REPL is currently at). Then you need to make sure you do not type at the REPL window, but you always use that shortcut instead directly inside your project files. Then you need to make sure you can setup a running REPL for your editor to be connected too that has everything your app would have when it runs. Finally you need to make sure you have another command to evaluate an entire namespace, and maybe reload it (and maybe one to reload the whole app). Once you've got that. You're ready to start developing a good workflow. But the above is mandatory to move to the next step.
yeah this is one of those things… the payoff is just huge, i.e. I just can’t imagine going back to 2019 writing a typescript app for my employer and, like, rebuilding the whole application every time I edited code to squash a bug, but—like you—it certainly didn’t gel for me on day one of clojure. At least for me, there was a year of yak shaving the editor, the aliases for dev with nrepl etc etc, and even still I catch myself not being interactive enough, e.g. not setting up a temporary scope or something for staying very in touch with what I’m doing that minute. E.g: I have a ring app, there’s something wrong with it, I attempt a fix, and then I eval everything and go check in my browser if it’s fixed, rather than evaluating the smallest scope that must logically contain the bug. Anyway long post but what’s your editor of choice?
@james.amberger @didibus I use Emacs+CIDER.
Some common commands I use are:
• cider-eval-last-sexp bound to C-x C-e
• cider-ns-refresh
• cider-load-buffer bound to C-c C-k
You're all set then!
I'd say after that for me it's a mix of using it for testing, exploration and experimentation. Imagine you're not sure how to print something so that it's formatted and aligned how you want it to be. It's much faster to try things at the REPL immediately see how it looks and so on. That would be "exploration". Once I have code I call it with some happy paths and edge input to see if it works. Writing tests is often a lot more work, so I treat the Repl as a really fast way to test things. And once I know I'm done with the code I go and retrofit tests to catch regressions. Those tests can be at a much smaller unit than a function as well, something you can't really do with unit tests. And "experimentation" its often about trying different ways to implement the same thing, or different way to structure it, experimenting with different abstractions, seeing how they feel when used and so on.
Jack said what I would've re: insert-at. Only thing left to say is that I usually don't bother making a comment block. Evaluating things at the top level of the file I'm working in is fine. Especially if something like (conj (drop n coll) x) occurred to me as something I wanted, I'd write it directly in the file with the values I want to check. In that sense the whole file is a comment block, until I'm ready to clean up.