Fork me on GitHub

Ugh. Early mornings...


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.


Beginners tend to want or expect things that are familiar etc.


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.

👍 3

Thoughts/Opinions welcome!


I think lazyness is necessary if you want a functional style in lots of cases.


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


tricks like (map + [1 2 3] (iterate inc 0)) are sure cute though


or, this one i have used: (map vector (iterate inc 0) [:foo :bar :baz])


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.

👍 3

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

👍 4

Exactly, with types this wouldn’t be a problem at all.

Rachel Westmacott11:02:23

Can I ask what web servers/frameworks people like to use?


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)


I think graphql with lacinia is pretty good


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.

Rachel Westmacott11:02:15

the most useful tool is probably your brain then

Rachel Westmacott11:02:34

i.e. if you find it hard to read then that is the best metric to judge it on

Rachel Westmacott11:02:04

also a lot of readability comes down to good naming, and I doubt you'll find an automated tool to test for name quality


There is also a Clojure plugin for Sonarqube that (among other things) can measure code coverage which might be helpful


oh interesting, that complexor thing looks like a good starting point


Oh nice, i'd been wondering about this. Always knew it would be possible, but wasn't sure if it existed.

Jordan Robinson11:02:55

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


coeffecty/hook/implicit style stuff like that is just not my jam anymore


(although i've not used it a while, maybe the api has changed)

Rachel Westmacott12:02:41

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

Rachel Westmacott12:02:08

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 catch and 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


i understand the words, not sure if i fully grok it


you mean impls provided to lift between the common monad types you're defining?


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 ReaderWriterPromise


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 -


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


perhaps ... catch threads the value inside the left branch into bindas if it were a right value


so catch is basically catch && return right


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


so left isn't triggered


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 -


(i'm not happy with the mlet syntax yet though... still too much noise)


could you do it with a ddo style syntax?


cos it seems like there are nested mlets


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 do


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


could you metadata tag?


i mean all ADTs are sort of waves hands variants under the hood


in the sense of it's possible to make that explicit


yeah, i'm keeping a tag around once something is wrapped in a context, it's the very first step that's the issue


the very first step?


establishing the context you mean?


so writing monad constructors? that's always p verbose i guess


i have confidence it will all fall out soon enough... everything has so far


Been watching the Mars landing live


very exciting stuff