Hey there!
Using fnlfmt
, I'd like to be able to specify configurable global body forms. I've got some macros where prefixing with with-
and def
doesn't make much sense. In order of preference:
~/.fnlfmtrc
file that looks for the first valid option from the current working directory, using package loaders, etc.;fnlfmt {:extra-body-forms [:if-let :with-logging :partial-right]}
I know fnlfmt
was designed to not require configuration, but unless fnlfmt
can dynamically determine macros, having configuration for body forms would allow for formatting more expressive DSLs and remove the need to extrapolate def
and with-
, particularly in cases where that's not desired.
I can open a PR, but I wanted to get thoughts first. 🙂
The difficulty is that fnlfmt is meant to implement some "standard" formatting style which other formatting tools (emacs, vim, etc) can also implement. If it loads configuration from a file that is specific to fnlfmt, then it's unreasonable for other formatting tools to be able to consistently agree with fnlfmt, which severely diminishes the utility of fnlfmt.
In theory this problem of having one standard implementation used by all tools could be solved by LSP, but unfortunately LSP does not implement the necessary commands to support this as the spec does not include any commands for reindenting the line you're editing. (LSP does include a command to format an entire file, but this is not sufficient as it erases authorship data for lines that would not otherwise be edited.)
It's a tricky problem. I don't like it, but given the desire to emphasize consistency across all tools so far I've yet to come up with a better solution than the convention of
with-*
anddef*
.
Thanks for getting back to me so quickly! I think I understand your reasoning here.
it's unreasonable for other formatting tools to be able to consistently agree with fnlfmt, which severely diminishes the utility of fnlfmt.
Could you expand on this? I'm not sure what formatting tools you would use with
fnlfmt
when it's considered canonical and I want to understand these use-cases better.(LSP does include a command to format an entire file, but this is not sufficient as it erases authorship data for lines that would not otherwise be edited.)
As in Git history, right? Yeah, that's a drag; I wonder if there are tools out there that specifically work with VCS in this way, albeit whether or not someone considers formatting as authorship is a whole other bikeshed. 😅
To demonstrate where I'm coming from, I started by comparing
fnlfmt
with other language formatters likegofmt
andprettier
as a means of applying a consistent formatting standard and that makes conceptual sense to me, assuming these are reasonable comparisons. (I like prettier's take on this in that by using the library, you're opting into a specific aesthetic.)When I do compare these, the only significant and meaningful difference I see between Fennel and Go / JavaScript are macros, which are inherently non-standard outside of Fennel's standard library. (Strings vs. keywords is the only other difference I could think of where context matters, but it's not relevant and is easily mitigated with
;;fnlfmt: skip
.)Given the code-generating nature of macros, I'm wondering if it's okay for a codebase to opt-in to losing the guarantees that a standard format gives you, in exchange for the ability to utilize macros on their own terms. I think this is possible without the slippery slope of allowing configuration to become too personalized, and I'd be curious to see if configuration could actually empower tooling as well. For example, tooling built on top of
fnlfmt
could benefit from access to explicit config over implicit config, e.g. a tool today needs to know aboutfnlfmt
macro heuristicswith-
anddef
, but in the future can read the same ruleset, especially iffnlfmt
provides the function to access the configuration.
If it's helpful to know, I use a neovim plugin called
conform
which runsfnlfmt
on save, and these are the workarounds I have enabled currently, as well as some I have tried before and / or thought about:#1. Customize and recompile
fnlfmt
with my macros in place, and install that globally / per-projectNot ideal, but it works well. This is what I use right now.
#2. Intercept the format function in
conform
and look for naming convention that tells me not to runfnlfmt
In this case with my test spec library, I named the files
file.spec.test
and looked for.spec.test
as an extension. This worked, but not havingfnlfmt
at all was a bummer.#3. Use
;;fnlfmt: skip
on each line.I don't think this is sustainable with nested macros. This could me mitigated by having
fnlfmt
skip all children expressions, but then you lose out on runningfnlfmt
where applicable.#4. Create another tool that runs
fnlfmt
first, then its own custom formatting.I'm wondering if this is what you meant by additional tooling, but this could be a wrapper library that calls
fnlfmt
, then corrects any mistakes fromfnlfmt
. It doesn't benefit from changes infnlfmt
and is more likely to duplicate effort, but this could be mitigated throughfnlfmt
exporting more inner functionality if necessary. This is a bit more verbose than I'd like (and requires me to also implement #2), but it could be an acceptable tradeoff.All that said, I'd love to hear more so I can understand your position and
fnlfmt
values better. At the moment, native configuration for macros infnlfmt
feels like the option with the least negative tradeoffs that the user can specifically opt into, and I also know you and other Fennel folks have far more context than I do about this. 🙂
Could you expand on this? I'm not sure what formatting tools you would use with
fnlfmt
when it's considered canonical and I want to understand these use-cases better.Maybe the best way to explain it is that fnlfmt's goal is not to be the canonical formatter--the goal is to be the reference implementation of the canonical formatting style. Every text editor that supports Fennel is also a formatting tool that should try to implement the same thing.
Without solving the M:N problem LSP-style, every complication to the formatting rules has its cost spread across N different editors.
Given the code-generating nature of macros, I'm wondering if it's okay for a codebase to opt-in to losing the guarantees that a standard format gives you, in exchange for the ability to utilize macros on their own terms.
I don't want to say this is a point we'll never compromise on, but I don't want to sacrifice the "implementation of the standard formatting algorithm" without exploring the alternatives.
For example, tooling built on top of
fnlfmt
could benefit from access to explicit config over implicit config, e.g. a tool today needs to know aboutfnlfmt
macro heuristicswith-
anddef
, but in the future can read the same ruleset, especially iffnlfmt
provides the function to access the configuration.You're right that the
with-*
anddef*
rules need to be better documented; having them noted in some sub-bullet of the readme isn't great. In fact, a full description of the canonical algorithm that fnlfmt implements should be its own separate document.As an example of an alternative, a configuration that does not require a full Fennel parser would be better. Rather than telling Vimscript and elisp programmers that in order to implement their indentation rules they have to understand the entire
.fnlfmtconfig.fnl
file, what if there was a.fnlmacros
file that contained a newline-separated list of identifiers that should be formatted as if they had a body form?I'm not really sure how much I think this is a good idea, but it's at least an example of an alternate solution with a lower implementation burden across the entire ecosystem.
If it's helpful to know, I use a neovim plugin called
conform
which runsfnlfmt
on save, and these are the workarounds I have enabled currently, as well as some I have tried before and / or thought about:This works fine for single-person repositories, but I highly discourage format-on-save for any codebase that has multiple contributors, due to the issues of authorship erasure. Indenting on a per-line or a per-function basis is the only thing that works on a larger scale.
(Strings vs. keywords is the only other difference I could think of where context matters, but it's not relevant and is easily mitigated with
;;fnlfmt: skip
.)This is one thing I think we can do better on without any negative trade-offs; recent changes to the Fennel parser now make it possible to wrap AST nodes in a way that can preserve formatting details like :kw-style or 0xfab0 notation for numbers. So there's potential for improvement here.
-Phil
I've been thinking about this some more, and I have another potential solution. What if instead of the rule of
with-*
anddef*
being assumed to have a body, we expanded it to any call which has a name that starts with a built-in body-having form? That would make it solet-foo
could be indented likelet
for example.
Nov 24, 2024 21:32:08 ~technomancy outgoing@sr.ht:
I've been thinking about this some more, and I have another potential solution. What if instead of the rule of
with-*
anddef*
being assumed to have a body, we expanded it to any call which has a name that starts with a built-in body-having form? That would make it solet-foo
could be indented likelet
for example.what about
each-other
andfor-tuna
style macros?-- Andrey Listopadov
what about each-other and for-tuna style macros?
Without knowing anything more about those macros, I think it would be appropriate to treat them as having a body?
Without knowing anything more about those macros, I think it would be appropriate to treat them as having a body?
Are these macros? I don't know. Same for
(fn defeat-enemy [...])
, meaning that evendef*
is a bit too broad.for-each
may well be a function likemap
. Yeslet-*
is unlikely to be a function, but if we're gonna make it such that all fennel body-forming macros have a user-naming pattern, we may incorrectly format a lot of code.There are a lot of body-forming macros: each, for, when, do, let, while. We can kinda count fn and macro here too.
Then, there are also false positive things like *collect and accumulate don't have bodies, but are still indented like they are. I'd change them to indent body expression with 4 spaces, and in case
collect
specifically indent two forms with 4 spaces, and the rest with 2, indicating that this is not exactly valid.
def
would probably be used in the same scenario aslocal
butlocal
is not indented like bodyform (I changed that in my config to indentlocal
as bodyform though)What I'm saying here is that it's too broad of a rule to add
each-*
,for-*
,while-*
,when-*
,do-*
,let-*
as patterns for something being a macro with a bodyform.But if we will do only some of them, why don't we do the other too? It'll be weird that this rule applies to some but not the other.
Anyway, these are just thoughts, I'm not against this, but I'll probably will have to do a lot of workarounds in my config if this gets to fennel-mode too to undo some false positives.