Thanks Jonah - I will digest this over coffee...

On 5 March 2015 at 16:45, Jonah Benton <jo...@jonah.com> wrote:
> Hi Colin- a simple example that uses a flow operator would be
>
> (-> context
>   do-something
>   do-some-io
>   do-a-last-thing )
>
> with
>
> (defn do-something [context]
>   (let [some-data (get-in context [:path :to :data :element])
>          result (pure-edge-function some-data)]
>     (assoc-in context [:path :to :revised :data] result)))
>
> and
>
> (defn do-some-io [context]
>   (let [some-other-data (get-in context [:path :to :data :to :save])
>          database (get-in context [:path :to :database :resource])]
>     (save! database some-other-data)
>      context)))
>
> Then save! can be a plain function, or
>
> (defprotocol Saver
>   (save! [this data]))
>
> (defrecord DatabaseSaver [conn ... ]
>   Saver
>   (save!  ))
>
> (defrecord AtomSaver [a ... ]
>   Saver
>   (save! ))
>
> And context is a map of maps that contains both application state and edge
> resources that should provide or receive data.
>
> Encoding conditional steps in a data flow presents a challenge. Several
> variants for doing this are in flow operators in Clojure and there are also
> a number of third party approaches. The below is a concrete example from an
> internal library that is not open source but doesn't differ significantly
> from approaches that are.
>
> A "flow" looks like this:
>
> (defflow renew
>     { :start                valid-request?
>      valid-request?        { true valid-token? false invalid-request }
>      valid-token?          { true ingest-token false not-authorized }
>      ingest-token          renew-session
>      renew-session         renewed?
>      renewed?              { true return-renewed-token false not-authorized
> }
>      return-renewed-token   :end
>      not-authorized         :end
>      invalid-request        :end })
>
> It's a regular map with at least a :start key and an :end value. Symbols in
> a flow map are plain functions that take a context map.
>
> Semantically, those functions represent either actions or decisions. A
> function whose value in the flow map is itself a map with true and false
> keys is a decision. A function whose value in the map is another function is
> an action.
>
> This particular flow performs a renewal of a web-like "session" token. It
> runs in a Pedestal interceptor, so it receives a context map (with :request,
> :response and other keys) as a parameter and at the end it populates the
> :response key of the map.
>
> The way to read this is-
>
> 0. a context map is prepared.
> 1. the data flow runner looks for the :start key. the value of :start is
> valid-request?
> 2. The value of valid-request? is a map, rather than a function, so the flow
> determines valid-request? is a decision function- an ordinary function that
> takes a context and is expected to return true or false.
> 3. the flow runner executes valid-request? If it returns true, the flow
> runner proceeds to valid-token? using the same context map. If false,
> proceed to invalid-request
> 4. valid-token? is similarly a decision function, it's executed similarly
> and is also expected to return true or false. invalid-request is an action.
> It's expected to return a new context map- in this case it populates the
> :response with a 4xx :status and some other response attributes.
> 5. the flow engine keeps going between keys and values until it comes to a
> value of :end. Then the resulting context is the output of the flow.
>
> The action function in the above called "renew-session" can do IO to renew a
> user session in the database. It's copied below.
>
> (defn renew-session [context]
>   (let [database    (get-in context [:components :database])
>         session-key (get-in context [:work :session-key])
>         renewed?    (db/renew-session! database session-key)]
>     (if (not (nil? renewed?))
>       (assoc-in context [:work :renewed-key] session-key)
>       context)))
>
> It simply pulls the database connection from the context, calls into a db
> specific function, db/renew-session!, which takes a connection and a key,
> and could be a protocol function that accepts a Record that encapsulates a
> specific storage resource, or could be a multimethod.
>
> This model of a data flow with actions and decisions follows Amazon's Simple
> Workflow Service, and the clever encoding of that flow as a map is due to
> Cognitect.
>
> A few projects exist now that encode flows in data form with an engine to
> process them. There are some trade-offs- it takes practice to read them, you
> can trade off a little in performance. In the above there's a fair amount of
> boilerplate that could be abstracted away along the lines of of Prismatic's
> Graph.
>
> You get a lot of other benefits- a consistent system shape, separation of
> concerns between core logic and edge IO, consistent error handling,
> logging/auditing, etc. Flows can be reused and can themselves compose,
> because everything takes a context map. You could also generate a visual
> decision tree of the model, or a real time heat map of decisions or of
> action performance.
>
> Anyway- sorry, this isn't intended to pitch an internal library, rather to
> share another thought process around the problems you raised that points in
> the direction of modeling and then executing the data flow, rather than
> relying solely on the call stack.
>
>
>
> On Thu, Mar 5, 2015 at 9:39 AM, Colin Yates <colin.ya...@gmail.com> wrote:
>>
>> Sounds interesting - are there are instances that I can look at?
>>
>> On 5 March 2015 at 14:15, Jonah Benton <jo...@jonah.com> wrote:
>> > Yes, exactly.
>> >
>> > That's fine, a node in the "middle" of the data flow can be responsible
>> > for
>> > reaching out to the edge to synchronously get some data to feed into a
>> > decision performed by the data flow. It would just do that through the
>> > edge
>> > functions.
>> >
>> > A request for data could also be asynchronously initiated once enough
>> > context had been determined by the data flow, and a promise added to the
>> > context map for later derefing (likely with a timeout).
>> >
>> > If the request for the data is context-free, it could be pulled in at
>> > the
>> > start of the data flow.
>> >
>> > By edge, it's is not that IO can't be done in the middle of a dataflow,
>> > it's
>> > just that functions that deal with dataflow follow one pattern (taking a
>> > context map) so they can be easily composed and rearranged, while
>> > functions
>> > that deal with edge resources take those resources along with other
>> > parameters more explicitly, so the data flow can run in an insulated
>> > fashion.
>> >
>> >
>> >
>> > On Thu, Mar 5, 2015 at 8:55 AM, Colin Yates <colin.ya...@gmail.com>
>> > wrote:
>> >>
>> >> Hi Jonah,
>> >>
>> >> This sounds very much like the model layer in DDD, which ironically is
>> >> exactly what I am building.
>> >>
>> >> However, in the middle of a data flow a function needs to reach out to
>> >> a repository to make a decision - how does that fit in with the data
>> >> flow approach?
>> >>
>> >>
>> >> On 5 March 2015 at 13:39, Jonah Benton <jo...@jonah.com> wrote:
>> >> > Hi Colin,
>> >> >
>> >> > Another option, other than HOFs or dynamic binding, is what might be
>> >> > called
>> >> > a data flow approach.
>> >> >
>> >> > At the edge of the application are the functions that explicitly take
>> >> > parameterized resources to perform edge state IO. These might be bare
>> >> > functions, or they might be protocol implementations that await the
>> >> > delivery
>> >> > of a type to perform their operation, or multimethods.
>> >> >
>> >> > At the center are functions that take a context map, which contains
>> >> > all
>> >> > relevant application state- IO resources and transient state data.
>> >> > Those
>> >> > functions are arranged in a data flow and capture the logic or state
>> >> > transitions of the application independent of any specific IO
>> >> > commitments.
>> >> > They can use schema or type annotation or pre/post conditions to
>> >> > enforce
>> >> > invariants.
>> >> >
>> >> > When data flow processing arrives at a place where some edge IO
>> >> > should
>> >> > occur, these data flow functions act as adapters to get-in the
>> >> > appropriate
>> >> > resource or Record from the context map and call the edge functions
>> >> > to
>> >> > perform IO.
>> >> >
>> >> > The result is a wider, flatter system, propagating state explicitly
>> >> > rather
>> >> > than implicitly through the call stack.
>> >> >
>> >> > Jonah
>> >> >
>> >> >
>> >> >
>> >> > On Wed, Mar 4, 2015 at 12:58 PM, Colin Yates <colin.ya...@gmail.com>
>> >> > wrote:
>> >> >>
>> >> >> Hi,
>> >> >>
>> >> >> I am looking for the Clojure equivalent of:
>> >> >>
>> >> >> class Whatever {
>> >> >>     @Transactional
>> >> >>     void doSomething(IDoSomething one, IDoSomethingElse two) {
>> >> >>       one.doSomething()
>> >> >>       two.doSomething()
>> >> >>     }
>> >> >> }
>> >> >>
>> >> >> where both one and two are dependency injected with a proxy which
>> >> >> resolves
>> >> >> to a thread local database connection. In addition, one might itself
>> >> >> have a
>> >> >> collaborator which itself has a collaborator which needs a
>> >> >> datasource.
>> >> >>
>> >> >> So far I have two protocols:
>> >> >>
>> >> >> (defprotocol IDoSomething
>> >> >>  (do-something [this ...])
>> >> >>
>> >> >> (defprotocol IDoSomethingElse
>> >> >>  (do-something [this ...])
>> >> >>
>> >> >> Each protocol may have a number of implementations, one of which is
>> >> >> a
>> >> >> JDBC
>> >> >> implementation:
>> >> >>
>> >> >> (defrecord JdbcIDoSomething [db]
>> >> >>   (do-something [this ...] ...))
>> >> >>
>> >> >> The problem is that the calling code only gets provided an
>> >> >> IDoSomething
>> >> >> and an IDoSomethingElse and it wants to do something like:
>> >> >>
>> >> >> (let [one (->JdbcDoSomething db) two (->JdbcDoSomethingElse db)]
>> >> >>   (with-transaction [tx db]
>> >> >>     (do-something one)
>> >> >>     (do-something-else two)))
>> >> >>
>> >> >> The problem here is that the implementations of do-something and
>> >> >> do-something-else won't have access to the local bound 'tx', they
>> >> >> will
>> >> >> have
>> >> >> their own 'db'.
>> >> >>
>> >> >> I realise the general argument is to be explicit and pass a db as
>> >> >> the
>> >> >> first argument to the protocol but this isn't appropriate in this
>> >> >> case
>> >> >> as
>> >> >> there are validly multiple implementations. I could abstract a
>> >> >> 'unit-of-work' and pass that as the first argument to the protocols
>> >> >> but
>> >> >> that
>> >> >> seems a bit painful.
>> >> >>
>> >> >> Also, these protocols may be used quite far away from where the
>> >> >> database
>> >> >> code lives and passing a parameter all the way through the call
>> >> >> stack
>> >> >> is
>> >> >> painful.
>> >> >>
>> >> >> I am using Stuart Sierra's components if that makes any difference.
>> >> >>
>> >> >> I can't be the first person to run into this but google is
>> >> >> surprisingly
>> >> >> unhelpful which makes me think I have missed something fundamental,
>> >> >> and
>> >> >> that
>> >> >> I have something upside down.
>> >> >>
>> >> >> What do you all do?
>> >> >>
>> >> >>
>> >> >> --
>> >> >> You received this message because you are subscribed to the Google
>> >> >> Groups "Clojure" group.
>> >> >> To post to this group, send email to clojure@googlegroups.com
>> >> >> Note that posts from new members are moderated - please be patient
>> >> >> with
>> >> >> your first post.
>> >> >> To unsubscribe from this group, send email to
>> >> >> clojure+unsubscr...@googlegroups.com
>> >> >> For more options, visit this group at
>> >> >> http://groups.google.com/group/clojure?hl=en
>> >> >> ---
>> >> >> You received this message because you are subscribed to the Google
>> >> >> Groups
>> >> >> "Clojure" group.
>> >> >> To unsubscribe from this group and stop receiving emails from it,
>> >> >> send
>> >> >> an
>> >> >> email to clojure+unsubscr...@googlegroups.com.
>> >> >> For more options, visit https://groups.google.com/d/optout.
>> >> >
>> >> >
>> >> > --
>> >> > You received this message because you are subscribed to the Google
>> >> > Groups "Clojure" group.
>> >> > To post to this group, send email to clojure@googlegroups.com
>> >> > Note that posts from new members are moderated - please be patient
>> >> > with
>> >> > your
>> >> > first post.
>> >> > To unsubscribe from this group, send email to
>> >> > clojure+unsubscr...@googlegroups.com
>> >> > For more options, visit this group at
>> >> > http://groups.google.com/group/clojure?hl=en
>> >> > ---
>> >> > You received this message because you are subscribed to the Google
>> >> > Groups
>> >> > "Clojure" group.
>> >> > To unsubscribe from this group and stop receiving emails from it,
>> >> > send
>> >> > an
>> >> > email to clojure+unsubscr...@googlegroups.com.
>> >> > For more options, visit https://groups.google.com/d/optout.
>> >>
>> >> --
>> >> You received this message because you are subscribed to the Google
>> >> Groups "Clojure" group.
>> >> To post to this group, send email to clojure@googlegroups.com
>> >> Note that posts from new members are moderated - please be patient with
>> >> your first post.
>> >> To unsubscribe from this group, send email to
>> >> clojure+unsubscr...@googlegroups.com
>> >> For more options, visit this group at
>> >> http://groups.google.com/group/clojure?hl=en
>> >> ---
>> >> You received this message because you are subscribed to the Google
>> >> Groups
>> >> "Clojure" group.
>> >> To unsubscribe from this group and stop receiving emails from it, send
>> >> an
>> >> email to clojure+unsubscr...@googlegroups.com.
>> >> For more options, visit https://groups.google.com/d/optout.
>> >
>> >
>> > --
>> > You received this message because you are subscribed to the Google
>> > Groups "Clojure" group.
>> > To post to this group, send email to clojure@googlegroups.com
>> > Note that posts from new members are moderated - please be patient with
>> > your
>> > first post.
>> > To unsubscribe from this group, send email to
>> > clojure+unsubscr...@googlegroups.com
>> > For more options, visit this group at
>> > http://groups.google.com/group/clojure?hl=en
>> > ---
>> > You received this message because you are subscribed to the Google
>> > Groups
>> > "Clojure" group.
>> > To unsubscribe from this group and stop receiving emails from it, send
>> > an
>> > email to clojure+unsubscr...@googlegroups.com.
>> > For more options, visit https://groups.google.com/d/optout.
>>
>> --
>> You received this message because you are subscribed to the Google
>> Groups "Clojure" group.
>> To post to this group, send email to clojure@googlegroups.com
>> Note that posts from new members are moderated - please be patient with
>> your first post.
>> To unsubscribe from this group, send email to
>> clojure+unsubscr...@googlegroups.com
>> For more options, visit this group at
>> http://groups.google.com/group/clojure?hl=en
>> ---
>> You received this message because you are subscribed to the Google Groups
>> "Clojure" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to clojure+unsubscr...@googlegroups.com.
>> For more options, visit https://groups.google.com/d/optout.
>
>
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clojure@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+unsubscr...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to the Google Groups
> "Clojure" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to clojure+unsubscr...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to