tolitius / mount

managing Clojure and ClojureScript app state since (reset)
Eclipse Public License 1.0
1.23k stars 89 forks source link

Clarify usage of mount/in-cljc-mode #55

Closed jocrau closed 8 years ago

jocrau commented 8 years ago

The CLJS documentation mentions that (mount/in-cljc-mode) has to be invoked "anywhere before a call to (mount/start), usually at the entry point of an app: in the -main, web handler, etc.".

This was not sufficient for me, as the namespaces that are required in the system namespace containing the -main method are evaluated before -main is invoked. The solution was to add (mount/in-cljc-mode) (right after after (ns ...)) in each namespace that contains (defstate ...).

Maybe there is even a cleaner solution to the mode-switching.

Thanks for "mount"!

tolitius commented 8 years ago

Thanks for bringing it up.

(mount/in-cljc-mode) sets the internal mount atom that is not used until (mount/start) is called. Can you share why you think just placing it before (mount/start) does not work for you?

I suspect the problem is somewhere else.

jocrau commented 8 years ago

I dug a little bit deeper. The exception I got was caused by the fact that I dereferenced a state within a def, which is - of course - evaluated at compile time. Then a NotStartedState is bound to the Var. The next time I reload this namespace with the def, I get a

CompilerException java.lang.ClassCastException: clojure.lang.PersistentArrayMap cannot be cast to java.util.concurrent.Future

Putting a (mount/in-cljc-mode) before the state is dereferenced in def avoids the exception.

Check out https://github.com/jocrau/mount/tree/issues/55 for an example how to invoke the exception.

tolitius commented 8 years ago

thanks for the example. in the future it probably would be easier to play with these instead the ones that baked into mount dev, but I understand what I you see.

Keeping states in defs "by yourself" is what mount tries to avoid :) If you need a state keep it in defstate, since it would plug into the mount lifecycle.

What would be the reason you need (def foo @config)?

jocrau commented 8 years ago

Using def was definitely the root cause for the issue. It would be great if mount could issue a proper warning if a state is used that way. The example namespace as consumer of config shouldn't be aware of the fact that mount is doing some voodoo in the background ;-). It should consume config as if it is defined as (def config (atom {...})) in the conf namespace.

It is interesting that it actually works if I put (mount/in-cljc-mode) before def.

I think you can close this issue. Thanks!

tolitius commented 8 years ago

When you do (def foo @xyz), xyz should be defined, since it is being deref'ed. If it is not defined, it throws. I think it is an expected Clojure behavior with or without mount.

With mount, in case xyz is a state var, I would suggest not to use it in def, in the same way you would not do (def conn @db-connection) before creating a db-connection.

In this case mount does not know config is used in def, in fact mount does not watch any usages, it just creates things and adds a way to restart them.

Let me know if this makes sense, or you have other questions.

jocrau commented 8 years ago

That makes sense to me, thanks. And (def conn @db-connection) is a good example of how not to architect a system ;-).

And I also follow your argument that mount should not behave differently from "pure" Clojure. But it would be totally fine (for a non mount user) to define an atom config in a configuration namespace with some "hard-coded" default settings. That atom could be dereferenced at any time. If I would replace this atom definition with defstate I would get the Exception above in (mount/in-cljc-mode).

What I would wish for is, that defstate puts something like (atom nil) in place until the system is started. It might be nit-picking, but when you say "If it is not defined, it throws", I think of "not defined" being

(def config (atom nil))

rather than

(def config nil)

Does that makes sense to you? (and feel free to close this issue if you have better things to do ;-) )

tolitius commented 8 years ago

in (mount/in-cljc-mode) mount's defstate is kind of like an atom: i.e. it is a DerefableState that extends Clojure/Script's IDeref.

In fact, if you use mount in cljc mode, in case you'd like to start your system lazily, you don't have to call (mount/start), since every time a state is deferenced, it would start and return its value.

In case mount is used in a default clj mode, a defstate would create a NotStartedState before (mount/start) is called. This way, whenever you try to access this state without starting it previously, you'll get an exception that NotStartedState can't be "used/cast/etc.." to whatever the type you expect.

So there is really no (def config nil) use case, unless you create it yourself :)

jocrau commented 8 years ago

I understand. Thanks again for the explanation! I will close this (non-)issue now. :-)