Sounds pretty sensible to me.
On Tue, Feb 17, 2015 at 4:29 AM, Scott Nelson <[email protected]> wrote: > Thanks for the suggestions. > > So it sounds like in the my case the state is just a normalized store of > artists, albums, and songs. Some portion of the app state would look > something like: > > {:artists [{:id 1 > :name "The Beatles"}] > :albums [{:id 1 > :artist-id 1 > :name "Abbey Road"} > {:id 2 > :artist-id 1 > :name "Rubber Soul"}] > :songs [{:id 1 > :album-id 1 > :name "Come Together"} > {:id 2 > :album-id 1 > :name "Something"} > {:id 3 > :album-id 2 > :name "Drive My Car"} > {:id 4 > :album-id 2 > :name "Norwegian Wood"}]} > > The :current-artist could actually then just reference > artists/ablums/songs and the various components could derive their state: > > {:current-artist {:id 1 > :albums [{:id 1 > :songs [{:id 1} > {:id 2}]} > {:album-id 2 > :songs [{:id 3} > {:id 4}]}]}} > > Similarly, the player component could derive state from references in the > :playlist portion of the app state: > > {:play-index 2 > :songs [{:id 1} > {:id 2} > {:id 3} > {:id 4}]} > > Am I on the right track here? > > As the user navigates around to various artist pages there will be API > requests being made and new artist/album/song data will be added to the > "store" I mentioned above. I think the hard part now would be knowing when > data is no longer needed and can be purged from the store. Any advice for > this? I'm imagining the need to scan through the store and delete items > that are not referenced by the :current-artist or :playlist. As other > areas of the application begin to reference this shared data the cleanup > process would become more and more coupled to those areas. > > On Monday, February 16, 2015 at 6:20:11 AM UTC-5, Oliver George wrote: > > Hey Leon > > > > > > It was really a matter of practicalities. > > > > > > There doesn't seem to be hook to inject derived state into OM cleanly > just now. I have tried various other approaches and my code was much less > managable. This approach is proving really convenient. > > > > > > One really strong win with this approach is that derived state happens > before OM snapshots state to decide what's changed. Before I had this I > had the issue of having to manually observe many paths to ensure that > components updated. Now it just happens. You might need to have > experienced the pain to fully appreciate that. It's big. > > > > > > The change to OM would be small but I figure it's better to prove the > approach in some code rather than ask for extensions. I think the change > would be either a new interface so that OM can derive state when fetching > the atom's value. Somewhere around here is the deref I needed to inject my > derived state. > > > > > > As for memoize-last. Well, memoize seems like a memory leak which > worried me in a browser... no way to clear out old cached info. CLJS > doesn't seem have a LRU memo function just yet (at least I couldn't get > core.memoize to work). memoize-last just happens to fit perfectly with > this particular use case. > > > > > > > > > > On Mon, Feb 16, 2015 at 9:16 PM, Leon Grapenthin <[email protected]> > wrote: > > Can you elaborate why you implement a new IDeref? Why not just calculate > and pass the derived state during rendering? Memoization in any fashion > could still happen. > > > > > > > > Also, is memoize-last just intended as an optimzation to consume less > memory, or does it serve another purpose? > > > > > > > > On Sunday, February 15, 2015 at 11:58:17 PM UTC+1, Oliver George wrote: > > > > > My approach to this stuff is to think in terms of state and derived > state. > > > > > > > > > > > > > > > > > > > > I have a few helpers to facilitate implementing that which amounts to > > > > > Use an atom for stateUse a function to derive stateUse a simple cache > to reduce load of derivation in render loopHack deref to make atom pass > back derived-state > > > > > > > > > If you're new to OM I'd encourage you not to get carried away with > picking up dodgy bits of code like the ones I'm sharing here until you're > confident you're making best use of OM as it comes out of the box. > > > > > > > > > > > > > > > That being said... > > > > > > > > > > > > > > > Here's my derived-atom! function. > > > > > > > > > > > > > > > > > > > > > > > > > (defn derived-atom! > > > > > > > > > > > > > > > " > > > > > > > > > > > > > > > This allows us to access derived values associated with atom state. > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > The implementation of atom does not use deref so we aren't > interfering > > > > > > > > > > > > > > > with how state transitions, they use .-state to access the pure > attribute > > > > > > > > > > > > > > > value. > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > The implementation of om ref cursors use deref to resolve the > current value > > > > > > > > > > > > > > > of state so we are able to apply our logic and have the result > available > > > > > > > > > > > > > > > via (value c) which means reference cursors will trigger updates > based > > > > > > > > > > > > > > > on changes to derived data too. > > > > > > > > > > > > > > > " > > > > > > > > > > > > > > > [iref derived-fn] > > > > > > > > > > > > > > > (specify! iref > > > > > > > > > > > > > > > IDeref (-deref [_] (derived-fn (.-state iref)))) > > > > > > > > > > > > > > > iref) > > > > > > > > > > > > > > > > > > > > This is how i used it as my om app-state > > > > > > > > > > > > > > > > > > > > > > > > > (defonce app-state (derived-atom! (atom {}) (memoize-last > derived-state))) > > > > > > > > > > > > > > > > > > > > memoize-last is a cutdown version of memoize which just remembers the > last version. That essentially means that we only calculate derived-state > once for each OM state > > > > > > > > > > > > > > > > > > > > > > > > > (defn memoize-last > > > > > > > > > > > > > > > "Returns a memoized version of a referentially transparent function. > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > The memoized version of the function keeps a cache of the *most > recent call* > > > > > > > > > > > > > > > from arguments to result. > > > > > > > > > > > > > > > " > > > > > > > > > > > > > > > [f] > > > > > > > > > > > > > > > (let [mem (atom {}) > > > > > > > > > > > > > > > lookup-sentinel (js-obj)] > > > > > > > > > > > > > > > (fn [& args] > > > > > > > > > > > > > > > (let [v (get @mem args lookup-sentinel)] > > > > > > > > > > > > > > > (if (identical? v lookup-sentinel) > > > > > > > > > > > > > > > (let [ret (apply f args)] > > > > > > > > > > > > > > > (reset! mem {args ret}) > > > > > > > > > > > > > > > ret) > > > > > > > > > > > > > > > v))))) > > > > > > > > > > > > > > > > > > > > Finally, here's me using derived state logic to do things like > lookups, validation and other things. It's all very app specific but > amounts to doing (assoc-in state [:a :b] :something) a lot. At this risk > of confusing things here's my generalised required field validation logic > which is implemented as derived state. > > > > > > > > > > > > > > > > > > > > > > > > > (def empty-values #{nil "" [] {} #{}}) > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > (defn validate-required-field [field] > > > > > > > > > > > > > > > (let [{:keys [required value errors]} field] > > > > > > > > > > > > > > > (if (and required (contains? empty-values value)) > > > > > > > > > > > > > > > (assoc field :errors (conj errors "This field is required")) > > > > > > > > > > > > > > > field))) > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > (defn is-required-field? > > > > > > > > > > > > > > > "Identifies walker nodes which are fields relevant to require logic" > > > > > > > > > > > > > > > [m] > > > > > > > > > > > > > > > (and (map? m) > > > > > > > > > > > > > > > (contains? m :required) > > > > > > > > > > > > > > > (contains? m :value))) > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > (defn validate-required-fields > > > > > > > > > > > > > > > "Derive errors associated with missing required fields" > > > > > > > > > > > > > > > [state] > > > > > > > > > > > > > > > (postwalk > > > > > > > > > > > > > > > #(if (is-required-field? %) (validate-required-field %) %) > > > > > > > > > > > > > > > state)) > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > (defn derived-state > > > > > > > > > > > > > > > "Used to include derived state for use by components." > > > > > > > > > > > > > > > [state] (-> state > > > > > > > > > > > > > > > ;derive-vertical-required > > > > > > > > > > > > > > > ;end-position-logic > > > > > > > > > > > > > > > ;maint-freq-logic > > > > > > > > > > > > > > > validate-required-fields)) > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > On Mon, Feb 16, 2015 at 4:17 AM, Leon Grapenthin <[email protected]> > wrote: > > > > > If they were changing, you could implement your own reference system, > where you have one stateful lookup map that you make globally available via > a ref cursor. Instead of the actual changing values, you'd use lookup keys > in your rendered cursors. Changes to values in the stateful lookup map > would would reflect everywhere they are looked up. > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > On Sunday, February 15, 2015 at 3:54:36 PM UTC+1, Scott Nelson wrote: > > > > > > > > > > > I’m working on an Om application that includes a music browser and > player with a simple artist -> albums -> songs hierarchy. When a user is > viewing an artist section I layout all the artist’s albums and songs > hierarchically and this works great since there is basically a 1-to-1 > correspondence between the component hierarchy and the data hierarchy. > > > > > > > > > > > > > > > > > > > > > > Here’s a basic example of what a piece of the application state > might look like under some sort of :current-artist key: > > > > > > > > > > > > > > > > > > > > > > {:name “The Beatles” > > > > > > > > > > > :albums [{:name “Abbey Road”, > > > > > > > > > > > :songs [{:name “Come Together”} > > > > > > > > > > > {:name “Something”}]} > > > > > > > > > > > {:name "Rubber Soul", > > > > > > > > > > > :songs [{:name "Drive My Car"} > > > > > > > > > > > {:name "Norwegian Wood"}]}]} > > > > > > > > > > > > > > > > > > > > > > When a user plays one of these songs I build an ordered playlist of > all the artist’s songs. There is a player component that plays the > playlist (even if the user navigates away from the current artist section) > and displays the currently playing artist, album and song name. To achieve > this I ended up duplicating a bunch of artist/album/song data elsewhere in > the application state. > > > > > > > > > > > > > > > > > > > > > > Here’s what the :playlist path of the application state might look > like after a user clicked the play button next to “Drive My Car”: > > > > > > > > > > > > > > > > > > > > > > {:play-index 2 > > > > > > > > > > > :songs [{:name “Come Together” > > > > > > > > > > > :album {:name “Abbey Road” > > > > > > > > > > > :artist {:name "The Beatles"}}} > > > > > > > > > > > {:name “Something” > > > > > > > > > > > :album {:name “Abbey Road” > > > > > > > > > > > :artist {:name "The Beatles"}}} > > > > > > > > > > > {:name “Drive My Car” > > > > > > > > > > > :album {:name “Rubber Soul” > > > > > > > > > > > :artist {:name "The Beatles"}}} > > > > > > > > > > > {:name “Norwegian Wood” > > > > > > > > > > > :album {:name "Rubber Soul" > > > > > > > > > > > :artist {:name "The Beatles"}}}]} > > > > > > > > > > > > > > > > > > > > > > I think this sort of denormalization works fine in my case since the > artist/album/song data is not changing but I'm wondering if there is a > better way to accomplish this. If the data were changing then I would > imagine it could become a challenge to keep the playlist data in sync. > > > > > > > > > > > > > > > > > > > > -- > > > > > > > > > > Note that posts from new members are moderated - please be patient > with your first post. > > > > > > > > > > --- > > > > > > > > > > You received this message because you are subscribed to the Google > Groups "ClojureScript" group. > > > > > > > > > > To unsubscribe from this group and stop receiving emails from it, send > an email to [email protected]. > > > > > > > > > > To post to this group, send email to [email protected]. > > > > > > > > > > > > > > Visit this group at http://groups.google.com/group/clojurescript. > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > -- > > > > > > > > > > > > > > > Oliver George > > > > > Director, Condense > > > > > 0428 740 978 > > > > > > > > -- > > > > Note that posts from new members are moderated - please be patient with > your first post. > > > > --- > > > > You received this message because you are subscribed to the Google > Groups "ClojureScript" group. > > > > To unsubscribe from this group and stop receiving emails from it, send > an email to [email protected]. > > > > To post to this group, send email to [email protected]. > > > > Visit this group at http://groups.google.com/group/clojurescript. > > > > > > > > > > > > -- > > > > > > Oliver George > > Director, Condense > > 0428 740 978 > > -- > Note that posts from new members are moderated - please be patient with > your first post. > --- > You received this message because you are subscribed to the Google Groups > "ClojureScript" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to [email protected]. > To post to this group, send email to [email protected]. > Visit this group at http://groups.google.com/group/clojurescript. > -- Oliver George Director, Condense 0428 740 978 -- Note that posts from new members are moderated - please be patient with your first post. --- You received this message because you are subscribed to the Google Groups "ClojureScript" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To post to this group, send email to [email protected]. Visit this group at http://groups.google.com/group/clojurescript.
