I just don't understand why people use mount, I must be missing something, its model of each namespace as a component, with state as a global thing in a namespace(maybe I am mistunderstanding this), just seems bad, like sticking (def state (atom {})) in every namespace would be
[6:41] https://github.com/tolitius/mount/blob/master/src/mount/core.cljc#L10-L14
GitHub tolitius/mount mount - managing Clojure and ClojureScript app state since (reset)
@hiredman: could you elaborate on the actual problem you see there?
[7:00] note that mount doesn't tie the functions in your namespace to the state in any way
[7:00] you can still use a protocol with it if you like
[7:00] all it does is track the lifecycle of stateful variables
global state variables, right?
[7:04] you can only have a single instance of these state variables per clojure runtime, and it is visible to any and all clojure code, right?
yes, what is the problem with that
[7:04] if I have a database, I necessarily have a single instance of that database
but that isn't a database
this goes for pretty much any external resource
that is a connection to a database
sure
[7:05] and there's still precisely one connection per database
and there are plenty of reasons you might want more than one connection to what ends up being the same database
could you name one
[7:05] I see no reason why you wouldn't want to centralize code that accesses an external resource
you structure your app in such a way as to have to logically distinct databases, eahc having their own "connection" but in production your ops team decides to use one database
[7:06] I've seen that happen multiple times
and how is this at odds with mount?
[7:06]
you create a defstate
per connection instance
[7:06] and it manages that connection instance
[7:07] either you have one connection defined in your app, or you have multiple
if you want to test running a cluster of your server, if you don't use global state you can run multiple copies in the same jvm, with global state you cannot
you would have to explicitly write your app to use multiple connections
[7:07] why would you need to run tests on the same jvm
[7:07] is there a repl shortage that we're experiencing?
[7:08] this is the only argument I've ever heard against the mount approach
writing a deftest that runs multiple jvms is way more involved
that's not what I meant
I am kind of surprised, are you seriously argueing that global state in an app is a good thing?
I meant that you should use lein profiles to specify a separate configuration for your test profile
[7:09] the actual code in the application should be environment agnostic
like you have no problem with code that does (def foo (atom {})) everywhere?
you still haven't provided a concrete example of a problem
[7:10] so if you have a problem with the mount approach, then could you please articulate it
[7:11] the problem with global state is generally that it ends up being being difficult to track in the application
[7:11] however, this is not what happens when you use mount
it is hard to articulate an objection to something that I think flies in the face of functional programming since the invention of lexical scope
[7:11] if something is global, it isn't scaoped
[7:11] scoped
so basically you're saying that you dislike it for dogmatic reasons
it cannot be local, you cannot have your own copy
passing your state around in a global var that's referenced everywhere as you do with component isn't any different conceptually
no, I mean, I have used and worked on systems that didn't have a way for different parts to keep state outside of (def foo (atom {})) and it was terrible
in fact I've seen it make things far more problematic, because now your entire app depends on the state being available
writing large test suites, ok, which bits of global state where need to be written and set up for this test
but majority of your application should be stateless by design
[7:13] the whole point of using a functional language is that you only deal with state at the edges of your application
[7:13] mount allows you to represent stateful resources and provide an API for them
[7:14] but those are predominantly dealing with IO, and have no actual business logic in them
[7:14] with mount, you don't need your external resources for testing vast majority of the time
[7:16] yet, I've seen many component based applications where state was passed around all over the place as the system map
[7:16] and none of the application could be tested without external resources being available
at my last job, our largest codebase, predated component, mount, whatever, and it was largely written as just a bunch of namespaces, and if they needed state the had to use some kind of global mutable thing. whenever the opportunity for a new project came up, no one chose to do that again
[7:17] sure, nothing guarantees automatic correctness
right
[7:18] you still have to be mindful of your design whatever approach you take
[7:18] and just because you saw a mess with global vars, doesn't mean global vars were the inherent problem
[7:18] same as if I saw a mess made with component, it's not necessarily component that's at fault
[7:19] however, if you understand why global state should be isolated then you can do that equally easily with both monut and component
[7:19] and neither is a substitute for that understanding
If I can’t run multiple instances of my system in the same jvm, I think that’s a design problem
I have yet to see a practical use case for that
[7:20] this pattern seems to be specific to the reloaded workflow
I am not saying global vars are a problem, clojure vars are global, I am saying a global atom is bad
[7:20] I have never used the reloaded workflow
my experience is that having a namespace that represents the resource is a very clean way to isolate stateful components
but I have definitely written tests, where I wanted to observe how multiple instances of a server interacted together
and I still think it's much cleaner to run each instance in its own jvm
[7:22] but that's the tradeoff you make
[7:22] both options are available
[7:23] however, you said that you don't understand why people use mount, and I'm explaining my rationale for using it :simple_smile:
there are trade offs right, depending on whatever, running each an its own jvm may be cleaner, but is that fast enough to run as part of your regular test run, how painful is it to maintain the clojure.test code to do that
I find that I don't need to access resource for majority my tests
[7:24] because I structure my apps to be predominantly resource agnostic
[7:24] in most cases I'm testing the business logic
[7:24] I'm not testing my database or the database connection
yogthos: yeah, I am just kind of shocked, like if I saw code in a code review that had (def foo (atom {})) in it, I would require a lot of explaining
[7:25] mount seems no different, and it is getting built in to stuff
in fact I would argue that if you need access to resource to run mosts tests, that's a problem with the deisgn
[7:25] I think the conceptual model for mount is very simple
[7:25] you have a resource and you have an API for accessing it
[7:26] to me it makes perfect sense to use a namespace to abstract that
[7:26] furthermore this helps provide real abstractions over the resource
[7:26] what if its nature changes
[7:26] I might be using a database today, and switch to using a service tomorrow
[7:26] the rest of the application shouldn't be aware of the details of where the data comes from
right, that is exactly the argument for lexical scope
[7:27] direct passing of parameters, etc
I don't see how that's the case
[7:27] if you pass parameters from your business logic, you've coupled your business logic to the resource
[7:27] that's a bad thing
I need an A, I don't know where A is, someone just passes me one
[7:28] and if fact the person that passes me an A, maybe have 100 different As to choose from, or may just create and destroy one on the spot
I'd much rather have a shallow layer that deals with IO that sits around the core business logic that's resource agnostic
[7:29] again, you have to actively structure your application to isolate stateful code
[7:29] period
[7:29] and mount doesn't make it any more difficult
sure, but code that takes parameters is by definition less coupled than code that doesn't
the only limitation is that you can't run multiple instances inside the same jvm, if that's a requirement then mount is a bad fit, if it's not then it's perfectly fine
[7:30] I've been working with Clojure for about 6 years now, and I haven't seen that be a requirement yet however
[7:31] right and in my case business logic takes data as a paremeter
[7:31] it is inherently decoupled from the resources
@hiredman, @donaldball:
if you want to test running a cluster of your server.. if I _can’t_ run multiple instances of my system in the same jvm, I think that’s a design problem it cannot be local, you _cannot_ have your own copy..
in those rare cases (the _only_ one I know of is "running tests in the _same dev_ REPL") you _can_: https://github.com/tolitius/yurt
but learning how people use mount, I see that there is no shortage of REPLs in that one use case. besides, running something like boot watch speak test
in another REPL is great regardless of whether you using mount or whatever
GitHub tolitius/yurt yurt - high quality mounted real (e)states
but isn't this just piling complexity (managing multiple clojure runtimes) on as a bandaid to a problem you wouldn't have to start with, with out global state?
ding ding ding
[7:35] I like it when arguments are provided (outside in), not discovered (inside reaching out)
it's a problem most people don't have though
you have the argument anyways, whether you see it or not
[7:35] the db is an argument to a subsystem that accesses the db
I see the db as a separate component from my business logic
[7:36] my view is that you should be able to take the business logic and package it as a library
i think that's optimistic in a lot of cases
if you can't then your system is coupled
[7:36] I haven't found this to be problematic myself
i have
[7:36] ¯_(ツ)_/¯
and that's why we have different people using different things
yeah
https://gist.github.com/hiredman/27733 is the earliest clojure code of mine I could find, from 7 years ago, anything earlier is likely lost to lisppaste
there isn't one true way to write applications
[7:38] that's a slippery slope fallacy though
I will make another fallacy --
I don't think anybody will argue that you can't make a mess with mutable state
[7:38] of course you can
[7:38] saying that because you can make a mess all state is bad is a stretch
[7:39] and I don't see how coupling your business logic to your resources makes the problem any better
I've lived a war story of trying to remove Korma (sql) from a system over a year or so... I'm afraid mount
will lead to similar situations, because the root cause (ambient global state) is the same
you still have global state for all intents and purposes, and now you can't even test anything without it
yogthos: you have been argueing against coupling business logic to resources, but no one was said you should
yet that's what I commonly see with component
[7:40] and not just component in fact
[7:40] if you look at other DI approaches, this is quite common
[7:40] you have a singleton that gets passed around all over the app
but that is a false dichotomy, right?
[7:41] just because you see it in component, doesn't mean you don't see it in mount
so is saying that mount = (atom {})
[7:41] yet you've made that analogy a couple of times here :simple_smile:
yogthos: false equivalence
[7:41] (is I think what you mean there)
[7:42] is defstate global mutable state?
hint: defstate
of course it is
[7:42] does it mean that it will make your app into a giant mess magically
[7:42] of course not
[7:42] in fact
[7:42] if you use monut or component, you're going to have to structure your app in a very similar way to avoid this problem
donaldball [7:43 PM] Huh, that’s not been my experience with component either. The various fns that require (probably protocol implementing) arguments get them explicitly, not the whole system blob.
but where do they get them from?
[7:43] they have to be passed in from somewhere
[7:43] since you're not keeping any global state, you have to pass it around
[7:44] so now parts of the application that shouldn't care about the state have to take it as a parameter
that is just not correct
yeah that isn't
ok...
In my case, a ring handler component got refs to the components needed by the various app handlers and injected them into the requests
right
when you create a component system, you tell it about the dependencies between components, and each component only has access to the bits you say it needs
yeah your system has to be accessible though
no it doesn't
to the individual components? no it doesn't
parts of it do
lots of people make the system accessible as part of the "reloaded" workflow, but it absolutely is not that way in a prod deployment
look, your app initializes, it creates the system then resources in that system have to become available to functions that use them
[7:46] there is no way around that
[7:46] now you can either keep that interaction at the edges of your app
yogthos: right, and the way that happens is, you explicitly tell component when creating the system, which components need each other
or not
[7:47] it's same with mount and component
components reaching their grubby little hands to find some piece of shared state that they didn't tell you that they needed initially is the problem
that's not the problem I'm talking about
[7:47] I'm saying that if you have a piece of business logic and it has to accept any stateful variable as a parameter because it calls a function that uses it then you've got coupling
[7:48] I'm also saying that plenty of systems using component do precisely that in the wild
[7:48] and I would go as far as to say that component encourages this approach by design
I want some sort of limits/bounds on the parameters to a subsystem. I hate having to use something that has interactions beyond where it should
[7:49] Having explicit arguments helps with ^
in fact examples of that can even be seen in presentations on component
people are so frigging lazy
if you have a piece of busines logic that has to use some state, but you reading it from a global place instead of passing it in, you've got even more coupling
i don't want the DB parameter passed around
@hiredman: my whole point is that business logic should never need state
-- said people who were bitten later
pretty much by definition I would say
[7:49] you should always be able to deal with external resources at the edges
[7:50] and if you aren't then I'd say that's a problem
[7:50] business logic should operate on the data
i can get behind that as a generalization
I'm a little surprised that this is controversial somehow
specifically, mount
doesn't support you in realizing that
so if we agree that business logic should be state agnostic
[7:51] mount doesn't only support it, it encourages this
let's write code
ok
[7:51] let's write code
what would be a simple example?
let's say we have a service operation to authenticate a user
[7:52] the user comes from the database
ok
we accept a request, get user info, authenticate
[7:53] I would treat authentication as the business logic
[7:53] while the code that talks to the client and the database would be the IO layer
[7:53] does that sound like a reasonable distinction?
seems reasonable, but it depends on where exactly that line is drawn
[7:54] say more about authentication?
so with mount I might write code like this
added a Clojure snippet
(POST "/auth" [user pass]
(let [user (db/get-user user pass)]
(ok (authenticate user))))
parts that care about the state are the POST handler and the db
k i don't like that right away =(
that's fine
[7:55] I do though
[7:56] and that's what counts to me :simple_smile:
Fair enough 😉 I was thinking authentication should know nothing about where it comes from
only parts that care about IO are the POST route and the database namespace
[7:56] once I got the data, that gets passed to the core of the app
[7:56] and the core of the application doesn't care one bit where it came from or where it goes
(defprotocol IAuthenticator (authenticate [_ user]))
(edited)
it doesn't
it knows it comes from the db
all it knows is that it got a map
not from an LDAP service
no it doesn't
[7:57] I pass the user value to the authenticate method
ok, which part is the map?
user
[7:57] that's the map returned from the database
ok
by the db/get-user
method
[7:58] the database API is effectively getters and setters
[7:58] there's no logic
[7:58] the POST handler also doesn't have any logic
[7:58] this is the layer I'm talking about
alright
you can think of it as serialization/deserialization of data from external resources
how do you test this?
I can test authenticate
by passing it a mock user record
[7:59] I don't need any access to resources to do that
[7:59] I can do end to end tests by providing a test resource
[7:59] but I would do that very rarely
is authenticate
a pure function?
I tend to run full tests before I commit code and on the build server
[8:00] yes
[8:00] absolutely
[8:00] it takes data and it returns data
dsesclei [8:00 PM] joined #clojure
it's not aware of any state or resources
you can test authenticate
, how do you test the whole route?
[8:02] it's not so obvious
the route would require a test resource for testing
in the db?
I would test it in a separate repl with a test profile
[8:02] right
[8:03] but as I pointed out already, my experience is that I need to do this kind of testing rarely
where are your pure functions now
precisely because I don't keep business logic in the layer that deals with the IO
[8:03] in my business logic
[8:04] the thing that's the lions share of the application
if you want to vary multiple implementations of components together -- I've found component to be very useful. Sometimes the components have to be a level up from typical logic (e.g. UserStore vs Database)
sure
[8:04] so how is this at odds with mount?
[8:04] you can create a protocol say for a queue
[8:05] then initialize instances of the protocol each with its own state
different vars?
of course
[8:06] if I have two different queues, they're conceptually two separate resources are they not?
[8:07] by their very definition
oh I meant different implementations of the same resource
[8:07] sorry confusing
[8:07] not separate resources
what is an example of that
Like LDAPUserStore vs DBUserStore
you'd write two separate implementations of that wouldn't you
[8:08] each would be a separate namespace (edited)
yes -- how do I instruct the subsystem that consumes a UserStore as to which one it should use?
I would be explicit about that
binding vars?
you're not going to be randomly switching between what store you use
right
so then can you give a concrete scenario
test vs normal
then I'd provide a different resource in the profile
[8:10] but why would I test against two completely different types of resoruces?
[8:10] that seems like a recipe for disaster to me
[8:10] if my app goes to ldap in production, I would surely want to use the same mock api in tests
sometimes it's the same impl, but a variant. For example, fault injection
let's put it this way, your worst case scenario with mount would be your best case scenario with component
you test that a slow network is handled properly with timeouts
you'd create a protocol and a record that wraps whatever states you want (edited)
[8:12] you'd still retain the same api for the resource in that case
[8:12] mock resources should have same api as the real resources
but you have to know which internal resource to mock in that case
if I had a mock connection in my defstate
then it should behave like a real one right?
[8:12] how so?
[8:12] what's an internal resource by the way?
you have to know how a subsystem works intimately, to be able to mock it effectively (with mount)
[8:15] that problem is gone _by design_ with component
I disagree
which is an important distinction
in fact sounds like you're setting yourself up for a bigger problem
[8:15] since you don't know how your system works, you can end up with bugs around the edge cases
[8:15] I certainly wouldn't advocate such an approach
[8:16] if you have a mock of a resource that doesn't exactly match how the real one works, that's extremely problematic in my opinion
the ability to cleanly interchange means you've designed good interface
but what does that have to do with mount?
[8:17]
if I have a database connection managed by deftate
[8:17] its interface is the API that I implement
[8:17] that's the resource
reaching for resources rather than taking your arguments
I think we're going in circles here
[8:18] we already covered this point earlier, with examples to boot
[8:21] if you prefer the way component works, it's there and nobody is asking you not to use it
[8:21] but there's absolutely nothing wrong with using mount either
[8:21] it's a personal preference, and different people like different things
[8:21] but I have yet to see a coherent argument against using mount
It's clear we disagree (still), but I'm happy to have had the discussion!
same here :simple_smile:
[8:31] I don't think it's wrong to disagree, a lot of things come down to tradeoffs, and different apps have different needs
[8:31] there's no silver bullet for this
[8:31] having options is a good thing in my opinion
[8:33] I find that people tend to have different preferences based on their experiences and the way they like to approach problems, and it's always good to have these kinds of discussions to get a broader view of things