1
\$\begingroup\$

I'd like some help to improve this cli program. It queries a city's weather and pushes a weather report.

It takes two arguments: the city to query and the apikey for OpenWeatherMap. For example lein run glasgow abcde12345.

How to make it more idiomatic clojure? How can I avoid having to pass the apikey multiple times?

(ns sunshine.core
  (:gen-class)
  (:require [clj-http.client :as http])
  (:require [clojure.data.json :as json])
)


(defn _get
  [url]
  (http/get url)
)


(defn _post
  [url payload]
  (http/post url payload)
)


(defn as_json
  [text]
  (json/read-str text :key-fn keyword)
)


(defn query_weather
  [apikey coords]
  (as_json
   (:body (_get (str "https://api.openweathermap.org/data/2.5/weather?lat=" (:lat coords) "&lon=" (:lon coords) "&appid=" apikey "&units=metric")))
   )
)


(defn get_city_coords
  [apikey city]
  (let [{:keys [lon lat]} (first (as_json
   (:body (_get (str "http://api.openweathermap.org/geo/1.0/direct?q=" city "&appid=" apikey)))
   ))]
    {:lon lon :lat lat})
  )


(defn unpack
  [raw_message]
  {:desc (:description (first (:weather raw_message))) :temperature (:temp (:main raw_message))})


 (defn get_weather
   [apikey coords]
   (unpack (query_weather apikey coords) )
 )


(defn as_weather_report
  [city weather]
  (str "Current weather in " city ": " (:temperature weather) "ºC with " (:desc weather))
)


(defn notify
  [message]
  (if (:deleted (as_json (:body (_post, "https://api.keen.io/dev/null" {:message message}))))
    (println message)
  )
)


(defn -main
  "Query weather and notify"
  [& args]
  (let [city (first args) apikey (second args)]
    (notify (as_weather_report city (get_weather apikey (get_city_coords apikey city))))
    )
)

```
\$\endgroup\$

1 Answer 1

2
+25
\$\begingroup\$

Some general things:

  • ending parentheses belong to the same line
  • use a single empty line between definitions
  • Clojure uses kebab-case (so as_json should be as-json, query_weather becomes query-weather and so on), this applies to names of arguments as well

ns declaration:

  • :gen-class should be the last reference (order of references is: (:refer-clojure ...) (:require ...) (:use ...) (:import ...) (:load ...) (:gen-class))
  • you don't have to use :require twice, just use two (or more) vectors inside one :require
(ns sunshine.core
  (:require [clj-http.client :as http]
            [clojure.data.json :as json])
  (:gen-class))

Next three functions (Clojure usually doesn't use underscores, so I just renamed them and used kebab-case)

(defn url-get
  [url]
  (http/get url))

(defn url-post
  [url payload]
  (http/post url payload))

(defn as-json
  [text]
  (json/read-str text :key-fn keyword))

query-weather: you can use destructuring here, as well as -> (thread-first macro) to increase readability

(defn query-weather
  [apikey {:keys [lat lon]}]
  (-> (str "https://api.openweathermap.org/data/2.5/weather?lat=" lat "&lon=" lon "&appid=" apikey "&units=metric")
      url-get
      :body
      as-json))

get-city-coords: again, you can use -> (thread-first macro). When you compare this function with the previous one, you can note the same sequence of str, url-get, :body, as-json- maybe you can also avoid this repetition somehow

(defn get-city-coords
  [apikey city]
  (let [{:keys [lon lat]} (-> (str "http://api.openweathermap.org/geo/1.0/direct?q=" city "&appid=" apikey)
                              url-get
                              :body
                              as-json
                              first)]
    {:lon lon :lat lat}))

unpack: you can destructure again

(defn unpack
  [{:keys [main weather]}]
  {:desc (:description (first weather))
   :temperature (:temp main)})

get-weather: without change

(defn get-weather
  [apikey coords]
  (unpack (query-weather apikey coords)))

as-weather-report: destructure

(defn as-weather-report
  [city {:keys [temperature desc]}]
  (str "Current weather in " city ": " temperature "°C with " desc))

notify: if with only one branch is when, and you can use -> again

(defn notify
  [message]
  (when (-> (url-post "https://api.keen.io/dev/null" {:message message})
            :body
            as-json
            :deleted)
    (println message)))

-main: destructure, and this time, you can use ->> (thread-last) macro

(defn -main
  "Query weather and notify"
  [& [city apikey]]
  (->> (get-city-coords apikey city)
       (get-weather apikey)
       (as-weather-report city)
       notify))
\$\endgroup\$
2
  • \$\begingroup\$ That is awesome! thank you for the thorough review. One question: is it possible or desirable to curry the functions that require the apikey? Would it make easier to read the main function because it would use thread-first instead of thread-last? \$\endgroup\$
    – Pablo
    Nov 17, 2022 at 17:32
  • \$\begingroup\$ Another related question is, if i had to create a http conection session, how would i used it over the lifetime of the program? Whould I have to curry the get and post functions? What would that look like? \$\endgroup\$
    – Pablo
    Nov 17, 2022 at 17:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.