One more macro?

Hello clojurians,

I’ve just published a tiny library containing one macro.

It aims to cover/extend the scope of macros like if, let, when, if-let, cond etc…

For those who want some context and details, there is an extensive readme.
But you can also get a quick taste of it via its docstring.

Please let me know what you think about it :slight_smile:

Have a nice day.

4 Likes

I haven’t used it, so take my feedback with a grain of salt.

I find that the API semantics were designed very tastefully.

I’d think twice before introducing it in my team: as all those libs which are their own DSL (like Specter), you have to ponder the tradeoff between expressiveness and accessibility.

I’m not very comfortable with the interpretation of symbols syntax (the ! prefix etc.). I like relying on the invariant that symbols are ‘atomic’ (and I guess my tooling likes it even more than I do). Maybe this sort of annotation should go to symbol metadata, or to the namespace part of the symbol (destructuring provides an official precedent for that).

1 Like

Thank you for your feedback, I really appreciate it.

I think I understand what you mean by “libs which are their own DSL”. But in the meantime, I do not really think of it as a DSL, since it is not domain specific but general purpose and maybe more importantly it does not bring a whole bunch of ideas and elements into the host language (as Specter does). The thing that is the most DSL-ish is the prefixed symbols I believe.

After reading your comment last night, I tried to remove them and strip the idea to its minimal form. I share your concerns about tooling and ‘atomicity’ of symbols. And I think that if the whole code fit in a page it should really increase accessibility.

Here what I came up with:

(ns question-mark.stripped.core
  (:require cljs.core))

;; thunk : lambda of zero argument

(defn- thunk-symbols
  "generating symbols to hold case thunks"
  ([] (map #(gensym (str "case_" % "_")) (range))))

(defn- compile-case
  [{:as   case
    :keys [test bindings return next]}]
  (let [cont (when next (list next))]
    (cond
      test `(if ~test ~return ~cont)
      bindings (let [[b1 b2 & bs] bindings]
                 `(if-let [~b1 ~b2]
                    ~(compile-case (assoc case :bindings bs))
                    ~cont))
      :else return)))

(defn- cases->thunks
  [cases]
  (mapv (fn [case]
          (list (:symbol case) [] (compile-case case)))
        cases))

(defn- normalize-body
  [body]
  (if (odd? (count body))
    (concat (butlast body) [::bottom (last body)])
    (concat body [::bottom nil])))

(defn- body->cases
  [body destructure]
  (mapv (fn [[left right] [sym nxt]]
          (let [bindings? (vector? left)
                bottom? (= ::bottom left)]
            {:return   right
             :symbol   sym
             :next     (when-not bottom? nxt)
             :test     (if-not (or bindings? bottom?) left)
             :bindings (when bindings? (destructure left))}))
        (partition 2 (normalize-body body))
        (partition 2 1 (thunk-symbols))))

(defn- emit-form
  [body destructure]
  (let [thunks (-> (body->cases body destructure) cases->thunks)
        return (nth (first thunks) 2)]
    (if-let [bindings (some-> (next thunks) vec)]
      `(letfn ~bindings ~return)
      return)))

(defmacro ?
  [& body]
  (let [destructure
        (if (:ns &env)
          cljs.core/destructure
          clojure.core/destructure)]
    (emit-form body destructure)))

There is an error in your Readme. In boolean connectors, (= true "..." (and true nil)) is false. Sorry I don’t know how to comment on your Readme in situ.

1 Like

nice catch ! thank you :slight_smile: