Transition from infix to prefix notation
Asked Answered
C

4

9

I started learning Clojure recently. Generally it looks interesting, but I can't get used to some syntactic inconveniences (comparing to previous Ruby/C# experience).

Prefix notation for nested expressions. In Ruby I get used to write complex expressions with chaining/piping them left-to-right: some_object.map { some_expression }.select { another_expression }. It's really convenient as you move from input value to result step-by-step, you can focus on a single transformation and you don't need to move cursor as you type. Contrary to that when I writing nested expressions in Clojure, I write the code from inner expression to outer and I have to move cursor constantly. It slows down and distracts. I know about -> and ->> macros but I noticed that it's not an idiomatic. Did you have the same problem when you started coding in Clojure/Haskell etc? How did you solve it?

Coomb answered 24/4, 2012 at 0:1 Comment(7)
I think it's only idiomatic because it's what you're used to. The prefix notation is much more consistent than infix and so it can be easier to read once you get used to it.Ladyinwaiting
Mike, consider you need to write expression (filter #(expr1) (map #(expr2) expr3)). Would you start typing from inner expr3 or from filter?Coomb
I usually start from the outside and work my way in. So I think about what I want and just walk backwards from that.Ladyinwaiting
That's maybe the key: in functional language you have to thing in opposite direction: from what you need towards what you have (contrary to OOP)Coomb
(filter #(expr1) (map #(expr2) expr3)) => (filter expr1 (map expr2 expr3)), if expr1 and expr2 are named functions. Clojure was designed to eliminate some of the superfluous parentheses typical of older lisps.Montero
Brian, I cannot see, how this may be related to infix/postfix notation and order of arguments.Coomb
It's not exactly, but it's related to making your code simpler / more readable. Just thought I'd mention it in case you were unaware. No sense using more parens than you really have to.Montero
C
10

I felt the same about Lisps initially so I feel your pain :-)

However the good news is that you'll find that with a bit of time and regular usage you will probably start to like prefix notation. In fact with the exception of mathematical expressions I now prefer it to infix style.

Reasons to like prefix notation:

  • Consistency with functions - most languages use a mix of infix (mathematical operators) and prefix (functional call) notation . In Lisps it is all consistent which has a certain elegance if you consider mathematical operators to be functions
  • Macros - become much more sane if the function call is always in the first position.
  • Varargs - it's nice to be able to have a variable number of parameters for pretty much all of your operators. (+ 1 2 3 4 5) is nicer IMHO than 1 + 2 + 3 + 4 + 5

A trick then is to use -> and ->> librerally when it makes logical sense to structure your code this way. This is typically useful when dealing with subsequent operations on objects or collections, e.g.

(->>
  "Hello World" 
  distinct 
  sort 
  (take 3))

==> (\space \H \W)

The final trick I found very useful when working in prefix style is to make good use of indentation when building more complex expressions. If you indent properly, then you'll find that prefix notation is actually quite clear to read:

(defn add-foobars [x y]
  (+
    (bar x y)
    (foo y)
    (foo x)))
Crossland answered 24/4, 2012 at 1:42 Comment(4)
Inconvinience with -> and ->> is that you have two of them. Some functions (cons, map, apply) take composite object as last argument, and others (conj, update-in) as first and you cannot chain them togather with single -> or ->>. P.S. In pure OOP languages like Ruby & Smalltalk infix notation is completely consistent: 1.+(2), [1].zip([2]) and composite object is always the argument before dot.Coomb
@Alexey: There is a reason for -> and ->> and the differing argument order. It is due the the distinction between the seq abstraction and collections. I think this confuses because a collection is often treated as a seq. I often refer back to Rich Hickey's explanation - groups.google.com/group/clojure/msg/a8866d34b601ff43Mon
Also if you really want it, the swiss-arrows library provides the diamond wand macro: https://mcmap.net/q/446741/-generalized-threading-macro-in-clojure/148578Mon
I think that the real reason is the following: he needed for functions like conj variable number of arguments so he cannot put the primary argument last. Though it would be nice if -> behave like -<> if neededCoomb
T
4

To my knowledge -> and ->> are idiomatic in Clojure. I use them all the time, and in my opinion they usually lead to much more readable code.

Here are some examples of these macros being used in popular projects from around the Clojure "ecosystem":

Proof by example :)

Tutankhamen answered 24/4, 2012 at 1:21 Comment(0)
C
3

I did indeed see the same hurdle when I first started with a lisp and it was really annoying until I saw the ways it makes code simpler and more clear, once I understood the upside the annoyance faded

initial + scale + offset

became

(+ initial scale offset)

and then try (+) prefix notation allows functions to specify their own identity values

user> (*)
1
user> (+)
0

There are lots more examples and my point is NOT to defend prefix notation. I just hope to convey that the learning curve flattens (emotionally) as the positive sides become apparent.

of course when you start writing macros then prefix notation becomes a must-have instead of a convenience.


to address the second part of your question, the thread first and thread last macros are idiomatic anytime they make the code more clear :) they are more often used in functions calls than pure arithmetic though nobody will fault you for using them when they make the equation more palatable.


ps: (.. object object2 object3) -> object().object2().object3();

(doto my-object
   (setX 4)
   (sety 5)`
Counterproof answered 24/4, 2012 at 0:12 Comment(3)
Arthur, so do you thing that prefix notation is as convinient for writing the code as infix invocation of .methods with dot, or is it really less convenient but has other advantages?Coomb
once I found the .. macro I find inflix notation in jave very cumbersome and hard to both read and type. (this is just my humble opinion)Counterproof
@ArthurUlfeldt, can you please elaborate on "of course when you start writing macros then inflix notation becomes a must-have instead of a convenience"? Or did you mean to say prefix here?Devaney
M
3

If you have a long expression chain, use let. Long runaway expressions or deeply nested expressions are not especially readable in any language. This is bad:

(do-something (map :id (filter #(> (:age %) 19) (fetch-data :people))))

This is marginally better:

(do-something (map :id
                   (filter #(> (:age %) 19)
                           (fetch-data :people))))

But this is also bad:

fetch_data(:people).select{|x| x.age > 19}.map{|x| x.id}.do_something

If we're reading this, what do we need to know? We're calling do_something on some attributes of some subset of people. This code is hard to read because there's so much distance between first and last, that we forget what we're looking at by the time we travel between them.

In the case of Ruby, do_something (or whatever is producing our final result) is lost way at the end of the line, so it's hard to tell what we're doing to our people. In the case of Clojure, it's immediately obvious that do-something is what we're doing, but it's hard to tell what we're doing it to without reading through the whole thing to the inside.

Any code more complex than this simple example is going to become pretty painful. If all of your code looks like this, your neck is going to get tired scanning back and forth across all of these lines of spaghetti.

I'd prefer something like this:

(let [people (fetch-data :people)
      adults (filter #(> (:age %) 19) people)
      ids    (map :id adults)]
  (do-something ids))

Now it's obvious: I start with people, I goof around, and then I do-something to them.

And you might get away with this:

fetch_data(:people).select{|x|
  x.age > 19
}.map{|x|
  x.id
}.do_something

But I'd probably rather do this, at the very least:

adults = fetch_data(:people).select{|x| x.age > 19}
do_something( adults.map{|x| x.id} )

It's also not unheard of to use let even when your intermediary expressions don't have good names. (This style is occasionally used in Clojure's own source code, e.g. the source code for defmacro)

(let [x (complex-expr-1 x)
      x (complex-expr-2 x)
      x (complex-expr-3 x)
      ...
      x (complex-expr-n x)]
  (do-something x))

This can be a big help in debugging, because you can inspect things at any point by doing:

(let [x (complex-expr-1 x)
      x (complex-expr-2 x)
      _ (prn x)
      x (complex-expr-3 x)
      ...
      x (complex-expr-n x)]
  (do-something x))
Montero answered 24/4, 2012 at 20:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.