tolitius / mount

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

State starts via side-effect #75

Closed grammati closed 7 years ago

grammati commented 7 years ago

Things defined with defstate start themselves, I think due to some sort of side-effect of printing.

Example:

Given this file:

(ns foo.core
  (:require [mount.core :refer [defstate]]))

(defn- start-fn []
  (println "Launching missiles!")
  (+ 40 2))

(defstate blah
  :start (start-fn))

I started a repl (nrepl, in emacs, via leiningen), evaluated the namespace, then simply evaluated blah twice.

foo.core> blah
Launching missiles!
#mount.core.DerefableState[{:status :ready, :val #object[mount.core.NotStartedState 0x20ae4d21 "'#'foo.core/blah' is not started (to start all the states call mount/start)"]} 0x199b0da2]
foo.core> blah
42

Note that I never called mount.core/start, but blah is now started.

So a defstate seems to create some sort of Heisenvar, such that simply looking at it changes its value.

tolitius commented 7 years ago

good observation, but nothing really Heisenberg'y :)

Why it happens

defstate creates a managed var which also is aDerefableState that extends clojure.lang.IDeref protocol (or IDeref in cljs).

A Clojure var is also "derefable":

boot.user=> (def a 42)
#'boot.user/a

boot.user=> @#'boot.user/a
42

In other words when you "evaluate" blah, it gets deref'ed, hence calls a deref function on it. When it happens, it gets started, since mount states are all DerefableStates.

=> (require '[mount.core :refer [defstate]])

=> (defn- start-fn []
     (println "Launching missiles!")
     (+ 40 2))

=> (defstate blah :start (start-fn))

=> @blah
Launching missiles!
#object[mount.core.NotStartedState 0x14116aaf "'#'boot.user/blah' is not started (to start all the states call mount/start)"]

=> blah
42

notice how blah is manually deref'ed, and you get the same behavior.

Yes, but why the DerefableState?

This has to do with two things:

As to not calling (mount/start) and still see states starting the first time they are deref'ed, sometimes proves to be quite useful, in cljc mode, since it enables a lazy application start.

Managing state in ClojureScript should give you more details about the cljc mode, lazy starts and the overall rationale.

grammati commented 7 years ago

Thanks for the detailed answer.

However, I'm afraid I don't really understand.

I get that a Var implements IDeref, and that just evaluating it causes its deref method to be called. But if defstate is essentially just def-ing a var that contains an instance of DerefableState, I would expect that evaluating that var would just return the DerefableState instance. But somehow, it seems that the deref method of the DerefableState being called. That is, there are two derefs happening implicitly - of the Var and then one of the DerefableState inside it. Could you explain how this is possible?

Thanks!

grammati commented 7 years ago

Sorry, nevermind, I figured it out. It's exactly what I thought at first - as side-effect of printing, the DerefableState instance gets started.

tolitius commented 7 years ago

ah.. yes, I see what you mean now. I thought you were talking about (println "Launching missiles!") within a :start function, but you mean REPL eval on P.

I'll add a couple of comments to the pull request you sent.