Ticket created by ~ioiojo on ~technomancy/fennel
This is a tracking issue for a more sophisticated method of loading Fennel macros at compile time.
One possible use for this is to expose Fennel macros to end users of an application which embeds Fennel modules, including user-facing macros. As of today, this can only be done by hand via running
fennel.eval
with compile scope, e.g.(local fennel (require :fennel)) (local compiler (require :fennel.compiler)) (local str (with-open [file (io.open :path/to/macros.fnl :r)] (file:read :a))) (local opts {:scope compiler.scopes.compiler}) (fennel.eval str opts)
Comment by ~ioiojo on ~technomancy/fennel
(match [:x :y :z] ([:x :y z &as xyz] ? (> z 1)) xyz)(match [:x :y :z] ((@as xyz [:x :y z]) ? (> z 1)) xyz)(match [:x :y :z] ([:x :y z] @as xyz ? (> z 1)) xyz)(match [:x :y :z] ([:x :y z] @alias xyz ? (> z 1)) xyz)I've noticed all the proposed approaches do some amount of mental overloading. Compared to prefix
@as
, the intra-table&as
avoids extra parens which require visual unwrapping. Compared to postfix@alias
or@as
, intra-table&as
doesn't risk stacking extra syntax (@as xyz ? expr
).After much internal debate, I'm joining the
&as
camp. Although I love Elixir's operator-based approach, intra-table&as
may be the best way to express whole-table destructuring in a lisp family language.
Comment by ~ioiojo on ~technomancy/fennel
it forces the mind to break out of the context of what it's reading
Very salient point. And well said.
(let [[a b c &as whole-vec] [1 2 3]] whole-vec)In this example, it does bother me how the
whole-vec
binding applies one level up froma
,b
andc
. This forces a context switch. A subtle context switch, perhaps, but a context switch nonetheless.Personally, I like how Elixir does whole-table destructuring:
def hi({:a, a} = yo), do: yoElixir's approach is what attracted me to the idea of making the whole-table destructure an operator, e.g. from:
def hi({:a, a} = yo), do: yoto
def hi(yo = {:a, a}), do: yoto
(fn hi [= yo {:a a}] yo)to
(fn hi [(@as yo {:a a})] yo)(or pick some other operator)
(fn hi [(:: yo {:a a})] yo)Personally I like a pure operator sans alphanumeric chars here to minimize mental load.
Comment by ~ioiojo on ~technomancy/fennel
This is the only downside I can see. Compared with the downsides of the other proposals so far, it seems like a small price to pay.
I've only written a modest amount of Fennel, but I did not ever think to use
&
— or&as
, nor anything else prefixed with&
— as an identifier until just now.I also prefer ~andreyorst's proposed syntax
(let [[a b c & rest &as whole-vec] [1 2 3 4 5]]) {:a a :b b &as whole}to
(let [@as whole-table {: key1 : key2} (foo)] ...)I especially like how
&as
complements the&
operator.
Comment by ~ioiojo on ~technomancy/fennel
Very much agreed on the lisp syntax being a good thing here. I saw this crop up in my code and just had to wonder — but curiousity satisfied. I enjoy the simplicity of Lua combined with homoiconicity. Fennel for great good. Thanks for taking the time.
Comment by ~ioiojo on ~technomancy/fennel
The other thing is that we are probably going to need to introduce new syntax for reader macros anyway for other purposes, so if we can piggy-back on that, we only need to introduce one new syntactic construct.
Ah, k.
The main downside here is that whole-table is not as "visible" in the code; it's a constituent of a larger symbol. Right now all locals that are introduced are visible as standalone symbols in the binding form that introduces them; this would be an exception to that rule.
Would you be so kind as to provide psuedo-code which demonstrates what you mean when you say the whole-table destructure binding wouldn't be as visible in the code?
Ticket created by ~ioiojo on ~technomancy/fennel
(local foo {}) (fn foo.concat [...] (let [args [...] res []] (each [_ arg (ipairs args)] (table.insert res arg)) res)) (fn foo.new [self init] (setmetatable {:x init} {:__index self :__concat foo.concat})) (let [a (foo:new 3) b (foo:new 6) c (foo:new 9) d (foo:new 1)] (print (fennelview (foo.concat a b c d))) (print (fennelview (.. a b c d)))) ;; foo.concat → [{ :x 3 } { :x 6 } { :x 9 } { :x 1 }] ;; .. → [{ :x 3 } [{ :x 6 } [{ :x 9 } { :x 1 }]]]I suspect this discrepancy in output has to do with Lua metamethods having infix notation, e.g.
(.. a b c d)
effectively transpiles to(((a .. b) .. c) .. d)
. IMO it would feel more lispy if the(foo.concat a b c d)
and(.. a b c d)
produced identical results. Wasn't exactly sure how to go about munging through Fennel's source tree for answers. And figured maybe it's best to assume metamethod overrides can only take two args. Can (should?) something be done about this?
Comment by ~ioiojo on ~technomancy/fennel
I like the idea of
@as
and the reader macro. Assuming I'm understanding it right,whole-table
is the variable name and is arbitrary, e.g.(let [@as/bar{: key1 : key2} (foo)] (. bar key1))That seems good enough to me syntax wise.
Implementation wise, however, if it is a kludge on the language as you say, then IDK. I couldn't comment on the tradeoffs. Could making
::
a reserved symbol be less intrusive on the language than a reader macro?
Comment by ~ioiojo on ~technomancy/fennel
That's a cool approach. I like it. And certainly special casing that
not= :: ":"
comes with purity tradeoffs. JW but is it feasible for the@as
to go outside the table?(fn abc [({:abc abc} @as full-table)] ...)Thought I'd throw in moar parens for great good ... heh.
Fun detour: I got the idea for
::
from Raku.# unpunned hash entry notation my %h = a => 10, b => 20; # punned my %h = :a(10), :b(20); # illegal punning my %h = ::(10);Raku limits what can be punned, e.g.
:a(1)
=="a" => 1
, but::
is reserved for object attribute access.
REPORTED
RESOLVED FIXED