timbo
timbo

Reputation: 14334

Can avoiding nil conjoins be done better?

I have a token scanner that simply returns nil for characters I'm not interested in. Rather than conj the nils to my token vector and then later stripping them all out, I want to simply not add them.

I'm using

;; dont conjoin if value false
(defn condj [v val]
  (cond-> v, val (conj val)))

to do this. Is there a specific operator or a more concise implementation?

Upvotes: 4

Views: 1724

Answers (4)

optevo
optevo

Reputation: 2104

Consider using into instead of conj:

(into [1 2 3] nil) ;=> [1 2 3]

(into [1 2 3] [4]) ;=> [1 2 3 4]

Note: the downside is you must return results in a sequence which adds a bit of overhead. However, if you forget to do this you get an error, it makes it easy to extend the logic when you want to append more than one item, the code is easily understandable and doesn't require any custom functions to be created.

Upvotes: 0

Rulle
Rulle

Reputation: 4901

I believe you can use transducers for this. They are explained here. Our reducing function is conj and we construct a transducer (remove nil?) that turns this function into one that will ignore nil:

(def condj ((remove nil?) conj))

Note that remove is the opposite of filter. We can also implement condj using (filter some?), some? being a function that is true for any value except nil:

(def condj ((filter some?) conj))

It seems to work:

user=> (condj [3 4 5] 9)
[3 4 5 9]
user=> (condj [3 4 5] nil)
[3 4 5]
user=> (condj [3 4 5] false)
[3 4 5 false]

Upvotes: 5

Alan Thompson
Alan Thompson

Reputation: 29958

I like the cond-> version and often use that to avoid repetition in the if version. Don't forget to be explicit about false values, though. I also like to use Plumatic Schema to be explicit about the data shape entering and leaving the function:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [schema.core :as s]))

(s/defn condj :- [s/Any]
  "Conjoin an item onto a vector of results if the item is not nil."
  [accum :- [s/Any]
   item :- s/Any]
  (cond-> accum
    (not (nil? item)) (conj item)))

(dotest
  (let [result (-> []
                 (condj :a)
                 (condj :2)
                 (condj false)
                 (condj "four")
                 (condj nil)
                 (condj "last"))]
    ; nil is filtered but false is retained
    (is= result [:a :2 false "four" "last"])))

You may also be interested in another version using my favorite library:

(s/defn condj :- [s/Any]
  "Conjoin an item onto a vector of results if the item is not nil."
  [accum :- [s/Any]
   item :- s/Any]
  (cond-it-> accum
    (not-nil? item) (append it item)))

For more complicated forms, using the placeholder symbol it makes it explicit where the value is being threaded. I also like the convenience functions not-nil? and append since they also make the intent of the code plainer.

Upvotes: 3

Carcigenicate
Carcigenicate

Reputation: 45750

I think the overly simplified approach is the cleanest here (it's also slightly more concise):

(defn condj [v val]
  (if val (conj v val) v))

I find that this is much easier to understand quickly. The only downside is v is duplicated since it isn't being threaded, but that's not a big loss in such a simple function.

Upvotes: 3

Related Questions