-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Ruminate Functional #16
Comments
I think the only thing jiraph.graph/Layer needs to have is |
I think we need wrappers for all the readers too. |
I think actually we don't need a new protocol at all, we just need to make jiraph.layer/Layer functional. Of course the layers need some way to mutably do changes, since the datastore is mutable, but those don't have to be exposed in any kind of protocol. For example, it could look something like this:
Then the client only ever uses this immutable
|
Further example: masai-sorted/specialized-writer[adjoin] could look like: (defmethod specialized-writer adjoin [layer layout keyseq _]
(when (every? (fn [[path format]] ;; TODO support any reduce-fn
(= adjoin (:reduce-fn format)))
layout)
(let [db (:db layer)
[id & keys] keyseq
writer (partial db/append! db)]
(fn [arg]
(retro/with-actions {:old nil, :new arg}
{this [#(write-paths! % writer layout id
(assoc-in {} keyseq arg)
false :codec)]}))))) |
Regarding the above concerns:
We may want to add a MutableLayer protocol for doing stuff the "old" way, and a wrapping deftype that "ports" this to the new functional protocol; jiraph.graph will only know about the functional protocol. |
We had some more discussions about how this will work. We no longer need update-in-node (or update-fn) to return an old/new/keyseq "burp". We get rid of update-fn, assoc-node, and dissoc-node at the layer level, and replace them all with a single update-in-node function, which returns an IOValue. We don't need the extra layer of indirection provided by update-fn, because IOValues are already functions. Now, a ruminating layer intercepts update-in calls directly, and computes "transformed" update-in calls to pass to each of its output layers (in rare cases it may even transform the call to its input layer). It then composes all of those IOValues and returns that as its own result from update-in. Removing assoc and dissoc means that more logic needs to go into the actions of our IOValues, instead of in jiraph.graph/update-in-node: the latter can simply delegate directly to jiraph.layer/update-in-node, which returns an IOValue. It's not yet clear whether we will:
|
One other tradeoff we're making by taking this approach is that we can no longer pass the old value to the output layers. However, this was really just an optimization anyway, and we can always just fetch the old value from the input layer in the transform if needed. We can use caching to optimize repeated reads of the same value if necessary. |
More thoughts from today's discussions! We needed some way to be able to read "mid-transaction" values, even if the transaction has already been applied. E.g., layer A has been committed, but layer B, which ruminates on A, has not. To re-compute B's actions, we need to know what A looked like in the middle of the transaction. So, we need IOValue objects that are more transparent (or, to look at it another way, more heavy-weight) than retro's "seq of functions per layer". Specifically, we decided to build a new jiraph io-value type, which carries forward a function We tried two approaches to this: Justin's (provided first below) is easier to understand and takes less code; mine (provided second below) is more complicated for no particular reason. I am including it only for posterity, in case it turns out there is an unanticipated problem with Justin's approach. Both approaches assume the existence of a function With that preamble aside, here is Justin's implementation of (def jiraph-compose concat)
(defn update-in-node [layer keyseq f args]
[{:write (fn [layer read]
(apply update-in-node! layer read keyseq f args))
:layer layer :keyseq keyseq :f f :args args}])
(defn actualize-ioval [ioval]
(reduce (fn [{:keys [actions read]} {:keys [write layer keyseq f args]}]
{:actions (update-in actions [layer] (fnil conj [])
#(write % read))
:read (fn [layer' read-keyseq]
(if-let [[read-path update-path get-path]
(and (same? layer layer')
(path-parts read-keyseq keyseq))]
(-> (read layer' read-path)
(apply update-in* update-path f args)
(get-in get-path))
(read layer' read-keyseq)))})
{:actions {} :read jiraph.graph/get-in-node}
ioval))
(defn ->retro-ioval [ioval]
(:actions (actualize-ioval ioval))) And my own, inferior versions: (defn combine-actions [action1 action2 read-wrapper]
(fn [layer read]
(when action1
(action1 layer read))
(action2 layer (wrap-read1 read))))
(defn jiraph-compose [{writes1 :writes wrap-read1 :wrap-read}
{writes2 :writes wrap-read2 :wrap-read}]
{:writes (reduce (fn [writes [layer action]]
(update-in writes [layer] combine-actions action wrap-read1))
writes1, writes2)
:wrap-read (fn [read]
(wrap-read2 (wrap-read1 read)))})
(defn ->retro-ioval [ioval]
(into {}
(for [[layer action] (:writes ioval)]
[layer [#(action % jiraph.graph/get-in-node)]])))
(defn update-in-node [layer write-keyseq f args]
{:writes {layer (fn [layer' read]
(apply update-in-node! layer' read write-keyseq f args))}
:wrap-read (fn [read]
(fn [layer' read-keyseq]
(if-let [[read-path update-path get-path]
(and (same? layer layer')
(path-parts read-keyseq write-keyseq))]
(-> (read layer' read-path)
(apply update-in* update-path f args)
(get-in get-path))
(read layer' read-keyseq))))}) |
Note that in my implementation, an io-value is a seq of tuples containing a function to mutably do the writing along with the arguments passed to update-in-node. In Alan's implementation, an io-value is a single function to read any value on any layer along with a map from layer to a function that mutably writes the changes for that layer. |
@amalloy, how are we going to get the read function into transform? |
I don't think we need to. Transform (on a ruminate layer, I assume you On 09/06/2012 07:32 PM, Justin Balthrop wrote:
|
Oh. I thought transform was going to return new parameters that could be passed to But even so, how do you vary the io-value returned based on calling |
Right, I realized on the way home that it's crazy, we do need to make On 09/06/2012 08:54 PM, Justin Balthrop wrote:
|
I think the format the transform takes has to be a little more complicated, like: given a keyseq on the source layer, it returns something like:
I haven't thought it through, and I'm sure the details aren't quite right, but does the approach make sense? |
I don't think that will be enough. What if the transform has to do a walk to calculate the updates? In that case, the list of keys to read depends on the values read at the previous walk step. |
We did a bunch of pair programming today designing stuff. Code attached for safekeeping; explanation to follow in the next day or so. (defn advance-reader [read actions]
(reduce (fn [read wrapper]
(wrapper read))
read, (map :wrap-read actions)))
(defn jiraph-compose [& fs]
(fn [read]
(first (reduce (fn [[actions read] f]
(let [more-actions (f read)]
[(into actions more-actions)
(advance-reader read more-actions)]))
[[] read]
fs))))
(defn read-wrapper [layer write-keyseq f args]
(fn [read]
(fn [layer' read-keyseq]
(if-let [[read-path update-path get-path]
(and (same? layer layer')
(path-parts read-keyseq write-keyseq))]
(-> (read layer' read-path)
(apply update-in* update-path f args)
(get-in get-path))
(read layer' read-keyseq)))))
(defn update-in-node [layer keyseq f args]
(fn [read]
[{:write (fn [layer]
(apply update-in-node! layer read keyseq f args))
:wrap-read (read-wrapper layer keyseq f args)
:layer layer :keyseq keyseq :f f :args args}]))
(defn ->retro-ioval [ioval]
(let [actions (ioval jiraph.graph/get-in-node)]
(with-actions nil
(reduce (fn [retro-ioval {:keys [layer write]}]
(update-in retro-ioval [layer] (fnil conj []) write))
{}, actions))))
(fn [read]
(let [people-update ((update-in-node people ["alan" :chair] inc) read)
[old-idx new-idx] ((juxt read (advance-reader read people-update))
people ["alan" :chair])]
(reduce into people-update
(when-not (= old-idx new-idx)
[((update-in-node layer [old-idx] disj "alan") read)
((update-in-node layer [new-idx] conj "alan") read)]))))
(defn top-level-indexer [field]
(fn [[source index] keyseq f & args]
(fn [read]
(let [source-update ((apply update-in-node source keyseq f args) read)
read' (advance-reader read source-update)]
(reduce into source-update
(when-let [id (first (if (seq keyseq)
(when (or (not (next keyseq))
(= field (second keyseq)))
keyseq)
args))]
(let [[old-idx new-idx] ((juxt read read') source [id field])]
(when (not= old-idx new-idx)
[((update-in-node index [old-idx] disj id) read)
((update-in-node index [new-idx] conj id) read)]))))))))j |
MotivationSo, to explain the code above. The problem we discovered, with both of the jiraph-iovalue structures @ninjudd and I discussed last week, was that there is no good way to provide the The solution we decided on is to make jiraph's IOValue be a function taking as input a The items in this vector of maps may not be executed exactly in sequence (after conversion to Retro IOValues, they will be "batched", one transaction per layer), but the Actualized IOValueAs mentioned previously, we introduced a new "type", a jiraph-specific analogue to the Retro IOValue. It is more transparent, so that more useful operations can be performed on it before actually committing changes to the database mutably. Specifically, each entry in the vector is a map with the following keys:
Implementation notes
Caution must be used when creating the |
As @amalloy and I were talking today, we realized that an alternative to implementing two-phase commits in flatland/retro#2 would be to make ruminate (#14) functional. So instead of the
ruminant-function
being a function with side-effects that updates the output layers, it will return anIOValue
with actions to perform on all the output layers. This can then be composed with the action map for the source layer and returned.As part of this change, we also discussed the possibility of adding a
jiraph.graph/Layer
protocol for all of the functional write methods. This would give us a more functional place to wrap layers, instead of wrapping the mutablejiraph.layer/Layer
. This wouldn't make any of the existing wrappers more difficult, and it would make functional ruminate way easier. As a consequence of this change, the bulk of the functionality injiraph.graph
would move from the mutable functions to the functional functions.The text was updated successfully, but these errors were encountered: