What is the :inline keyword in Clojure functions?

Viewed 1904

I've came across this code snippet in Clojure's source code:

(defn ==
  "Returns non-nil if nums all have the equivalent
  value (type-independent), otherwise false"
  {:inline (fn [x y] `(. clojure.lang.Numbers (equiv ~x ~y)))
   :inline-arities #{2}
   :added "1.0"}
   .....

What does :inline do?

Update: Also found good example here.

2 Answers

Note that in general, inlining provides little to no performance benefits, because the JVM JIT is able to inline hot function calls at runtime. A warmed-up JIT will thus often produce the same performance as an explicitly inlined function.

The benefits to inlining explicitly in Clojure are thus minimal, and it isn't even too clear to me when it would kick in, but arguably it would save you a tiny fraction of time when the JIT is cold, so on the first few times executing.

Another benefit of inlining is that it bypasses the Clojure boxing that normal function calls have. If you are using interop in a function where the Java method has overloads for specific primitive types, when wrapping it in a Clojure function, Clojure will at the call-site convert things back into boxed numbers, and your Java method will end up being called with a boxed variant. But if you inline it, it skips the function and so no boxing will happen, the value as-is, in the type it is currently, will be provided to the Java method.

How inlining works is as follows:

When the compiler sees a call to a Var, if the Var has :inline meta defined, instead of calling the function in the body of the Var, the compiler will call the :inline function where it passes to it the rest of the form non-evaluated and it will never call the function of the Var, but instead it will replace the whole form with the expansion returned by the :inline function.

If you only want to inline certain arities, but not all of them, you can optionally provide an :inline-arities meta on the Var as well, which is a predicate that will be called with the number of arguments of the current call and if it returns true, will use the :inline function to expand it, otherwise will call the function inside the Var as normal.

Remark that this works at the Var level, you cannot add :inline meta to a fn and expect it to be inlined, it only works if you have a Var.

Inlining is very similar to macros, in fact, you can inline things with a macro as well, the only difference is that macros have no fallback functions for where they can't be used, for example they cannot be used where only functions are supported, notably in higher-order calls. For example, you can't pass a macro as the reducing function to reduce. That's where inlining comes in, an inlinable function has both a macro that can inline the call where it can, and a function for where it can't, that way they work as a hybrid macro/function where the macro is used where macros can be used and the function is used where macros can't be used.

Because of that, it would be a little strange, even though it is permitted, that the inlined function, where it can run "as-a macro", would behave in a different way than when it can only run "as-a function".

This is why when you inline a function, please make sure the inlined function expands in a way that will behave as a function, and similarly to the fallback function for when it can't be inlined.

There also exists a definline macro, that defines an inlined function where it automatically infers the fallback function from the provided inlining function, that serves as a bit of a convenience in not having to write both. It is "experimental" I believe because I think it isn't clear if it would always successfully infer the fallback function you'd have written yourself. And also because I think they didn't know how useful it be, and it isn't as flexible as using the meta, since you can't control for specific arities. That said, it is usable if you want too.

Here's an example that explains well the difference between inline and macro:

(defmacro add-macro
  [a b]
  `(+ ~a ~b))

(definline add-inline
  [a b]
  `(+ ~a ~b))

(add-macro 1 2)
;; => 3

(add-inline 1 2)
;; => 3

(reduce add-macro [1 2])
;; Syntax error compiling at (src/playground.clj:62:1).
;; Can't take value of a macro: #'playground/add-macro

(reduce add-inline [1 2])
;; => 3

Macros work similar in that it applies to Vars with a meta tagged with :macro true, which tells the compiler not to call the function in the Var with the evaluated arguments, but instead with the non-evaluated arguments and replace the whole form with the expansion it returns. The compiler will check for the :macro meta before it does so for :inline, this is a detail, but if you were wondering, in the case both meta are found :macro and :inline, the macro will win.

Related