Combining Clojure macros: cond-> and as->

I want it all

by Oliver Hine

Published 2015-06-10

Threading for the win

I love the way the threading macros allow me to keep code lean and clean, expressing what I want to do without having to concern myself with wiring function calls together.

When dealing with varying function signatures or repeated references to the threaded value, I have as->. When there are many branches in logic and I want conditional threading I have cond->.

But sometimes cond-> has the same problem as -> when function signatures differ; and by the way, wouldn't it be nice if my condition predicates could use the current value being threaded rather than those bound before the threading block began?

Does exactly what it says on the tin

condas-> combines the semantics of cond-> and as-> to give you more flexible conditional threading. You may not need to use it often, but in the more complicated areas of your codebase where readability is valued at a premium you will hopefully find it helps.

(defmacro condas->
  "A mixture of cond-> and as-> allowing more flexibility in the test and step forms"
  [expr name & clauses]
  (assert (even? (count clauses)))
  (let [pstep (fn [[test step]] `(if ~test ~step ~name))]
    `(let [~name ~expr
           ~@(interleave (repeat name) (map pstep (partition 2 clauses)))]
       ~name)))

Now when I have data like a Google geocode response (geo-location.json in the code below) I can write code like the following, keeping more context inline and reducing the need for lexical binding. Note that the location value is available for use in the conditionals allowing logic to be separated into cleaner and simpler parts.

(condas-> (json/decode (slurp "geo-location.json") keyword) location

  (some #{"street_address"} (:types location))
  (assoc location :address? true)

  (:address? location)
  (assoc location
         :country
         (:short_name
          (first (filter #(every? (set (:types %)) #{"country" "political"})
                         (:address_components location))))

         :postcode
         (:short_name
          (first (filter #(every? (set (:types %)) ["postal_code"])
                         (:address_components location)))))

  (= "US" (:country location))
  (-> location
      (assoc :zipcode (:postcode location))
      (dissoc :postcode))

  (when-let [country (:country location)]
    (not= "UK" country))
  (assoc location :overseas? true))

Conga

Google geocode response

{
  "address_components": [
    {
      "long_name": "277",
      "short_name": "277",
      "types": [
        "street_number"
      ]
    },
    {
      "long_name": "Bedford Avenue",
      "short_name": "Bedford Ave",
      "types": [
        "route"
      ]
    },
    {
      "long_name": "Williamsburg",
      "short_name": "Williamsburg",
      "types": [
        "neighborhood",
        "political"
      ]
    },
    {
      "long_name": "Brooklyn",
      "short_name": "Brooklyn",
      "types": [
        "sublocality_level_1",
        "sublocality",
        "political"
      ]
    },
    {
      "long_name": "Kings County",
      "short_name": "Kings County",
      "types": [
        "administrative_area_level_2",
        "political"
      ]
    },
    {
      "long_name": "New York",
      "short_name": "NY",
      "types": [
        "administrative_area_level_1",
        "political"
      ]
    },
    {
      "long_name": "United States",
      "short_name": "US",
      "types": [
        "country",
        "political"
      ]
    },
            {
      "long_name": "11211",
      "short_name": "11211",
      "types": [
        "postal_code"
      ]
    }
  ],
  "formatted_address": "277 Bedford Avenue, Brooklyn, NY 11211, USA",
  "geometry": {
    "location": {
      "lat": 40.714232,
      "lng": -73.9612889
    },
    "location_type": "ROOFTOP",
    "viewport": {
      "northeast": {
        "lat": 40.7155809802915,
        "lng": -73.9599399197085
      },
      "southwest": {
        "lat": 40.7128830197085,
        "lng": -73.96263788029151
      }
    }
  },
  "place_id": "ChIJd8BlQ2BZwokRAFUEcm_qrcA",
  "types": [
    "street_address"
  ]
}
submit to reddit