Skip to content

Instantly share code, notes, and snippets.

@pandeiro
Last active October 5, 2021 06:02
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pandeiro/9a1c8fd431e1b4c78c99 to your computer and use it in GitHub Desktop.
Save pandeiro/9a1c8fd431e1b4c78c99 to your computer and use it in GitHub Desktop.

hiredman [6:39 PM]

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)

yogthos [6:59 PM]

@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

hiredman [7:03 PM]

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?

yogthos [7:04 PM]

yes, what is the problem with that

[7:04] if I have a database, I necessarily have a single instance of that database

hiredman [7:04 PM]

but that isn't a database

yogthos [7:04 PM]

this goes for pretty much any external resource

hiredman [7:04 PM]

that is a connection to a database

yogthos [7:04 PM]

sure

[7:05] and there's still precisely one connection per database

hiredman [7:05 PM]

and there are plenty of reasons you might want more than one connection to what ends up being the same database

yogthos [7:05 PM]

could you name one

[7:05] I see no reason why you wouldn't want to centralize code that accesses an external resource

hiredman [7:06 PM]

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

yogthos [7:06 PM]

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

hiredman [7:07 PM]

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

yogthos [7:07 PM]

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

hiredman [7:08 PM]

writing a deftest that runs multiple jvms is way more involved

yogthos [7:08 PM]

that's not what I meant

hiredman [7:09 PM]

I am kind of surprised, are you seriously argueing that global state in an app is a good thing?

yogthos [7:09 PM]

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

hiredman [7:09 PM]

like you have no problem with code that does (def foo (atom {})) everywhere?

yogthos [7:09 PM]

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

hiredman [7:11 PM]

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

yogthos [7:11 PM]

so basically you're saying that you dislike it for dogmatic reasons

hiredman [7:11 PM]

it cannot be local, you cannot have your own copy

yogthos [7:12 PM]

passing your state around in a global var that's referenced everywhere as you do with component isn't any different conceptually

hiredman [7:12 PM]

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

yogthos [7:12 PM]

in fact I've seen it make things far more problematic, because now your entire app depends on the state being available

hiredman [7:13 PM]

writing large test suites, ok, which bits of global state where need to be written and set up for this test

yogthos [7:13 PM]

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

hiredman [7:17 PM]

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

yogthos [7:17 PM]

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

donaldball [7:19 PM]

If I can’t run multiple instances of my system in the same jvm, I think that’s a design problem

yogthos [7:20 PM]

I have yet to see a practical use case for that

[7:20] this pattern seems to be specific to the reloaded workflow

hiredman [7:20 PM]

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

yogthos [7:21 PM]

my experience is that having a namespace that represents the resource is a very clean way to isolate stateful components

hiredman [7:21 PM]

but I have definitely written tests, where I wanted to observe how multiple instances of a server interacted together

yogthos [7:22 PM]

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:

hiredman [7:23 PM]

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

yogthos [7:24 PM]

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

hiredman [7:24 PM]

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

yogthos [7:25 PM]

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

hiredman [7:27 PM]

right, that is exactly the argument for lexical scope

[7:27] direct passing of parameters, etc

yogthos [7:27 PM]

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

hiredman [7:27 PM]

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

yogthos [7:28 PM]

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

hiredman [7:29 PM]

sure, but code that takes parameters is by definition less coupled than code that doesn't

yogthos [7:30 PM]

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

tolitius [7:31 PM]

@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

hiredman [7:34 PM]

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?

ghadi [7:34 PM]

ding ding ding

[7:35] I like it when arguments are provided (outside in), not discovered (inside reaching out)

yogthos [7:35 PM]

it's a problem most people don't have though

ghadi [7:35 PM]

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

yogthos [7:35 PM]

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

ghadi [7:36 PM]

i think that's optimistic in a lot of cases

yogthos [7:36 PM]

if you can't then your system is coupled

[7:36] I haven't found this to be problematic myself

ghadi [7:36 PM]

i have

[7:36] ¯_(ツ)_/¯

yogthos [7:37 PM]

and that's why we have different people using different things

ghadi [7:37 PM]

yeah

hiredman [7:37 PM]

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

yogthos [7:37 PM]

there isn't one true way to write applications

[7:38] that's a slippery slope fallacy though

ghadi [7:38 PM]

I will make another fallacy --

yogthos [7:38 PM]

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

ghadi [7:39 PM]

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

yogthos [7:39 PM]

you still have global state for all intents and purposes, and now you can't even test anything without it

hiredman [7:39 PM]

yogthos: you have been argueing against coupling business logic to resources, but no one was said you should

yogthos [7:39 PM]

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

hiredman [7:40 PM]

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

yogthos [7:41 PM]

so is saying that mount = (atom {})

[7:41] yet you've made that analogy a couple of times here :simple_smile:

hiredman [7:41 PM]

yogthos: false equivalence

[7:41] (is I think what you mean there)

[7:42] is defstate global mutable state?

ghadi [7:42 PM]

hint: defstate

yogthos [7:42 PM]

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.

yogthos [7:43 PM]

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

hiredman [7:44 PM]

that is just not correct

ghadi [7:44 PM]

yeah that isn't

yogthos [7:44 PM]

ok...

donaldball [7:44 PM]

In my case, a ring handler component got refs to the components needed by the various app handlers and injected them into the requests

yogthos [7:44 PM]

right

hiredman [7:44 PM]

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

yogthos [7:45 PM]

yeah your system has to be accessible though

ghadi [7:45 PM]

no it doesn't

hiredman [7:45 PM]

to the individual components? no it doesn't

yogthos [7:45 PM]

parts of it do

ghadi [7:45 PM]

lots of people make the system accessible as part of the "reloaded" workflow, but it absolutely is not that way in a prod deployment

yogthos [7:46 PM]

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

hiredman [7:46 PM]

yogthos: right, and the way that happens is, you explicitly tell component when creating the system, which components need each other

yogthos [7:47 PM]

or not

[7:47] it's same with mount and component

ghadi [7:47 PM]

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

yogthos [7:47 PM]

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

ghadi [7:48 PM]

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 ^

yogthos [7:49 PM]

in fact examples of that can even be seen in presentations on component

ghadi [7:49 PM]

people are so frigging lazy

hiredman [7:49 PM]

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

ghadi [7:49 PM]

i don't want the DB parameter passed around

yogthos [7:49 PM]

@hiredman: my whole point is that business logic should never need state

ghadi [7:49 PM]

-- said people who were bitten later

yogthos [7:49 PM]

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

ghadi [7:50 PM]

i can get behind that as a generalization

yogthos [7:50 PM]

I'm a little surprised that this is controversial somehow

ghadi [7:51 PM]

specifically, mount doesn't support you in realizing that

yogthos [7:51 PM]

so if we agree that business logic should be state agnostic

[7:51] mount doesn't only support it, it encourages this

ghadi [7:51 PM]

let's write code

yogthos [7:51 PM]

ok

[7:51] let's write code

ghadi [7:52 PM]

what would be a simple example?

yogthos [7:52 PM]

let's say we have a service operation to authenticate a user

[7:52] the user comes from the database

ghadi [7:52 PM]

ok

yogthos [7:52 PM]

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?

ghadi [7:54 PM]

seems reasonable, but it depends on where exactly that line is drawn

[7:54] say more about authentication?

yogthos [7:55 PM]

so with mount I might write code like this

yogthos [7:55 PM]

added a Clojure snippet

(POST "/auth" [user pass]
      (let [user (db/get-user user pass)]
        (ok (authenticate user))))

yogthos [7:55 PM]

parts that care about the state are the POST handler and the db

ghadi [7:55 PM]

k i don't like that right away =(

yogthos [7:55 PM]

that's fine

[7:55] I do though

[7:56] and that's what counts to me :simple_smile:

ghadi [7:56 PM]

Fair enough 😉 I was thinking authentication should know nothing about where it comes from

yogthos [7:56 PM]

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

ghadi [7:56 PM]

(defprotocol IAuthenticator (authenticate [_ user])) (edited)

yogthos [7:56 PM]

it doesn't

ghadi [7:57 PM]

it knows it comes from the db

yogthos [7:57 PM]

all it knows is that it got a map

ghadi [7:57 PM]

not from an LDAP service

yogthos [7:57 PM]

no it doesn't

[7:57] I pass the user value to the authenticate method

ghadi [7:57 PM]

ok, which part is the map?

yogthos [7:57 PM]

user

[7:57] that's the map returned from the database

ghadi [7:57 PM]

ok

yogthos [7:57 PM]

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

ghadi [7:58 PM]

alright

yogthos [7:58 PM]

you can think of it as serialization/deserialization of data from external resources

ghadi [7:58 PM]

how do you test this?

yogthos [7:59 PM]

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

ghadi [8:00 PM]

is authenticate a pure function?

yogthos [8:00 PM]

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

yogthos [8:00 PM]

it's not aware of any state or resources

ghadi [8:01 PM]

you can test authenticate, how do you test the whole route?

[8:02] it's not so obvious

yogthos [8:02 PM]

the route would require a test resource for testing

ghadi [8:02 PM]

in the db?

yogthos [8:02 PM]

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

ghadi [8:03 PM]

where are your pure functions now

yogthos [8:03 PM]

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

ghadi [8:04 PM]

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)

yogthos [8:04 PM]

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

ghadi [8:06 PM]

different vars?

yogthos [8:06 PM]

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

ghadi [8:07 PM]

oh I meant different implementations of the same resource

[8:07] sorry confusing

[8:07] not separate resources

yogthos [8:07 PM]

what is an example of that

ghadi [8:07 PM]

Like LDAPUserStore vs DBUserStore

yogthos [8:08 PM]

you'd write two separate implementations of that wouldn't you

[8:08] each would be a separate namespace (edited)

ghadi [8:08 PM]

yes -- how do I instruct the subsystem that consumes a UserStore as to which one it should use?

yogthos [8:08 PM]

I would be explicit about that

ghadi [8:09 PM]

binding vars?

yogthos [8:09 PM]

you're not going to be randomly switching between what store you use

ghadi [8:09 PM]

right

yogthos [8:09 PM]

so then can you give a concrete scenario

ghadi [8:09 PM]

test vs normal

yogthos [8:10 PM]

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

ghadi [8:11 PM]

sometimes it's the same impl, but a variant. For example, fault injection

yogthos [8:11 PM]

let's put it this way, your worst case scenario with mount would be your best case scenario with component

ghadi [8:11 PM]

you test that a slow network is handled properly with timeouts

yogthos [8:11 PM]

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

ghadi [8:12 PM]

but you have to know which internal resource to mock in that case

yogthos [8:12 PM]

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?

ghadi [8:14 PM]

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

yogthos [8:15 PM]

I disagree

ghadi [8:15 PM]

which is an important distinction

yogthos [8:15 PM]

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

ghadi [8:17 PM]

the ability to cleanly interchange means you've designed good interface

yogthos [8:17 PM]

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

ghadi [8:17 PM]

reaching for resources rather than taking your arguments

yogthos [8:18 PM]

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

ghadi [8:29 PM]

It's clear we disagree (still), but I'm happy to have had the discussion!

yogthos [8:30 PM]

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment