Search code examples
javaclojureletarity

Clojure let vs multi-arity


Thinking functional like, but in clojure, which is better, more performatic and less heavy to the JVM

(defn- on-message
  ([options ch {:keys [headers delivery-tag]} ^bytes payload ^CompanyProto$Company$Builder company]
   (check-id company)
   (save company options)
   (basic/ack ch delivery-tag))
  ([options ch ^PersistentHashMap kwargs ^bytes payload]
   (on-message options
               ch
               kwargs
               payload
               (-> (CompanyProto$Company/newBuilder)
                   (.mergeFrom payload)))))

or

(defn- on-message [options ch {:keys [headers delivery-tag] ^bytes payload}]
  (let [company (-> (CompanyProto$Company/newBuilder) (.mergeFrom payload))]
    (check-id company)
    (save company options)
    (basic/ack ch delivery-tag)))

Solution

  • The answer to your direct question is that there is no overhead introduced in a function call simply because that function happens to have multiple arities. In Clojure, all function calls are compiled into a method invocation. The exact method invoked depends on the number of arguments -- as far as the JVM is concerned, each arity is compiled as a different method. The determination of which method to call is made at compile-time, so there is no overhead at run-time.

    That being said, there is some overhead to making nested function calls. Specifically, each call grows the call-stack by a constant amount until that call returns. However, this overhead is minimal and unlikely to have a measurable impact on performace in this case.

    @ToniVanhala I did with let first, but a friend said that let is bad and not functional (he is a erlang developer), I'm new in functional langs... the other thing that he said is that is weird that clojure is not stackless

    This seems to be the real question. So it's worth addressing this.

    let is simply an expression that allows us to bind values to variables. These bindings are immutable. Furthermore, it's well-known that let can be implemented as a macro-abstraction over lambda (or, in Clojure's vocabulary, fn). So there's certainly nothing about let that makes it "not functional".

    There's also nothing "weird" or puzzling about Clojure not being stackless. Clojure is a JVM language, and a call-stack is deeply embedded in the JVM's abstract computational model. While there are ways to work around this (e.g., continuation-passing style), these methods either require abandoning deep JVM interop and/or paying a performance penalty. Clojure is first and foremost a pragmatic language, and this is a pragmatic concession.

    Besides, Clojure (being a Lisp) is really a language framework with which you forge the language you want. Your language might make different trade-offs. For example, your language might be "stackless". Or it might have first-class continuations (disclaimer: I am the author of pulley.cps). Or it might have a completely different paradigm (i.e., logic vs functional). But it can also be very simple. With Clojure, you can choose what you pay for.

    Ironically, your friend is right about the code not being functional. However, it's not because of the let. Rather, just about everything except the let is not functional. For example, (save company options) is clearly a side-effecting operation. That doesn't necessarily mean your code is bad, though -- even the purest functional program must, at some point, interact with the real world if it is to have any practical value.