3
\$\begingroup\$

I am currently working on writing a library for use with LIFX light bulbs over their UDP API. I have written the code for creating the packet headers, but I'm unsure whether it is the most idiomatic approach, as I am new to Clojure. Please tell me what you think about the following code.

(ns lifx-lib.packet.header
  (:require [clj-struct.core :as struct]))

(def protocol-num 1024)
(def header-size 36)

(defn- frame-header
  [size tagged source]
  (struct/pack
    "<2HI"
    size
    (bit-or protocol-num 0x1000 (if tagged 0x3000 0x0000))
    source))

(defn- address-header
  [address resp ack sequence]
  (struct/pack
    "<2Q"
    address
    (bit-or
      (if resp (bit-shift-left 1 48) 0)
      (if ack (bit-shift-left 1 49) 0)
      (bit-shift-left sequence 56))))

(defn- protocol-header
  [type]
  (struct/pack "<Q2H" 0 type 0))

(defn create-header
  [params]
  (let [
      size     (:size params 0)
      tagged   (:tagged params true)
      source   (:source params 0)
      address  (:address params 0)
      resp     (:resp params false)
      ack      (:ack params false)
      type     (:type params 0)
      sequence (:sequence params 0)]
    (into []
      (concat
        (frame-header size tagged source)
        (address-header address resp ack sequence)
        (protocol-header type)))))
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Looks pretty good to me. There are a few shortcuts you could add if you don't mind using the Tupelo Clojure library. One of them is vals->map and with-map-vals.

Called with a list of symbols like (vals->map a b c) returns a map like {:a a :b b :c c}:

(let [a 1
      b 2
      c 3]
  (vals->map a b c))  ;=>  {:a 1 :b 2 :c 3} }

Given a map like {:a 1 :b 2 :c 3} (such as generated by (vals->map a b c)), performs safe let destructuring using grab like:

   (let [some-map  {:a 1 :b 2 :c 3} } ]
     (with-map-vals some-map [a b c]
        (+ a b c)))  ;=>  6

with-map-vals is safe for typos since grab will throw if the requested key is not present. See vals->map for simple creation of labeled data maps.

For default values, I think the simplest way is like this:

  (let [defaults {:a 1 :dir :up :name "joe"}
        v1       {:a 2}
        v2       {:a 9 :dir :left :name "sally"}]
    (spyx (into defaults v1))
    (spyx (glue defaults v2))

There is also the pure clojure way:

; Pure clojure way
(let [{:keys [a dir name ] } v1]
  (spyx :pure-1 (vals->map a dir name)) )

; can't use `defaults` here, need map with keyword => symbol
(let [{:keys [a dir name] :or {a 1 dir :up name "joe"}} v1] 
  (spyx :pure-2 (vals->map a dir name)))

(let [{:keys [a dir name ] :or {a 1 dir :up name "joe"}} v2] 
  (spyx :pure-1 (vals->map a dir name)) )

See also Clojure Destructuring.


Lastly, instead of concat I like to use glue.

; Glue together like collections:
(is (= (glue [ 1 2] '(3 4) [ 5 6] )       [ 1 2 3 4 5 6 ]  ))   ; all sequential (vectors & lists)
(is (= (glue {:a 1} {:b 2} {:c 3} )       {:a 1 :c 3 :b 2} ))   ; all maps
(is (= (glue #{1 2} #{3 4} #{6 5} )      #{ 1 2 6 5 3 4 }  ))   ; all sets
(is (= (glue "I" " like " \a " nap!" )   "I like a nap!"   ))   ; all text (strings & chars)

; If you want to convert to a sorted set or map, just put an empty one first:
(is (= (glue (sorted-map) {:a 1} {:b 2} {:c 3})   {:a 1 :b 2 :c 3} ))
(is (= (glue (sorted-set) #{1 2} #{3 4} #{6 5})  #{ 1 2 3 4 5 6  } ))

Lastly, I wouldn't bother with defn- It just makes testing harder and provides little value. If you have non-public functions that you don't want in the documentation (via the lein-codox plugin), you can add metadata to the function like:

(defn ^:no-doc my-helper-fn
  ...)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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