Affect Oriented Programming (or how to escape the concrete jungle)

In commemoration of Juneteenth, I’m proud to announce the initial release of af.fect, a pre-alpha library for functional inheritance, or what I’m calling affect oriented programming. I tend to like releasing libraries on national holidays - especially those pertaining to freedom and liberty - and it’s been a lot of burning of the midnight oil to get here, but after a year of thinking on it and rewriting it, many recent weekends banging on it, and one long marathon weekend to get a simple expression of it out, things landed in place just in time for the holiday and I think it’s now at a place where I can present the idea.

Affective Programming

Ideally, we can keep all the benefits of functional programming, while clawing back inheritance from object oriented programming in order to ameliorate the excessive concretion we sometimes see in some code bases. More on the high-level philosophy later, let’s jump into some of the lower-level terminology of the library.

What is an affect?

An affect is an intentional effect. All affects are effects, but not all effects are affects. An affect, in the affective programming sense of the word, is both a higher order function and a lower order function - what we might call a dual order abstraction.

An affect has two phases of execution: its affective phase and its effective phase. To get a better idea of what we’re talking about, let’s jump into an example:

The effective context of the base affect is simply an identity over it’s arguments:

(af/fect 1 2 3)
;=> (1 2 3)

However, when you pass it a map with an :as keyword in it, it becomes a factory for producing another affect:

(defn strings->ints [& string-ints]
  (->> string-ints
       (map str)
       (mapv edn/read-string)))

(def +s
  (af/fect
   {:as ::+s :with mocker
    :op +
    :ef (fn [{:keys [args]}]
          {:args (apply strings->ints args)})
    :mock [[1 "2" 3 4 "5" 6] 21]}))

:as declares that the map being passed in is an environment map, which is also useful for debugging and referring to affects programmatically. Here we’re inheriting default behaviors from the base affect and adding new behaviors to it with the passed in environment map.

:with allows us to mixin additional behaviors from additional affects.

:af and :ef allow us to pass in functions that update the internal environment of the affect. The :af function is an affector and the :ef function is an effector. Affectors update the environment prior to it being inherited from parent affects by child affects. Effectors update the environment prior to the execution of the operator (passed to :op or to :op-env receiving the whole environment) on the :args of the enviroment in its lower order, effective phase.

Let’s see its effective execution:

(+s "1" 2)
;=> 3

Of course, now we might ask, “why not just make it a normal function”, like: (defn +s [& args] (apply strings->ints args))?

Because now we can start building functions through accretions of behaviors, while not closing over all aspects of their implementations. Let’s easily add another behavior, with additional mock-checks, that allows us to add over ints, strings and vectors of ints.

(defn vecs->ints [& s]
  (->> s
       (reduce (fn [acc arg]
                 (if (vector? arg)
                   (into acc arg)
                   (conj acc arg)))
               [])))

(def +sv
  (+s
   {:as ::+sv
    :ef (fn [{:keys [args]}]
          {:args (apply vecs->ints args)})
    :mock [[1 [2]] 3]}))

(+sv "1" [2] 3 [4 5])
;=> 15

Here we can see that both the +s effector and the +sv effector have been comped together to create a new effector for use during effective execution time.

Also notice our new mock in +sv: A downstream implementer may not know about the internal details of +s, but if any new mocks violate the expectations of upstream implementations, those downstream mocks with throw errors at compile time because mocks run at compile time, during the affective phase of execution, during inheritance.

Let’s use our utility pretty-printing function to check out the internals of +sv to see how its current affective environment looks:

(+sv :af/pp)
=> {:args (),
    :finally [:base],
    :was :af.ex/+s,
    :is :af.ex/+sv,
    :joins [:mock :void :base],
    :affects [:mock-0 :with-0 :void-0 :base],
    :op #function[clojure.core/+],
    :void [:children :with :mock :blah],
    :effects [:af.ex/+sv-0 :af.ex/+s-0 :children-0 :base],
    :mocks [[1 "2" 3 4 "5" 6] 21 [1 [2]] 3]}

Here we can see that:

  1. The mocks have been concatenated together
  2. The effectors +sv and +s have been added to the :effects
  3. Mixing in mocker added its affector to the :affects and its joiner to the :joins
  4. :as became :is and the parent it inherited from became :was

And there’s a few other things to point out, but let’s go over the implementation of the mocker affect:

(defn failure-message [data input output actual]
  (str "Failure in "   (or (:is data) (:as data))
       " with mock inputs " (pr-str input)
       " when expecting "    (pr-str output)
       " but actually got "     (pr-str actual)))

(defn mocker-af [{:as env :keys [mocks op comp-effects]}]
  (when mocks
    (let [failures (->> mocks
                        (partition 2)
                        (mapv (fn [[in out]]
                                (assert (coll? in))
                                (println :comp-effects comp-effects)
                                (let [result (apply op (:args (comp-effects {:args in})))]
                                  (when (and result (not= result out))
                                    (failure-message env in out result)))))
                        (filter (complement nil?)))]
      (when (seq failures)
        (->> failures (mapv (fn [er] (throw (ex-info (str er) {})))))))))

(def mocker
  (af/fect
   {:as :mock
    :void :mock
    :join #(when (:mock %2)
             {:mocks (vec (concat (:mocks %1) (:mock %2)))})
    :af mocker-af}))

There are still some bugs to work out and there’s a lot more to go over in terms of how to use af.fect to create useful abstractions, but it may be more instructive to look at a more involved example, where lots of different techniques are employed to produce affects. For that purpose, I’ve implemented a frontend library for creating higher order components (HOCs).

comp.el

So, I’m also proud to announce the early, early pre-alpha of comp.el!

This whole exploration into functional inheritance was originally instigated by my annoyance with the over-abundance of concretions I was noticing in large Re-frame front-end applications. We would be building large component C, which wrapped large, complex component B, which wrapped an even more complex component A. Far too often, when wanting to build a component D, which was only slightly different than C, perhaps in how B behaved for C, we would have to re-implement a concrete version of A, B and then finally D, all over again.

Because we were always closing over our components with functions, it became hard to compose them together in a higher order way. We were lost in a concrete jungle with no way to abstract out common, reusable component behaviors. Thus, af.fect was born. That was over a year ago and many iterations later, I’ve got something working well enough to build some basic things. The API is likely to change and there’s definitely a lot of improvements to be made, but I figured I could finally spike something out to start a wider discussion.

The comp.el repo has an example project implementing a basic todomvc, which builds on top of Re-frame’s todomvc example. I dislike working with CSS syntax and files, so I brought in material-ui and Radiance for css-in-cljs. And it works as you’d expect:

You can try it out live here: comp.el todomvc

The beginnings of the comp.el library are here: comp.el/src/comp at main · johnmn3/comp.el · GitHub

And the meat and potatoes of their usage in the todomvc app are in the views namespaces in the example directory here: comp.el/ex at main · johnmn3/comp.el · GitHub

The library builds components by wrapping affective elements like so:

(def el
  (af/fect
   {:as ::el :with [s/radiant c/click p/props p/void]
    :op-env form-1-or-2}))

(def div
  (el
   {:as ::div
    :props {:comp :div}}))

(def grid
  (el
   {:as ::grid
    :props {:comp mui-grid/grid}}))

(def container
  (grid
   {:as ::container
    :props {:container true}}))

(def item
  (grid
   {:as ::item
    :props {:item true}}))

#_...etc

These components can then be used to compose together more components or can be used directly within a reactive hiccup tree in Reagent or Re-frame.

In the todomvc example, you can see how I build up components first as abstract affects and them define more traditional, concrete components for hanging in the finally hiccup tree.

(def new-todo
  (comp/el
   {:as ::new-todo
    :props (merge styled/new-todo
                  {:comp todo-input
                   :placeholder "What needs to be done?"})}))

(defn new-todo-box []
  (let [all-complete? @(subscribe [:all-complete?])]
    [comp/container styled/new-todo
     [comp/item {:xs 1
                 :on-click #(dispatch [:complete-all-toggle])}
      [comp/arrow-down {:font-size "large"
                        :style (merge {:padding-top 3
                                       :padding-left 0
                                       :color        "#e6e6e6"}
                                      (when all-complete?
                                        {:color "#737373"}))}]]
     [comp/item {:xs 11}
      [new-todo {:on-save #(when (seq %)
                             (dispatch [:add-todo %]))}]]]))

I was mostly following exactly how the Re-frame todomvc example was handling state. In a real world application, I’d more likely be baking all state management into specific affects that handle state in a more coordinated fashion.

I’ll be adding a form validation example to the repo soon that does just that, going back to the original impetus for the library. Here’s the general gist of what it looks like though:

(def input
  (comp/el
   {:as ::input :with [hide-required use-state validations]
    :props {:comp mui-grid/text-field}}))

(def form-input
  (input
   {:as ::form-input
    :props {:style {:width "100%"
                    :padding 5}}}))

(def email-input
  (form-input
   {:as ::email-input
    :label "Email"
    :props {:placeholder "john@example.com"
            :helper-text "validating on blur"
            :validate-on-blur? true}
    :valid [#(<= 4 (count %))        "must be at least 4 characters"
            #(= "@" (some #{"@"} %)) "must contain an @ symbol"
            #(= "." (some #{"."} %)) "must contain a domain name (eg \"example.com\")"]}))

(def password ; <- abstract
  (form-input
   {:as ::password-abstract
    :props {:label "Password"
            :type :password}
    :valid [#(<= 8 (count %)) "must be longer than 8 characters"]}))

(def password-input
  (password
   {:af ::password-input
    :validate-on-blur? true}))

(def second-password-input
  (password
   {:as ::second-password-input :with submission
    :valid    [#(= % (password-input :state))
               "passwords must be equal"]
    :fields   [email-input password-input second-password-input]
    :props {:on-enter (fn [{:as _env :keys [fields]}]
                        (ajax-thing/submit-fields fields))}}))

(def submit-btn
  (btn
   {:as ::submit-btn :with submission
    :props {:variant  "contained"
            :color    "primary"
            :on-click (fn [{:as _env :keys [fields]}]
                        (ajax-thing/submit-fields fields))}
    :fields [email-input password-input second-password-input]}))

#_...impl

(defn form [{:as props}]
  [container
   {:direction "row"
    :justify   "center"}
   [item {:style {:width "100%"}}
    [container {:direction :column
                :spacing 2
                :style {:padding 50
                        :width "100%"}}
     [item [email-input props]]
     [item [password-input props]]
     [item [second-password-input props]]
     [container {:direction :row
                 :style {:margin 10
                         :padding 10}}
      [item {:xs 8}]
      [item {:xs 4}
       [submit-btn props
        "Submit"]]]]]])

That’s the general gist of it - keep things as abstract affects until you need to hang them in the concrete hiccup tree. Don’t concretize the components until you need to.

Still working on the API - I have been mixing prop attrs into the environment map, and then cleaning out the environment at the end of the effective phase of execution, but I decided to keep them in their own props map here, to make more clear to readers what’s going on - that there’s a difference between environment attrs and prop attrs - but I may switch it back to keeping them all in the same environment map.

Feel free to check it out, kick the tires, mull over the idea and think about how the whole paradigm could be improved or simplified. It’s in the very beginning stages of development and I’d be very open to opinions on what you think the pros and cons of different strategies might be and how it should work.

Happy Juneteenth!

I’d like to wrap up with some discussion on this Juneteenth holiday. Juneteenth is the oldest celebration of the ending of slavery in the United States. On January 1, 1863, President Lincoln signed the Emancipation Proclamation abolishing slavery but it took another two and a half years, on June 19, 1865, for the news of freedom to reach the rest of the country, marking the beginning of emancipation for many of our African American brothers and sisters.

On that day of June 19th, Union Army general Gordon Granger announced, in Galveston, Texas, General order No. 3, stating:

“The people of Texas are informed that in accordance with a Proclamation from the Executive of the United States, all slaves are free. This involves an absolute equality of rights and rights of property between former masters and slaves, and the connection heretofore existing between them becomes that between employer and hired laborer.”

While that day was not a victory, it was America’s Second Independence Day and echos our pursuit for freedom, liberty and justice for all and is a cause for celebration around the world. If Juneteenth isn’t normally a holiday for you, consider making it one, where ever you are!

Happy holidays, warm wishes, much love and happy hacking Clojure folks!

7 Likes

congrats on releasing this library!

looks interesting, especially the example with todomvc – i will need more time to understand all of it :slight_smile:

looking at the GitHub readme, you mention the :af keyword but it’s not part of the code snippet (except for the mocker) – is that intented?

1 Like

Hey @teawaterwire, thanks! Yeah, the readme was a bit of a late night brain dump - it needs a whole rewrite to make things clearer.

Some things to note - I may add the ability to create an affect without using an :as keyword in the environment map, like by prepending a ^:as/af before the environment map, or something, so as to separate the constructor logic from potentially external parameter data, but I wanted to keep things simple at first so that folks can see that there’s no magic going on here - it’s just maps and functions. In prior iterations I’ve tried extending the protocols for maps, using just metadata on an affect to carry the environment, and a few other techniques, which are all viable, but this version is the simplest to understand for expository purposes. There’s also the idea of having an explicit affect or derive function that derives a new affect from an old one, but I do happen to like the minimalism of the current API, and how you don’t have to require in extra stuff, as the api follows the affects around implicitly - downstream consumers don’t need to bring in af.fect to use and extend affects.

We might also want to explore using interceptor chains for the affector and effector stacks, to allow for more fine-grained control over the different phases of the affect - construction time and execution time. I’ve also considered having more stacks - beginning, middle and end - for each phase, but again that’s harder to understand as an introduction to the general idea I’m trying to present, so I went with simple for now.

Anyway, the challenge is “what would a Clojurey take on inheritance look like” and I’d be interested in seeing alternative takes as well. I’m interested in what the simplest possible inheritance scheme could be, what folks would want from the API and what incentives and tradeoffs we should be considering.

2 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.