Fork me on GitHub
#off-topic
<
2021-08-07
>
cp4n12:08:14

Question for those familiar with the "Clojure for Web Development" book: Is the second edition going to have a lot of outdated things that won't work or I should avoid now that there is a later edition? I bought the second edition and Brave and True at the same time recently and just have not gone far enough in my Clojure journey yet to get into the web dev stuff, so was curious about this.

seancorfield15:08:37

@cp4n I believe @yogthos changed a lot of things between the 2nd and 3rd editions but I don't know how "outdated" the 2nd ed would be overall. Let me open up the 3rd ed and see if that's covered in the preface...

seancorfield15:08:17

...hmm, there is no information in the 3rd ed about what changed, sorry.

seancorfield15:08:40

The Luminus template doesn't list changes made to it either (in any useful detail).

yogthos17:08:29

There really haven't been any major changes in the template, major changes for 3rd edition are largely luminus agnostic. Most new content focuses on re-frame, shadow-cljs, and reitit.

seancorfield18:08:43

Thanks for responding @yogthos -- I remember that we had talked about the changes between 2nd and 3rd eds but I didn't remember what those changes were! The penalty of getting old 🙂

yogthos01:08:30

I have memory of a goldfish myself, so can relate :)

cp4n15:08:41

Thanks for checking. I read often that Clojure is pretty good when it comes to maintaining backward compatibility, although I know that sometimes libraries fall out of style or become unmaintained. I think maybe I'll just poke around to see if people are stilling doing things a certain way and if not just look at it as an example and not invest a lot in that section

cp4n15:08:17

... or if anything just get the newer book!

seancorfield15:08:37

My opinion on Luminus is that it is not a good choice for new-to-Clojure folks because there are so many "moving parts" and if something "breaks" as you're working through the book, it's hard for beginners to debug what they did wrong -- and also often very hard for people here to help them (because, again, so many interacting libraries that it's hard to know how to direct them in debugging anything).

cp4n15:08:24

That's good to know. Is there a community preference in libraries for web apps that are more bare-bones so might be forgiving and beginner friendly?

seancorfield15:08:26

A lot of people recommend just building something with Ring + Compojure (or Ring + reittit) to understand how requests, responses, and routing work in Clojure web apps.

seancorfield15:08:32

Also look at my usermanager example https://github.com/seancorfield/usermanager-example/ (Ring + Compojure + Component + Selmer + next.jdbc) and the version linked from the README that swaps our Compojure and Component for reitit and Integrant.

seancorfield15:08:10

Once you understand how all those pieces fit together and work with each other, you can take a look at Luminus -- but it adds a lot more libraries on top (and uses Mount, which I think is a terrible choice compared to Component or Integrant, because Mount uses global state).

cp4n15:08:08

This is great, thank you very much

dgb2318:08:21

mount seems to be super repl friendly

dgb2318:08:58

I think I get what you mean. It doesn’t seem to have you wire things up together upfront. Althrough I’m not entirely sure if the alternatives do that, but they seem that way.

jjttjj18:08:27

Yeah I think it's a personal thing, but I've always personally found it a little awkward to think in terms of the component library, but that seems like a sort of unpopular opinion in the Clojure world. https://github.com/riverford/objection has been a personal favorite of mine, but I was thinking recently about taking another look at mount.

Darin Douglass18:08:46

i started with mount, was bit by the "always start everything loaded" behavior of mount, went to mount-lite because you can explicitly define relationships between the states in your system which meant only what you wanted would be started, then landed on redelay as my current goto https://github.com/aroemers/redelay 1. slim state definitions like mount 2. everything starts up automatically when needed

deleted18:08:22

you can start only parts of a mount application if you want: https://github.com/tolitius/mount#start-and-stop-parts-of-application

Darin Douglass18:08:39

Yep, but requiring that in every app felt cumbersome and pretty brittle since it’s easy to forget a state from the app/lib that incorrectly handles state (which was our problem) etc. I had heard of mount-lite which felt like a formalization of that “start only what you want” paradigm with the explicit does extension it has. Then I wanted auto-starting and added it to mount-lite only to be told about redelay by the maintainer. :p

emccue18:08:06

I'm still a rebel, manually starting stuff in order and manually making a system map

🏴‍☠️ 4
emccue18:08:15

Which I feel is a better thing to teach

emccue18:08:19

Since all the libraries around stateful stuff basically accomplish that manual task with some degree of automation

emccue18:08:28

So recognizing that task as a concrete thing would let people recognize the benefits and tradeoffs of the various libs

emccue18:08:45

Instead of "use component. Okay now you need a record. Okay now you need to implement this protocol. Now you need to construct a prestarted map. Now you need to make a logical interface to the component"

➕ 4
emccue18:08:29

It's "pass stateful things as arguments, destructure from a larger map to accomplish dependency injection"

emccue18:08:40

Followed by "okay now here are your options if you need to have dependency injection for starting these stateful components"

seancorfield18:08:34

And nowadays you don't need a record for Component, and if you have no dependencies, a Component doesn't even need to be a map.

emccue19:08:04

Thats because of extend-via-metadata, right?

seancorfield19:08:52

Yes. next.jdbc actually uses a function as a component, because you can attach metadata to it.

Max00:08:11

Are there any good examples/documentation out there for using Component witout any records? Component’s readme still makes it sound like records are required

seancorfield01:08:21

@U01EB0V3H39 No, it assumes you know how to extend a protocol via metadata, per the Clojure core docs. Here's an example from next.jdbc tho': https://github.com/seancorfield/next-jdbc/blob/develop/src/next/jdbc/connection.clj#L318-L326

seancorfield01:08:03

That returns an empty hash map with metadata for the start function, and that returns a function with metadata for the stop function.

seancorfield01:08:07

(the unstarted component isn't interesting here; you start it and you get back a function that you can call to get the underlying connection pool, and when you stop that you get back an unstarted component that is an empty hash map)

Max01:08:21

I know this is a bit of a divergence, but while we’re on the topic: 😅 Are there things you like about component that would lead you to choose this approach over integrant? Since their approaches seem fairly similar

seancorfield02:08:40

Yes, I like that Component is simpler -- 2 lifecycle points vs 7 -- and I like the "locality" that you get with protocol implementations that you don't get with multimethods (where you have to hunt around in the code for all the possible pieces and put them back together in your mind).

thanks3 5
seancorfield02:08:48

I find programs that use Integrant much harder to "grok" 😞

walterl19:08:35

Having used Duct (Integrant) on a fairly large web app for a while now, I've come to see Integrant as a more idomatic Clojure implementation of the idea presented by Component by virtue of using multimethods for starting/stopping (as opposed to OOP-y interface methods), and a more declarative system config (just a map). That said, passing a system map (or subsets thereof) around everywhere has proven to be not great. We're now looking at alternatives which accepts the reality that for most of these things ns-global state is just fine, and a whole lot simpler to work with. Mount looks like it could hit a sweet spot.

Drew Verlee19:08:07

Couldn't just an atom be used for global state?

walterl19:08:40

That's the idea for many of the cases, but sometimes you want some coordination.

walterl19:08:05

That's where I suspect mount may offer that little bit extra

emccue19:08:42

I still stand by recognizing stateful components as a distinct thing as having benefit. At work we don't pass db in to every persistence call and its okay - not my taste, but okay

emccue19:08:46

you have to remember that like every node.js project has some nonsense like

export const sequelize = new Sequelize();

emccue19:08:09

and its fine, mostly. Not great for a large codebase that grows in the direction of having many service-like things and not great for interactive development

emccue19:08:12

but they don't do that anyways, so 🤷

walterl19:08:15

Passing around component maps that need to be used 3 calls down from a given component is really not great either. 😓

vemv21:08:53

> as opposed to OOP-y interface methods there's some talk in which rich highlights interfaces as one of Java's finest constructs. I've worked/talked a fair bit with people averse to protocols/interfaces and more often than not their "repl-friendliness" argumentation is flawed

hiredman21:08:58

The problem with multi methods is both interfaces and implementations of those interfaces have global scope

hiredman21:08:31

So they are much more annoying to spin off a mock for in tests

👀 2
hiredman22:08:28

With protocols you can at any point spin off an anonymous implementation using reify

hiredman22:08:46

I've not used duct, but before duct existed a project I worked on used a fork of component that replaced the protocols with multimethods, and that was my experience, annoying to write little one off implementations for tests

vemv22:08:06

oh now I wonder how to solve that if I were using Integrant. Sounds like a good puzzle with-redefs or anything like that would be a no-go, I care about tests being parallelizable

Drew Verlee00:08:59

@U0NCTKEV8 can you give a code example of what you mean here: > > With protocols you can at any point spin off an anonymous implementation using reify

emccue03:08:45

(defmulti thing type)

(defmethod thing String [s] 8)

(defprotocol Thinger (thing-2 [_]))
(extend-protocol Thinger String (thing-2 [_] 8))
(deftype DummyType [])

(defmethod thing DummyType [_] 1)

(reify Thinger (thing-2 [_] 1))

emccue03:08:06

if you are just dispatching on type you can still pull it off, but you need a new named type for each implementation

emccue03:08:29

with protocols/interfaces and reify you can make a lot more in a more "lightweight" way

emccue03:08:42

and depending on the exact dispatch mechanism of the multimethod it might be harder to make fake impls

walterl01:08:56

@U0NCTKEV8 is completely right about replacing objects in Integrant systems. It's possible, but gets messy quickly: changing component A's referenced component B with a testing one T is as simple as updating A's config map entry. But that only swaps out B for A. All other components that use B will still be referencing B, and not T. A possible solution is to have a proxy ("interface") component B' that other components (like A) reference, and which substitutes either B or T, but now you have an inheritance hierarchy of components, and need to keep track of a list of "don't use directly" components. 😣

👀 5
walterl01:08:14

Will need to check if mount has a solution for that problem.

hiredman01:08:40

Mount wires together via cars in namespaces

🚗 5
hiredman01:08:36

So you cannot have multiple implementations or a single implementation instantiated multiple times parameterized differently

walterl01:08:44

Yeah, but the problem could persist, depending on when references are resolved.

emccue01:08:01

and i think you can all maybe start to see why i just have a function that makes the whole map

emccue01:08:18

less think, more explicit

walterl02:08:36

@U3JH98J4R Will that address replacing all refs to a given component with another one? It's not apparent to me.

emccue02:08:02

yeah, since you make the map explicitly

emccue02:08:10

(defn make-real-system []
  (let [b (b/make-b)
        a (a/make-a b)]
    {:a a
     :b b}))

5
emccue02:08:10

(defn make-test-system []
  (let [t (t/make-t)
        b (b/make-b)
        a (a/make-a t)]
    {:a a
     :b b}))

emccue02:08:35

since you are biting the cost of explicitly writing stuff out, you don't have the restrictions imposed by the generalized mechanisms

walterl02:08:59

Yeah, that'll do it. It strikes me as very similar to the intermediate component B' I mentioned above. Will have to think about it further.

dpsutton19:08:09

I’ve never worked on such a codebase but I think I would prefer component. Seems like it is the only way to have multiple services in the same jvm.

🎯 4
walterl19:08:45

Integrant can do that too. The whole system is initialized to a system map (no global state), so you can init multiples of those, with different configs.

dpsutton19:08:33

Ive seen those when reading the code you are talking about. It seems you define one “normal” one and a test one there in the code

walterl19:08:22

Duct adds another layer ("profiles") on top of Integrant configs, which changes those dynamics a bit (profile configs gets merged into a base profile, yada yada yada...). What I'm referring to here is Integrant (without Duct) being initialized with (ig/init config). You can do that multiple times in the same JVM/REPL, with different configs, and it will return different system maps.

dpsutton19:08:28

Could test multiple instance behavior in the same jvm which I would love

dpsutton19:08:16

But I haven’t felt the dev annoyance at how to call functions in a namespace that expect dependencies

sova-soars-the-sora19:08:54

Hey people who like computers and CS... I found something nifty today. https://papertime.app/

👀 4
sova-soars-the-sora19:08:02

The ivory-tower equivalent of tea time... Paper Time! 😃

deleted19:08:01

doesn't mount have yurt for multiple mount systems in a single system?

walterl19:08:21

It does, yes. I just read that in the "differences" link you posted above.

👍 2
hiredman22:08:34

Something to keep in mind is that differences document is written by mount

hiredman22:08:01

And parts of it are just nonsense (component requiring whole app buy in)

✔️ 8
hiredman22:08:00

It has been my opinion since first an encountering mount that mount is just global state with extra bells and whistles that make you think you are doing something more sophisticated than (def something (atom {}))

💯 2
hiredman22:08:43

But it is basically just defing global state like that, and while I haven't used mount, before mount existed I did work in a large clojure codebase that had just kind of grown organically and kept state it global def'ed atoms and similar

hiredman22:08:48

I have 0 desire to go back to anything close to that, and strongly advise avoiding building your apps like that

seancorfield22:08:36

It was the path we went down at World Singles Networks in the early days because it was "easy" and we were new to Clojure. It's a decision we are still digging ourselves out from under. We adopted Component incrementally (so, yeah, that "whole app buy-in" criticism is nonsense) and where we've been able to completely remove those global state def's the code has been so much easier to work with and maintain. Mount sounds "easy" compared to Component or Integrant but it's a bad architecture choice as far as I'm concerned. I would never make that mistake again.

🙂 4
deleted22:08:22

it's not just (def something (atom {})) , though. it relies on the require order to define the start and stop order, sure, but it has things for starting and stopping only parts of the app, starting with swapped out components, and starting with swapped out start/stop functions, and even starting multiple systems if you pull in yurt. it doesn't seem like it's the thing it's being accused of being

hiredman22:08:19

It relies on a custom def macro and defstate does a lot of stuff, in terms of watching the order that code is loaded, etc, but ultimately it is defining a var (a global)

hiredman22:08:10

Ultimately you are using the global

hiredman22:08:41

Just in your coordinating function or whatever

hiredman22:08:52

It may just be a hindsight is 20/20 thing, it is exasperating to hear people use mount, but 10 years ago if you had shown it to me I might have thought it was the bees knees

hiredman22:08:56

The only clojure projects I have worked on professionally have been large long term projects where I have worked with the same code for several years

dgb2322:08:20

Reminds me a bit of the discussions around singletons

dgb2322:08:10

But mount doesn’t quite seem like that, because it has ways to interact and query these things in a nice way. The broken way is possibly avoidable with just discipline for many cases.

hiredman22:08:50

It is entirely the issue with singletons

hiredman22:08:27

Mount is singletons with extra steps

hiredman22:08:15

Every def is a singleton, so the safe things to def are immutable values, and recipes for building things (functions, type definitions, etc)

hiredman22:08:49

Which is also why libraries that introduce their own def style macros for delivering their favorite mutable thing as a singleton are bad

dgb2323:08:20

But doesn’t a ton of stuff just depend on discipline? Especially in Clojure? We can do all kinds of wonky things.

dgb2323:08:52

The biggest issue with globals is that it isn’t clear what is going on with them. If that is the case with something like mount, say in a large enough program, then one can face frustrations like explained above.

dgb2322:08:54

Also there seems to be a clear way of seeing what is running