The following functions compile in really abnormal ways:
(fn a [...] (+ ...))
(fn b [...] (- ...))
(fn c [...] (* ...))
(fn d [...] (/ ...))
(fn e [...] (% ...))
(fn f [...] (// ...))
The result I got from fennel 1.3.0 is:
local function a(...)
return ...
end
local function b(...)
return ( - ...)
end
local function c(...)
return ...
end
local function d(...)
return (1 / ...)
end
local function e(...)
return ...
end
local function f(...)
return (1 // ...)
end
return f
At the bare minimum, it would be helpful if this was some kind of error. Ideally, I would have these compile into some simple loops that are capable of handling things in more intuitive ways. For example, for the first one, something like:
function a(...)
local out = 0
for i = 1,select('#', ...) do
out = out + select(i, ...)
end
return out
end
Thanks for making an awesome language, btw.
Yeah, this is legitimately a long-standing problem inherent in Fennel's design.
At the bare minimum, it would be helpful if this was some kind of error.
Making it an error to call
+
directly on...
is fine, (I would take a patch for this) but it wouldn't really solve the root problem; the problem is that any function could return multiple values, and it's impossible to determine this at compile-time.A more thorough fix would certainly be to compile out to an IIFE as you've described. However, there is a very significant performance overhead to this, especially when considering its impact on the tracing JIT; a call which used to be compiled down to a handful of native instructions might now get no JIT at all. Making every single addition call slower to paper over an edge case is not a good trade-off.
So a fix for this would be a new set of forms which do the job of the regular operators but are multival-friendly. I would certainly consider a proposal to introduce such a thing, since this comes up frequently. But I don't think that there can be any acceptable fix for the problem if you're not aware of it up-front, unfortunately. (Or maybe there is and we just haven't thought of it yet?) Perhaps just the existence of these alternate forms could be a kind of signal that raises awareness of the shortcomings of the regular operators.
I can think of a few solutions for now:
Make
+
into a macro that sees how many arguments it's being called with, and only generate a loop if it's a variadic call, else generatex + y + z ...
Make it an error to call variadic built-in operators, add new library functions
fennel.sum, fennel.mul
, etc. that are explicitly made in order to operate on multiple values, and suggest to use those in these edge cases.The thing is, this is only the tip of the iceberg for weird behaviors of fennel when using the built in math operators. I can't remember what other stuff I ran into, but I'll be sure to document it.
- Make
+
into a macro that sees how many arguments it's being called with, and only generate a loop if it's a variadic callIf this were possible we would have done it already. =)
(fn nums [] (values 1 2 3 4)) (fn calculate [f] (+ 3 (f))) (calculate nums)
How do you detect a variadic call at compile time?
- Make it an error to call variadic built-in operators, add new library functions
fennel.sum, fennel.mul
, etc. that are explicitly made in order to operate on multiple valuesSure, but the problem is there is no such thing as a library function in Fennel.
suggest to use those in these edge cases.
It's difficult to know how to suggest this in a way that would be effective.
The reference already describes this limitation, but people still run into problems with it.
Always open to suggestions about how to make it clearer of course!
A more thorough fix would certainly be to compile out to an IIFE as you've described. However, there is a very significant performance overhead to this, especially when considering its impact on the tracing JIT; a call which used to be compiled down to a handful of native instructions might now get no JIT at all. Making every single addition call slower to paper over an edge case is not a good trade-off.
I was wondering why no lisps (as far as I know) have substitution-like macros? In all lisps call to a macro is substituted to its expanded form, but what if there was a second type of macros that are just name substitutions wihtout the call context?
To illustrate this better, here's an example:
(macro-subst add `(fn [...] (faccumulate [res# 0 i# 1 (select :# ...)] (+ res# (select i# ...)))))
Now, any time the
add
symbol is met IT get's replaced with the form:(add 1 2 3) becomes:
((fn [...] (faccumulate [res_0_ 0 i_0_ 1 (select :# ...)] (+ res_0_ (select i_0_ ...)))) 1 2 3)
And
(let [plus add] (plus 1 2 3))
becomes
(let [plus (fn [...] (faccumulate [res_0_ 0 i_0_ 1 (select :# ...)] (+ res_0_ (select i_0_ ...))))] (plus 1 2 3))
So the idea is pretty similar to C's #define in that the macro doesn't use any of its surroundings, it is just a substitution (with all macro fancy stuff happingin to gensyms, e.t.c.).
This is just an idea, I'm not suggesting this as a solution.
However, if Fennel had such substitution macros, we could define function-like operators, that get compiled to IIFES, and can be used as almost normal functions, e.g. you can do (reduce add 0 [1 2 3]) because it is just an anonymous function after the substitution.
-- Andrey Listopadov
[Edited because of a brain blast I had while thinking about this issue and re-reading your reply to me]
I think this can be done in a sub-optimal way without extreme difficulty.
Only the last item being fed to + needs to be checked. All earlier ones are assumed to be summable.
If this last item is ... or a function call, use a loop. Otherwise, just rely on the arg count to generate a sum a + b + c + etc. If the programmer really wants to optimize their adding, they can use local variables to explicitly denote the number of arguments to +. Otherwise, we get predictable DWIM lisp behavior that feels elegant.
This macro would work, but the only problem is
(= (type last) :number)
doesn't cut it in terms of checking for ... or function calls -- it fires too easily, since the type of every macro argument tends to be "table".(macro +. [...] (let [len (select "#" ...) last (select len ...)] (if (= (type last) :number) (accumulate [sum 0 i v (ipairs [...])] (if (> i 1) `(+ ,sum ,v) v)) `(accumulate [sum# 0 _i# v# (ipairs ,[...])] (+ sum# v#)))))
If this last item is ... or a function call, use a loop.
This would be a significant performance regression. We could add a vararg-friendly variant for the arithmetic operators, but slowing down every addition on a function call result by a significant factor is not an acceptable fix.
I've added a compiler error for
(+ ...)
here: https://git.sr.ht/~technomancy/fennel/commit/313e9decf17d9af0e65323594898d3c5476769baThis will at least make it clearer that what is being attempted here will not work. The suggestion points people towards
accumulate
as an alternative. I think that's the best we can do.
We can continue the discussion if anyone thinks there's more we can do here, but I think for the time being this is not actionable.