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 @dominicm 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.
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
mogge
👋
Morning
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)
Morn'
@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?
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
Morning
Morning all!
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.
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
the most useful tool is probably your brain then
i.e. if you find it hard to read then that is the best metric to judge it on
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 interesting, that complexor thing looks like a good starting point
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)
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 Either
s 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)
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
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
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
ah gotcha
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
perhaps ... catch
threads the value inside the left branch into bind
as if it were a right value
oh right
so catch is basically catch && return right
so left isn't triggered
gotcha
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
(i'm not happy with the mlet
syntax yet though... still too much noise)
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
right right
could you do it with a ddo
style syntax?
such that
cos it seems like there are nested mlet
s
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
it's the same sig as catchError
in haskell - https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Except.html
mvs?
monadic-values
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
sort of
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
much better... i'm pretty happy with this: https://gist.github.com/mccraigmccraig/07a6ef639eff0d5c6957c5fe60b12b92
Been watching the Mars landing live
very exciting stuff