4 ms·
You're making so many broad, inaccurate and false generalizations that I won't bother replying to every item. But let's take macros: yes, as an average applica
by hocuspocus 5y ago
You're making so many broad, inaccurate and false generalizations that I won't bother replying to every item.
But let's take macros: yes, as an average application developer, you should probably avoid macros. But for some problems they're a way better tool than alternatives like runtime reflection or code generation. For instance I'm happy to use macros-based libraries that automatically derive JSON codecs. Unless you're a library developer, macros and their inherent complexity (outside of the Lisp family I guess) aren't an issue in FP languages that I know of.
- TeMPOraL 5y ago> yes, as an average application developer, you should probably avoid macros. I disagree. I think you should just learn to use them correctly. They're just another tool, and not a hard one to grok. If you're allowed to design code on your own - write modules, classes, APIs - then you should be able to handle macros responsibly as well.
- filoeleven 5y agoIn your experience with FP languages, how often do you reach for macros over functions, if you could ballpark it? Or if it makes more sense, in which scenarios are you more likely to reach for macros? The example I always hear about is creating DSLs. I haven’t done that yet with FP (Clojure), and I have not yet created a macro outside of learning exercises. So I have only a vague notion of when they are most useful. I’m not arguing to avoid them, but I have internalized the Clojure preference for “data over functions over macros.” This question is open to anyone. I’m mostly interested in the opinions of folks who have learned to use macros as “just another tool,” and who have a good set of heuristics for knowing when it’s the right tool.
- TeMPOraL 5y agoI did a quick scan of one of my side projects, and found a defun:defmacro ratio of 10:1, and (defun + defmethod):defmacro ratio of 16:1. In another codebase I worked on, the ratio is 7:1 and 9:1, respectively, but it gets tricky to count with grep[0]. Correcting for greppability, I'd estimate the ratio closer to 20:1. That may seem like a lot of macros, but it's actually few functions. A big reason to use macros is to have them generate functions for you, or remove the need for having them in the first place[1]. The macros I write tend to cluster in two categories, that you could almost call "functional" and "imperative". 1. The "functional" macros are code generators that eliminate boilerplate. For example: (switch-key key-event ("RET" (funcall on-activate widget) t) ;; return T to mark event as consumed ("ESC" (... do some stuff ...) t) (t nil)) The line above looks like a simple switch-case, but it hides quite a bit of complexity. It ultimately expands to `cond` (Common Lisp's if/else/elseif), but: - It injects appropriate code to compare against data hidden in key-event, which is a structure containing codes and modifiers. - It injects code to transform string key descriptors like "RET" or "C-a" (CTRL+A) or "C-M-<F12>" (CTRL+ALT+F12) into something that can be compared against data in key-event. - It detects when the switch keys are string literals (like in example above), and instead of injecting code to convert them, it executes it during compilation[2] time - this means string literals have zero runtime performance impact, they're compiled into the right data structures up front. Conceptually, the macro is rather trivial - but this is already what Lisp people would call a DSL. This particular example is driven by the desire to have a switch-like API for key events over an Emacs-style keybinding notation, that's also zero-cost if keys are known at compile time. I have more macros in this style. What makes these macros "functional" is that the code they generate doesn't have any compile-time or runtime side effects (other than those introduced by user code that's given as input to a macro invocation). 2. The "imperative" macros, or state-modifying macros, that modify compilation state. E.g. (define-event foo (some-slot default-value) "Documentation") This is a top-level macro that expands into: - A global variable containing event definition - as metadata for debugging, reflection, compile-time correctness checking. - A bunch of top-level functions like `make-foo` and `send-foo` (note the function name contains the name used in define-event). Other macros like this generate classes, or family of classes, or fill in some global hash tables, etc. These macros create proper DSLs - they contain complexity that allows me to make concepts like "events" or "components" behave as if they were a part of the language already. This almost always forces them to be "imperative" - they alter the language runtime state[3], by defining new functions, methods, classes, types, creating or modifying global variables, etc. But the resulting functions, or instances of classes, can be then used in regular functional code. -- On the "data over functions over macros", I'm not exactly sure what Clojurists mean by this, but I think I may have an idea. Many times, I've found myself writing an "imperative" macro that would define something as a global entity in the system, only to later wish I kept it as a data structure I can pass around. My favorite example is defining REST API routing. In one project I worked on, we had a bunch of top-level macro invocations like: (define-rest-function foobar-handler "/api/foo/bar" (some request params) "Some docstring" (actual handler body...)) The macro did the obvious thing - created a handler function, adorned with REST-related boilerplate, and registered it as the handler for the route "/api/foo/bar" in the global routing table. This pattern is something I see people commonly do, but it becomes a problem the moment you want to have more than one routing table - whether for testing or extending via composition. So these days, I try to avoid macros like these - that define a global state for things that are only "global" in the sense that, in production, there should be only one of them. From one little job I did in it, I recall the Clojure style is to keep such definitions as data structures, and I think it's a good idea. -- [0] - Macros are often used to generate functions, so in many places, code that's technically a function definition sits inside a macro invocation, making it hard to count with casual grepping like I'm now doing. [1] - In my experience, two main roles of a functions is modularity (including testability) and readability. Macros eliminate some of the latter - they let you simplify code that would otherwise have you create functions just for the sake of keeping another function's body simple. [2] - Actually, during "macro expansion time", which is separate from "compilation time" in Common Lisp, but it's usually the same thing. [3] - The distinction between "compile time" / "load time" and "run time" in Lisp family of languages is mostly a matter of opinion. Defining a class isn't much different conceptually from modifying a global hash table, except it usually happens during what one would consider "compilation" instead of "execution".
- filoeleven 5y agoThanks so much for the extensive reply! There’s a lot to chew on in there. I think I’d be comfortable implementing the first kind of macro (switch-key) in my own programs because it’s quite targeted in its goal and encapsulates some stuff to make the overall flow more readable. This makes sense to me, and I’m thinking about how they could improve my learning project. I probably just have to try sprinkling some in to get a better feel for them. Your second example (define-event) is more foreign to me, likely because I’m working in Clojure. Compile-time correctness checking isn’t really a thing there, and debugging is improving but (from what I hear) nothing like other Lisps. That leaves using imperative macros to create top-level functions / classes / hash table entries, and that’s beyond my current level of need/understanding and may not be idiomatic for the language. I don’t know enough to say for sure. Your last example definitely illustrates “prefer data over functions over macros.” Your estimate of 20 functions : 1 macro is also good evidence for at least the second half of that, too.