Separating effects from business logic

Thanks a lot for sharing and taking the time to write your blog post.

I also were on a journey to search an approach that yields more pure functions. Last year I watched David Nolen’s talk and for the first time I saw something that really felt like software lego blocks. I slightly modified David’s approach by only allowing functions that receive a map, add something and return it. I wrote about it in the readme of the world lib. The world lib is already “deprecated” in our codebase, since we discovered that we need even less tools for the described approach.

I took the time to convert your code example into the style we use to structure our code. We started to use this style since last year and are happy so far.

(defn add-fixed-url
  [w]
  (assoc w
         :fixed-url
         (identity ;; lib.rss/fix-url
           (:url w))))

(defn add-request
  [w]
  (assoc w
         :request
         {:request-method :get
          :url (:fixed-url w)
          :headers {"User-Agent" "https://yakread.com"}
          }))

(defn add-response
  [{:keys [http/client] :as w}]
  (assoc w
         :response
         (client (:request w))))

(defn parse-urls
  [params]
  ;; lib.rss/parse-urls
  (:items (:body (:response params))))

(defn add-feed-urls
  [w]
  (assoc w
         :feed-urls
         (->> w
              (parse-urls)
              (map :url) ;; mapv
              (take 20)
              vec)))

(defn add-biff-tx
  [{:keys [session] :as w}]
  (assoc w
         :biff/tx
         (for [url (:feed-urls w)]
           {:db/doc-type :conn/rss
            :db.op/upsert {:conn/user (:uid session)
                           :conn.rss/url url}
            :conn.rss/subscribed-at :db/now})))

(defn add-biff-response
  [{:keys [feed-urls] :as w}]
  (assoc w
         :biff/response
         (if (empty? feed-urls)
           {:status                     303
            :biff.response/route-name   :app.subscriptions.add/page
            :biff.response/route-params {:error "invalid-rss-feed"}}

           {:status                     303
            :biff.response/route-name   :app.subscriptions/page
            :biff.response/route-params {:added-feeds (count feed-urls)}})))

(defn transact!
  [{:keys [biff/submit-tx] :as w}]
  (assoc w
         :biff/tx-report
         (submit-tx (:biff/tx w))))

(defn prepare
  [w]
  ;; Do all side-effect free steps in the prepare phase. For the sake of this
  ;; example I consider the http request as "side-effect free" since it only
  ;; reads. However, it might be desirable to separate it (still IO that
  ;; requires retry logic).
  (-> w
      (add-fixed-url)
      (add-request)
      (add-response)
      (add-feed-urls)
      (add-biff-tx)
      (add-biff-response)))

(defn handle!
  [w]
  (-> w
      (prepare)
      (transact!)))

(comment

  ;; Rich comments test:

  (def w
    {:url "https://news.ycombinator.com/rss"
     :http/client (fn [_request]
                    ;; fake HTTP response:
                    {:status 200
                     :body
                     {:items
                      [{:url "http://www.example.com/blog/post/1"}
                       {:url "http://www.example.com/blog/post/2"}]}})
     :biff/submit-tx (fn [_tx]
                       ;; fake transaction report:
                       {:tx-date (java.util.Date.)})})

  (-> w
      (prepare))

  (handle! w)


  ;; Alternative test approach:

  (defn add-fake-response
    [w]
    (assoc w
           :response
           {:status 200
            :body
            {:items
             [{:url "http://www.example.com/blog/post/1"}
              {:url "http://www.example.com/blog/post/2"}]}}))

  (defn add-fake-tx-report
    [w]
    (assoc w
           :biff/tx-report
           {:tx-date (java.util.Date.)}))

  (def test-w
    (-> w
        ;; Re-arrange the lego blocks for the test:
        (add-fixed-url)
        (add-request)
        (add-fake-response)
        (add-feed-urls)
        (add-biff-tx)
        (add-biff-response)
        (add-fake-tx-report))
    )

  ;; do the assertions on `test-w` ...
  )

3 Likes