leonoel / flow

13 stars 0 forks source link

Trying to understand the specifications #1

Open bsless opened 2 years ago

bsless commented 2 years ago

Hello,

In trying to understand missionary, I have followed your recommendation to go over the task and flow specifications first. I thought perhaps trying to formalize them or rephrase them might help me understand them better.

The comments are taken almost verbatim from the spec in the readme. Does the spec look correct?

(require '[clojure.spec.alpha :as s])

;;; A notifier is a zero-argument function provided by the consumer.

;;; Called by the producer each time it becomes ready to transfer a
;;; value, at which point it must stop signaling until the transfer is
;;; confirmed by the consumer.

;;; It must not be called after the terminator has been called.
;;; It must not block,
;;; it must not throw,
;;; its return value should be ignored.
(s/def :flow/notifier
  (s/fspec :args (s/cat)))

;;; A terminator is a zero-argument function provided by the consumer.

;;; It is called exactly once by the producer when:
;;; - the process instance has no more values to transfer and
;;; - all of its resources have been released.
;;; It must not block,
;;; it must not throw,
;;; its return value should be ignored.
(s/def :flow/terminator
  (s/fspec :args (s/cat)))

(defn derefable? [x] (instance? clojure.lang.IDeref x))

;;; An iterator is an object provided by the producer, it must be
;;; callable as a zero-argument function and derefable.

;;; deref is called by the consumer to trigger the transfer of a value,
;;; at which point the producer becomes allowed to signal again.

;;; The consumer must not call deref before the producer calls the notifier.
;;; deref must not block
;;; deref must return the transferred value

;;; It may throw an exception to indicate a failure,
;;;   in this case it must not call the notifier again.

;;; The consumer may call the iterator as a zero-argument function to
;;; cancel the process instance.

;;; The producer must expect this operation to happen at any time
;;; including after termination,
;;;   it must be idempotent,
;;;   it must not block,
;;;   it must not throw,
;;;   its return value should be ignored.
(s/def :flow/iterator
  (s/and
   (s/fspec :args (s/cat))
   derefable?))

;;; A flow is a function provided by the producer taking two arguments,
;;; a notifier and a terminator.
;;; It is called by the consumer to spawn a new instance of the process
;;; it must not throw
;;; it must not block
(s/def :flow/flow
  (s/fspec
   :args (s/cat
          :notifier :flow/notifier
          :terminator :flow/terminator)
   :ret :flow/iterator))

(defprotocol IConsumer
  (-notifier [consumer opts] "Create a [[:flow/notifier]]")
  (-terminator [consumer opts] "Create a [[:flow/terminator]]")
  (-spawn [this flow] "Call [[:flow/flow]] to return a [[:flow/iterator]].")
  (-iterate [this iterator]))

(def consumer? #(satisfies? IConsumer %))

(s/fdef -notifier
  :args (s/cat :consumer consumer?
               :options map?)
  :ret :flow/notifier)

(s/fdef -terminator
  :args (s/cat :consumer consumer?
               :options map?)
  :ret :flow/terminator)

(s/fdef -spawn
  :args (s/cat :consumer consumer?
               :flow :flow/flow)
  :ret :flow/iterator)

(s/fdef -iterate
  :args (s/cat :consumer consumer?
               :flow :flow/iterator)
  :ret any?)

(defprotocol IProducer
  (-flow [this opts] "Create a [[:flow/flow]]")
  (-iterator [this opts] "Create a [[:flow/iterator]]")
  (-notify [this notifier] "Call the `notifier` each time the producer is ready to transfer a value."))

(def producer? #(satisfies? IProducer %))

(s/fdef -flow
  :args (s/cat :consumer producer?
               :options map?)
  :ret :flow/flow)

(s/fdef -iterator
  :args (s/cat :consumer producer?
               :options map?)
  :ret :flow/iterator)

(s/fdef -notify
  :args (s/cat :consumer producer?
               :notifier :flow/notifier))
bsless commented 2 years ago

Feel free to use this alternative representation in the documentation, too, if you'd like

leonoel commented 2 years ago

Specs for :flow/notifier :flow/terminator :flow/iterator :flow/flow are correct but keep in mind they're not pure functions, and side effects are not encoded by the spec.

The producer and the consumer do not represent concrete objects, therefore I don't think it makes sense to reason about them in terms of types. The producer is the process logic encapsulated by the flow, and the consumer is the process logic actually running the flow.

Try to define a flow in the repl, then call this flow as a function. You are the consumer, deref the iterator and observe how the producer reacts.

(def flow (m/seed (range 3)))
(def it (flow #(prn :notify) #(prn :terminate)))          ;; prints :notify
@it                                                       ;; prints :notify, returns 0
@it                                                       ;; prints :notify, returns 1
@it                                                       ;; prints :terminate, returns 2