Right now the case macro expands to the code that does repeated lookups:
(case [1 2 3]
[a b c] (print a b c)
_ (print :else))
;; macrodebug:
(let [(_1907_) [1 2 3]]
(if (and (= (_G.type _1907_) "table")
(not= nil (. _1907_ 1))
(not= nil (. _1907_ 2))
(not= nil (. _1907_ 3)))
(let [a (. _1907_ 1)
b (. _1907_ 2)
c (. _1907_ 3)]
(print a b c))
true
(let [_ _1907_]
(print "else"))))
This is both not safe and not efficient.
It's not safe, because lookup may call a custom __index
function, which may be not pure. Using case
in a tight loop is also less efficient than writing a similar code by hand.
I think case
or match
should capture lookups like this:
(let [(_1907_) [1 2 3]]
(if (= (_G.type _1907_) "table")
(let [_1908_ (. _1907_ 1)
_1909_ (. _1907_ 2)
_1910_ (. _1907_ 3)]
(if (and (not= nil _1908_)
(not= nil _1909_)
(not= nil _1910_))
(let [a (. _1907_ 1)
b (. _1907_ 2)
c (. _1907_ 3)]
(print a b c))
true
(print "else")))
true
(print "else")))
This, however, leads to code duplication of other branches. Another approach is to pre-declare right amount of vars in the scope of the macro (as many as there are the maximum bindings in any of the patterns), and set them in the if clause:
(case [1 2 3]
[a b c] (print a b c)
[a b] (print a b)
_ (print :else))
;; macrodebug
(let [(_1909_) [1 2 3]]
(var (_1910_ _1911_ _1912_) (values nil nil nil))
(if (and (= (_G.type _1909_) "table")
(do (set _1910_ (. _1909_ 1)) (not= nil _1910_))
(do (set _1911_ (. _1909_ 2)) (not= nil _1911_))
(do (set _1912_ (. _1909_ 3)) (not= nil _1912_)))
(let [a _1910_
b _1911_
c _1912_]
(print a b c))
(and (= (_G.type _1909_) "table")
(do (set _1910_ (. _1909_ 1)) (not= nil _1910_))
(do (set _1911_ (. _1909_ 2)) (not= nil _1911_)))
(let [a _1910_
b _1911_]
(print a b))
true
(let [_ _1909_]
(print "else"))))
This is less functional, but more robust than repeating bodies. The flip side is that it compiles to a lot of IIFEs:
local _1909_ = {1, 2, 3}
local _1910_, _1911_, _1912_ = nil, nil, nil
local function _1913_(...)
_1910_ = (_1909_)[1]
return (nil ~= _1910_)
end
local function _1914_(...)
_1911_ = (_1909_)[2]
return (nil ~= _1911_)
end
local function _1915_(...)
_1912_ = (_1909_)[3]
return (nil ~= _1912_)
end
if ((_G.type(_1909_) == "table") and _1913_(...) and _1914_(...) and _1915_(...)) then
local a9 = _1910_
local b8 = _1911_
local c = _1912_
return print(a9, b8, c)
else
local function _1916_(...)
_1910_ = (_1909_)[1]
return (nil ~= _1910_)
end
local function _1917_(...)
_1911_ = (_1909_)[2]
return (nil ~= _1911_)
end
if ((_G.type(_1909_) == "table") and _1916_(...) and _1917_(...)) then
local a9 = _1910_
local b8 = _1911_
return print(a9, b8)
elseif true then
local _ = _1909_
return print("else")
else
return nil
end
end
The second proposal, to pre-declare right amount of vars in the scope of the macro, no longer generates IIFE. I think that's probably the solution we should go for.
I agree this is a good idea! I probably won't do it myself, but I'd be happy to take a patch for it.