Hi @onetom, I see your problem. I think it can be solved using:
(let [started (promise)]
(state :start @(deliver started (my-start-expr))
:stop (my-stop-expr @started)))
But that is not really nice. Maybe we could bind the result of the start expression to something? Any suggestions?(stop (state :start 123 :stop (prn %)))
could print 123
... 😕
intuitively i was expecting state
to handle the value of the stop parameter specially if it's a function
Hmm, that may be a bit too implicit, and could also clash with a "proper" %
in the stop expression..
err... what is a "proper" %?
(state :start ... :stop (map #(do-stuff %) things))
Here the %
is used by the anonymous function, and should not be bound to the result of the :start
expression.
(if start
`(#'map->State (merge ~(dissoc fields :start :stop :name)
{:start-fn (fn [] ~start)
:stop-fn (fn [] ~stop)
:name ~name}))
(throw (ex-info "missing :start expression" {})))
so this is the state macro...
what if the :stop-fn
would receive the value of the current state by default?
like :stop-fn (fn [*state*] ~stop)
it's a bit automagical, i agree, but is there a reason why stopping is not done like that?
as u can tell im kinda stumbling in the dark, because i've only used component & danielsz/system and only looked into mount and mount-lite seriously in the past ~3 days. im also confused by the slight differences between the two and i also went thru their evolution once again in just 3 days. i read all the latest commits between the lastest releases and the latest snapshot versions in both repos. so forgive me if im mixing things up a bit or don't make much sense 🙂
No problem, you have a valid point
on top of that i've only used boot
in the past ~4years of my clojure career and im just learning lein
, which means there is suddenly no more boot.pods
; everything is under one classpath...
have you personally used the (with-session ...)
construct in your projects?
it would be good if you could show some examples how does it look like for u.
im also confused a bit by your recommendations in the Design consideration section of the docs.
> Having a global state does not mean you should forego on the good practice of passing state along as arguments to functions.
seems to contradict this statement:
> Try to use your defstate as if it were private. Better yet, declare it as private. This will keep you from refering to your state from every corner of your application, making it more componentized.
if my state is private, then it means i have some functions implicitly using it, no?
like the tx!
, db
& q
functions in my example:
https://gitlab.com/onetom/mount-lite-example/blob/master/src/app/db.clj#L13-15
I haven't used boot
a lot, so I can't really say something about that, as far as it is related to mount(-lite) 🙂
In the end the project that inspired us to create version 2 of mount-lite
did not require multiple sessions after all, so no, we have not used with-session
a lot.
What I mean in the documentation, is as follows:
(defn- do-stuff* [impl arg1 arg2]
...)
(defstate ^:private impl ...)
(defn do-stuff [arg1 arg2]
(do-stuff* @impl arg1 arg2))
This way, the actual implementation function receives the state directly, which is easier to test. But the public API of this namespace, does not see which states are used at all. It could be none, it could be many; the caller does not care and does not have to pass any states explicitly (i.e. it is private).🙂 thats funny (that u havent used with-session
at the end)
^^
okay, so according to ur example, i would say u did forego on the good practice of passing the state along as arguments to functions 🙂
and also you did NOT forego at the same time 🙂
Would it help you if this
is bound in the :stop
expression?
Yeah, true
Maybe I should update the documentation then, if it is not clear what I mean. I hope above example clears it up a bit.
And this
being bound to the start value.
so then your tests would look like:
(ns app.stuff-test
(:require [app.stuff :as stuff]))
(def do-stuff* #'stuff/do-stuff*)
(deftest stuff-test
(let [mock-state (...)]
(is (do-stuff* mock-state 1 2))))
yep
yeah, this
sounds quite good, however, it would i need to use it as just this
or @this
?
Could be just this
, there is no benefit to dereferencing there in my opinion.
so how would you explain it then?
> if you are using the anonymous state constructor macro called state
the stop expression will implicitly have access to the value of the anonymous state with the this
symbol
(defstate some-state
:start 123
:stop (println "Stopping" @some-state))
as an anonymous state, some-state
would be defined as:
(state some-state
:start 123
:stop (println "Stopping" this))
do i understand it well?
Source of the screenshot: https://youtu.be/13cmHf_kt-Q?t=985
If you are promoting opaqueness from the public API point of view, but suggest testing via a private API which receives the states it operates on explicitly, then it means the test code will be coupled to the implementation details
The this
would also be bound in the defstate
stop expression, but maybe it won't be used.
> ...also be bound...
why also? where else this
is bound?
I mean, not just in anonymous states
ah, i see. thats good because it means one can reuse stop functions
@onetom Alright, released a new snapshot, with the this
bind in
@onetom: Customers
should not be a coupling object inside a component at all. It is better to have two components: db
and email
. In which case, they can be stubbed for testing easily with mount/start-with
. Protocols and Records in this case are "very" optional, but since Component is all about it and your question is specifically about this piece of code:
(defprotocol EmailSender
(send [this address message]))
(defprotocol Database
(query [this q]))
(let [customers {:db (reify Database (query [_ q] (println "querying DB with" q)))
:email (reify EmailSender (send [_ a m] (println "sending email to" a "body:" m)))}]
(mount/start-with {#'app/customers customers})
if we agree not to have a coupling Customers
object, and just have db
and email
instead, we can simply do:
(mount/start-with {#'app/db #(println "querying DB with" %) ;; or whatever the DB state is
#'app/email #(println "sending email to" %1 "body:" %2)})
no ceremony.
more examples here: https://www.dotkam.com/2016/01/17/swapping-alternate-implementations-with-mount/