This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
- # adventofcode (18)
- # announcements (1)
- # asami (99)
- # babashka (4)
- # beginners (45)
- # calva (20)
- # cider (44)
- # cljdoc (5)
- # clojure (66)
- # clojure-australia (2)
- # clojure-europe (36)
- # clojure-nl (11)
- # clojure-norway (4)
- # clojure-seattle (1)
- # clojure-uk (88)
- # clojurescript (37)
- # community-development (8)
- # conjure (8)
- # datascript (4)
- # datomic (29)
- # depstar (12)
- # emacs (7)
- # events (1)
- # fulcro (29)
- # graalvm (4)
- # graphql (2)
- # helix (2)
- # integrant (4)
- # jobs (7)
- # jobs-discuss (1)
- # lsp (3)
- # malli (6)
- # off-topic (61)
- # pathom (67)
- # pedestal (3)
- # re-frame (9)
- # reitit (4)
- # remote-jobs (13)
- # reveal (18)
- # shadow-cljs (59)
- # spacemacs (1)
- # sql (7)
- # startup-in-a-month (1)
- # tools-deps (29)
- # vim (12)
So, I overheard someone the other day express this statement "Most [experienced] clojure devs, if asked, would say that if Clojure was redone, laziness would not feature [given that mostly everything is eager]" I'm intrigued by this. As a relative newcomer to Clojure I have no opinions on this, but perhaps others might?
Does laziness still make sense? (given that a lot of newcomers, including myself, have been (and continue to do so from time to time) bitten by the laziness "bug", i.e., works in the repl, but not when "running", as the repl is eager...)
Yes laziness still makes sense, and I’d also strongly disagree with the assertion that laziness wouldn’t feature if clojure was redone. As @U09LZR36F says transducers would just be more prominent. Also clojure has never cared for what newcomers think 🙂 it’s been designed to cater for experienced developers. laziness has a big advantage/disadvantage (trade off) over eagerness (even in the form of transducers) — which is that lazy seqs cache their values. Indeed this caching is I think the main issue/feature with laziness. This means you can return to previously seen values and guarantee a stable/repeated result. This can also make working with lazy seqs easier in development, as you can capture and inspect them more easily, but if you capture a transduction over some I/O stream it will be gone when you next look at it. You’ll still be able to capture any individual value, but not the whole sequence of results (without more work). The disadvantages of this are of course well known.
Also clojure has never cared for what newcomers think is a pretty strong statement. All languages should strive to welcome newcomers IMHO.
The clojure community is welcoming and does cater for beginners. The language however isn’t designed to appeal to beginners, it’s designed to first and foremost solve and avoid problems with other approaches. Beginners expectations don’t really align with good design as beginners essentially tend to want easy, not simple.
Also I think it’s fair to say the core language itself prefers implementation simplicity in favour of features that primarily benefit the uninitiated. Some things like easier/friendlier error messages, could have in theory been baked in rather than reusing java’s exceptions etc… However doing so would probably have meant hiding or wrapping host platform features, which would be a lot of work, and would complicate the implementation, make interop more problematic, make work on clojure itself harder, and also be difficult to make as performant.
Stu Halloway has some comments around the web riffing on the perception of "The most important measure for a language is how quickly a beginner can be productive". I think Rich talks about this in Design, Composition, Performance where he looks at different instruments. e.g. the Violin is not beginner friendly, but we don't need to send PRs to "fix" that, it's a tool that can be learned and you can make beautiful things in it once you do. Clojure's stance is that if there's a compromise between new-to-clojure devs and experienced-clojure-devs, the decision will always favour the latter.
you are always going to be able to opt-in to lazy behaviour, since we have closures and macros, but seqs being lazy by default doesn't seem to be nearly as useful in practice as i once thought it might be
I think, most of the issues are caused by mixing lazy and eager constructs without any visible difference. In comparison with that, transient and persistent data structures are easy to spot; modifications of state are easy to spot; core.async blocking operations are easy to spot.
ah, you mean the loss of context on errors during lazy thunks... i do remember that being difficult to wrap my head around when learning clojure
I don't think laziness would be omitted, I've not heard that notion before. I think laziness wouldn't be the default for the sequence operations and instead transducers would feature more strongly in the core with laziness just being one possible consumption of that.
i think @jiriknesl has it pretty much down - laziness in the lang is very desirable, the stumbling point is mixing lazy and eager. conceptually a lot easier to deal with when you get used to thinking in terms of instructions vs actions (or map vs reduce/fold etc)
@dharrigan I've had thoughts to that effect, but I'm beginning to rethink my position to something closer to what @jiriknesl suggested, though I've not considered it in terms of clearly separating them 😃... I do however find it useful to use eager constructs with a lazy context.
@jiriknesl When you're thinking of visible difference, do you mean some way of more clearly marking the difference? Similar to
! to indicate side effects?
@U0JUM502E yes, I think of visual representation of things that return lazyseq and even maybe having non-lazy alternatives
cough i have to admit that when you have types, it's def easier when the compiler goes NO NO STUPID PROGRAMMER THAT ONE IS LAZY which i have found useful with laziness in non-clj situations
for my stuff, I'm quite happily using reitit + malli (for REST/routing/specs), honeysql (v1 and experimenting with v2), juxt clip (for lifecycle management), aero (for configuration) and a few other problem specific libraries (for kafka, redis)
Does anyone know of a tool for assessing clojure code complexity? I'm in a new codebase and I'm finding it really tough going, I think it would be great if I could somehow rank all the functions/macros etc in the codebase and give each one a complexity score, so we can target refactoring in the most useful places. Something like complexity of function = number of functions called x the rarity of each function. So that a function that calls a lot of infrequenctly used functions is high complexity (requires a lot of investment to understand) and a function that calls a few frequently used functions is low complexity.
also a lot of readability comes down to good naming, and I doubt you'll find an automated tool to test for name quality
You could give https://github.com/lokori/uncomplexor/blob/master/src/leiningen/uncomplexor.clj a go as a rough metric
There is also a Clojure plugin for Sonarqube that (among other things) can measure code coverage which might be helpful
Oh nice, i'd been wondering about this. Always knew it would be possible, but wasn't sure if it existed.
I really like liberator, but agree that graphql with lacinia is pretty cool too
i liked liberator but ultimately the model they use is too like a pants reader monad for me to be super stoked about using it again
there's a couple that want to be run as lein templates - I've never really understood that - perhaps it's just me that has no idea what features they are going to need at the beginning of a project
also, if you're using tools.deps then what do you do? make a lein project and then manually convert it?
@mccraigmccraig trying to remember the codebase, are you using
Eithers for error handling at all?
(as in, non fatal stuff, i guess, though you can also catch and still
(Either.left <ex-info>) i guess)
doing exactly that in the
bind impl, then providing
reject on the monad interface, so that they work seamlessly with more complex types - e.g.
writer log is preserved through error handling
for extra points i'm doing all the monads i'm interested in as monad-transformers, so then i have an inner-context which does auto-lifting of values between different monad types, which is nice in a dynlang where the compiler won't let me know that a particular fn or value needs lifting
yep, where feasible... e.g. i've got a synchronous
ReaderWriterException which lifts easily to an async
ReaderWriterPromise or i guess
ReaderWriterTask in the terminology you were using a while back
the idea being that i can take existing code which deals with simple types, just plain
Promise or whatever, and mix it easily with new code which is
i'm currently exploring with a greenfield lib project @alex.lynham ... which i may or may not try and reverse into
funcool/cats later, depending on how the answer to some questions pans out
the impl i'm working on atm uses an
Either approach, as part of a
ReaderWriterExceptionTransformer - and you can see
catch turns out to be the same as
bind , but applies the function to the failure branch - https://github.com/mccraigmccraig/laters/blob/exploration/src/laters/control/rwexception.cljc#L144
right so it's kinda the same as a fold over the tagged union where it's only affecting the left branch? like, catching it
catch threads the value inside the left branch into
bindas if it were a right value
catch is just catch... it's up to you whether you return or reject (or throw if you want - that's equivalent to reject) - hence it's like bind - plain value in, monadic value out
it's the same sig as
catchError in haskell - https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Except.html
here's an example... using
handle rather than
catch, but samesame... you can see you have access to the
writer during handling, and the
handle bifn is basically a bind fn - https://gist.github.com/mccraigmccraig/81ae4e4ff90334676589515d2c570430
which makes me think it should be possible to compose the returned monads from functions with those bindings internally
cos every nested
mlet could be reified into a separate function and then bound within a
ddo right? same as haskell/typescript
yeah, i think i can do better though... and infer the context from the mvs in most cases
the fns which don't take a monadic-value are the tough ones (like
return )... haskell can infer their type, i think from the result hole... cats uses a combination of dynvar and protocols... i'm currently using a lexically scoped binding, but it's lead to macros where i don't really want macros
yeah, i'm keeping a tag around once something is wrapped in a context, it's the very first step that's the issue
much better... i'm pretty happy with this: https://gist.github.com/mccraigmccraig/07a6ef639eff0d5c6957c5fe60b12b92