~technomancy/fennel#168: 
Avoid repeated lookups in case/match

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
Status
REPORTED
Submitter
~andreyorst
Assigned to
No-one
Submitted
1 year, 10 months ago
Updated
7 months ago
Labels
enhancement macros

~xerool 7 months ago

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.

~technomancy 7 months ago

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.

Register here or Log in to comment, or comment via email.