What's the logic behind the core language macros?

What you’re proposing is semantically just Common Lisp’s let, which you could express with a map literal for the bindings, except for possible side effects of course (I think someone already mentioned this). Clojure’s let corresponds more closely to Common LIsp’s let*.

The semantically parallel let does have its uses IMHO, but it’s not clear which one is the better default. Choices.

So, just implement it, call it something like plet or cl-let. It’s not as trivial as implementing let* by expanding to nested lets, but you can use Clojure’s destructuring and/or some gensym indirection.

1 Like

Addressing one point from OP that has gotten lost in deeper semantic discussions. In Common Lisp and Scheme, let and let* have this syntax:

(let ((a 1)
      (b 2)
      (c (list 'foo 'bar)))
  (cons (+ a b) c))

That’s very verbose, with parentheses stacked up. Working with any lisp requires dealing with adjacent parentheses–that’s OK–but we know they can be a source of error if one isn’t careful. Common Lisp’s let/let* is the syntactic starting point for Clojure’s let, but because Clojure’s let dispenses with delimiters around individual bindings, and uses brackets rather than parentheses in order to set off the bindings, it’s a lot easier to read, imo. Sure, you have to get used to it, and whitespace or commas are important for readability.

[I had not thought previously about Rich’s point quoted by @dave.liepmann that using brackets prevents seeing the bindings as a function call. That would have been be less of an issue with Common Lisp’s `let` syntax in Common Lisp, since `((foo bar) baz)` is almost never a function call. But it could be confusing in a Lisp-1 such as Clojure. Perhaps it is confusing sometimes in Scheme.]

1 Like

I wouldn’t say that having parentheses around each single clause is »very verbose«—it’s just a pair of parentheses. I personally feel more comfortable with them, e. g. because of better natural indentation after line breaks, and more natural working with structural editing on clauses. Such things involve tradeoffs.

1 Like
(defmacro chiclet [bmap & body]
  `(let [~@(reduce (fn [acc [k v]] (conj acc k v)) [] bmap)]
     ~@body))

(chiclet {x 1 y 2} (+ x y))
3

I would never use this lol. Parallel implementation(s) already exist ala plet and could added as a second pass after the map transform.

chiclet with symbol topsort would actually be pretty useful.

1 Like

I would try to make it fail at macroexpansion time if the user accidentally has syntactic interdependence between the bindings. You can do that by first binding the value forms to gensyms, then binding the gensyms to the actual symbols (or destructuring forms).

1 Like

This is exactly why I don’t like the “implicit pairs” syntax in Clojure. In Scheme, I don’t mind that extra pair of parantheses, but as someone who likes to fit everything into an 80 character line, I often find myself adding a line break between a binding and its value, which is really awkward to read if all bindings are stacked on top of each other without clear visual separation. Empty lines in between binding pairs make reading a bit easier, but they create even more gaps.

Also, long let-vectors with strict indentation often create huge gaps which lose visual cohesion. If the binding values are larger expressions, it seems like they are just lost somewhere in the void. With parantheses around each binding, there would be at least some structural unity to this mess.

Maybe some kind of typographic indentation guide in code editors like a dotted horizontal line would really help here.

An illustrative (and stupid) example:

(let [some-really-long-binding-name (fn [x y z]
                                     (* x
                                        (+ y z 1 2 3 (+ 4
                                                        (* 2
                                                           x
                                                           y)
      foo                           bar
      short-name                    (->> v
                                         (map my-mapping-fn)
                                         (filter nil?)
                                         (apply +)
                                         (reduce (fn [acc x]
                                                  (assoc acc x))
                                          [])
      x                             "help me, I am lost"]
 …)

vs with extra parantheses (I think square brackets would be most idiomatic here):

(let [[some-really-long-binding-name (fn [x y z]
                                      (* x
                                         (+ y z 1 2 3 (+ 4
                                                         (* 2
                                                            x
                                                            y)]
      [foo                           bar]
      [short-name                    (->> v
                                          (map my-mapping-fn)
                                          (filter nil?)
                                          (apply +)
                                          (reduce (fn [acc x]
                                                   (assoc acc x))
                                           [])]
      [x                             "help me, I am lost"]]
 …)

Even though they don’t help much with the huge gaps in indentation, the parantheses at least provide structural separation and I can quickly jump to the closing bracket with my cursor and take advantage of structural editing.

First example with stacked bindings and empty lines in between:

(let [some-really-long-binding-name
      (fn [x y z]
       (* x
          (+ y z 1 2 3 (+ 4
                          (* 2
                             x
                             y)

      foo
      bar

      short-name
      (->> v
           (map my-mapping-fn)
           (filter nil?)
           (apply +)
           (reduce (fn [acc x]
                    (assoc acc x))
            []))

      x
      "help me, I am lost"])

vs with extra parantheses:

(let [[some-really-long-binding-name
       (fn [x y z]
         (* x
            (+ y z 1 2 3 (+ 4
                            (* 2
                               x
                               y)]
      [foo
       bar]
      [short-name
       (->> v
            (map my-mapping-fn)
            (filter nil?)
            (apply +)
            (reduce (fn [acc x]
                     (assoc acc x))
             []))]
      [x
       "help me, I am lost"]])
2 Likes

I would say no. It’s personal probably, but for me, my refactors would often be adding behavior in-between things, and again, if I was using unordered let and suddenly needed order, now I have a lot of refactoring to do, if it’s default ordered, I just add the operation in-between wherever it needs to be and I’m done.

All my side effects almost always exclusively is orchestrated using let.

See this example here: Example of a complex business process to implement in Clojure. Please link to your solutions for alternative ways to implement the same in Clojure (or other languages). · GitHub

Most of my code is like that, I use a top-level let to orchestrate the end-to-end user/business operation, which delegates to small, either pure functions or only side-effecting functions.

Now, there’s probably some use of let here where not everything needs to be ordered, but again, the cognitive load to figure out what can and cannot be ordered for me is just too high. I’d rather not have to worry about it, unless I were to purposely want to parallelize or make some things concurent.

Thanks for the concrete example @didibus ! Yeah, that looks very different from what I write day to day (where I’m usually dealing with lots of interleaving math/conversions that make my brain hurt). There is some stuff there I’ve never even seen, like a binding with no binding

          _ (when-not valid-input? (throw (ex-info "Invalid input to bar."
                                                   {:type :invalid-input
                                                    :msg "All values of bar input must be numbers"})))

or get functions that are impure.
it’s such a different way of using let :slight_smile:

In the end it kinda reads like imperative code. The let logic is very step-by-step simple and linear and so it doesn’t get confusing to read it top to bottom. I can see how rewriting it with an unordered let wouldn’t benefit you really - and could actually kinda makes it look messier.

If the bulk of the code you write is like this then I can see how Clojure let seems like the better default. Thanks again for showing me something new ;))

1 Like

I feel you on a lot of your frustrations @peterh and I’ve sort of gone through the same thought process. I also tried to keep it to 80 columns…

For a while I wrote things as

(let [some-really-long-binding-name
      (fn [x y z]
       (* x
          (+ y z 1 2 3 (+ 4
                          (* 2
                             x
                             y)
      ;;
      foo
      bar
      ;;
      short-name
      (->> v
           (map my-mapping-fn)
           (filter nil?)
           (apply +)
           (reduce (fn [acc x]
                    (assoc acc x))
            []))
      ;;
      x
      "help me, I am lost"])

I think it’s a tad better than putting new lines

  • it feels faster to visually scan
  • I use Emacs’ Ctrl + Up/Down Arrow to jump between functions. So I only have new lines between functions

However in the end I returned to your first style

I decided this is an advantage. When I look at a let block I typically want to first quickly scan all the binding names - ignoring the righthand logic. Vertical alignment here is crucial. And second, I want to quickly jump to some binding logic and look at it in isolation. The floating “blocks” on the right ends up feeling very nice. You immediately know at what column it starts and it’s spaced out with room to breath

For what it’s worth, this (code that @didibus shared) looks more like the production Clojure code that I’m used to seeing on a day-to-day basis over the past decade plus.

1 Like

That’s clever :slight_smile: . And yeah without the check it’d be a bit dangerous. I’d give it a try but I’m guessing the destructuring bit would be a bit tricky to handle

Okay, then I’m the odd one out! :))

To me that just reminds me of all the statefullness and imperative code I’d been running away from by coming to Clojure. Looking at that example again I just have to conclude that maybe the problem space requires it and there is no clean way to abstract it away.

You guys are way better coders than me so I think network/web/db code is probably just kinda tricky that way and in my 10+ years coding I’ve never touched that whole area. So it’s my own blindspot and I don’t really want to judge stuff I don’t know.

But nonetheless it sort of leaves me with the feeling it’s orthogonal to functional programming and immutable datastructures (and I like when the tools synergize and lean into that). The more I look at it the goofier it feels. Stuff like empty binding b/c you’re trying to stuff something into the sequence of operations … I can’t help but feel you’re sorta using an escape hatch to fight the original intent of the form - if that makes sense?

You’re kinda cramming imperative linear C-like step by step code into something that’s really about setting some local bindings.

For instance something like this

(def a 10)
(def b (state-change-and-get a))
(change-some-state b)
(def c (get-some-state b))
(other-state-change c)

to my eyes looks like a bit less of a hack than

(let [a 10
      b (state-change-and-get a)
      _ (change-some-state b)
      c (get-some-state b)
      _ (other-state-change c)]
nil)

But you can’t do defs in a function - so it’s not a really a solution

Could just be a matter of familiarity though :))

When I’m not in frontend territory (where the DOM API is inherently object-oriented), I only really use the empty binding for throwing exceptions or to set temporary print statements for debugging intermediate let-bindings.

Impure let bindings are convenient in cases where there is a lot of interop with libraries from Java or JS that are not designed in a pure, functional style and I think that one of the great selling points for Clojure is its interoperability, so I don’t really mind this “escape hatch” here (although I agree that it feels awkward, maybe to remind us that we should avoid it… :)).

Maybe I’ll adopt your style with the semicolons between stacked let-bindings, which also seems like a nice opportunity to put comments there when needed!

1 Like

This is unfortunately often true. My position is that if these shenanigans are kept to a minimum then it is justified on grounds of pragmatism.

1 Like

Ya, that makes sense.

I think at some point it becomes unavoidable. When I model a business process for example, it’s inherently a series of steps with side-effects intermixed.

I find the missionary example, it’s linked as a comment in my gist, that’s the more declarative version I think, but it still is a bit like declaring the series of steps.

It’s still a huge improvement on a C-like, where you’d not only define the series of business steps, but also for each step you’d imperatively manipulate memory and memory slots and all that.

And if you take an OOP approach, you don’t model things like a flowchart as I did here. Normally in OOP you model things as hierarchies of objects and all that, one object calling another and on, following more of a layered design. This is a relatively shallow model, where steps are imperatively layed down at the highest level, like they say, imperative shell, functional core, and that’s the model I follow.

I do think there’s an alternative to flowcharts, that’s more declarative, which is dependency resolution. Instead of saying do A then B and give B the result from A. You say that B depends on A. You declare the dependency of each step, and the dependency resolution figures out the order from that.

I don’t think that’s what your let would do though. A let that works declaratively to track dependencies would still be ordered, but it would be based on what depends on what.

(let {b (+ 10 a)
      a (* 2 3)})

For example, this would know that a (* 2 3) has to run first and be bound to a before running b (+ 10 a).

But I suspect this isn’t what you want either, because this is even harder to read, you have to mentally realize that there is order and it’s not even top to bottom, but based on the dependencies.

2 Likes

I don’t agree. Just because something has sequential steps doesn’t mean that it’s »imperative«, at all. It is absolutely normal and good functional style when bindings in a let depend on each other. The thing to watch out for is that they operate on the same level of abstraction.

1 Like

I feel what I’m talking about is a sort of middle ground between flowcharts/steps and trees/declarative.

(defn my-new-func
  [arg1 arg2]
  (let [a (some-thing arg1)
        b (another-thing arg1 arg2)
        c (+ a b)
        d (do-another-thing b)
        e (do-more-stuff b c)]
    (final-thing e)))

Text is linear and the steps are linear. So they match each other. It’s very workeable

You can rewrite this as some declarative thing …

a depends on arg1
b depends on arg2
c depends on a and b
d depends on b
e depends on b and c

… but as you point out that’s really confusing to read and think about. (I couldn’t figure out the missionary thing… that looks complex). Even though you remove the order dependence, you actually don’t easily “see” the tree/graph. There isn’t really a good way to represent that (maybe the visual programming folks would disagree)

To repeat myself a bit here… the middle ground that I thought was always implicit is to use scoping (and indentation) to get you sort of half way there. There is an inherent hierarchy to the bindings b/c you can’t have cycles

arg1 arg2 are the base of the pyramid
then you have a and b
then c and d
then e

(defn my-new-func
  [arg1 arg2]
  (let {a (some-thing arg1)
        b (another-thing arg1 arg2)}
    (let {c (+ a b)
          d (another-thing b)}
      (let {e (more-stuff b c)}
        (final-thing e d)))))

When you refactor…

  • you get order independence at each level
  • you minimize the number of potential inputs (they’re up and to the left)
  • you minimize the number of downstream things that can be affected (they’re down and to the right)
  • drift rightward by one column is negligible.

I get now with your examples that there are good scenarios to have sequential code but just like we have scary symbols ! * @ for when there is “dangerous” stateful code - I’d sort of expect something similar for the scenarios when you have sequential (and probably stateful) code - where the reader should pay attention. But most of the time in our “functional core” we should have patterns that are easier to reason about (and I guess this part is arguable :smiley: )

I’m all for pragmatic escape hatches - it’s just nice when they’re obvious and highlighted. I like that you add prefixes for your functions - such as impure-

Now if there is a better way to represent state changes (with something like Missionary) - that seems a much more complex issue… and I don’t have anything smart to say there :))

anyways, sorry again if I’m sort of repeating myself here a bit

I mentioned a symbol topsort + chiclet macro

the topsort algorithm is here

The gist of the macro in handwaving steps:


  1. get the dependency graph by filtering the body for symbols on the LHS of the let bindings
  2. use topsort to generate the order of the declarations based on the dependency graph
  3. generate the standard let bindings based on topsort order
  4. name it something hip and blog about it as one clojure’s missing pieces.
  5. profit?

not too sure how to deal with impure stuff… maybe hide it in a subfunction.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.