hylang / hyrule

A utility library for Hy
MIT License
44 stars 9 forks source link

time-it macro #39

Open pyx opened 7 years ago

pyx commented 7 years ago

As inspired by @gilch in discussion here: https://github.com/hylang/hy/pull/1179

The macro:

(defmacro/g! time-it [expr &optional setup round]
  `((fn []
      (do (import [timeit [timeit :as ~g!timeit]])
      ~(when setup setup)
      (defn testee [] ~expr)
      (setv kwargs {})
      ~(when round `(assoc kwargs "number" ~round))
      (apply ~g!timeit [testee] kwargs)))))
hy 0.11.0+353.gca6fd66 using CPython(default) 3.5.2 on Linux
=> (defmacro/g! time-it [expr &optional setup round]
...   `((fn []
...       (do (import [timeit [timeit :as ~g!timeit]])
...       ~(when setup setup)
...       (defn testee [] ~expr)
...       (setv kwargs {})
...       ~(when round `(assoc kwargs "number" ~round))
...       (apply ~g!timeit [testee] kwargs)))))
=> (time-it (inc 1))
0.6166748020004889
=> (time-it (inc anwser))
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 1, in _hy_anon_fn_2
  File "/usr/lib64/python3.5/timeit.py", line 213, in timeit
    return Timer(stmt, setup, timer, globals).timeit(number)
  File "/usr/lib64/python3.5/timeit.py", line 178, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
  File "<input>", line 1, in testee
NameError: name 'anwser' is not defined
=> (time-it (inc answer) (do (print "thinking.." (setv answer 42))))
thinking.. 42
0.641465346001496
=> (time-it (inc a) (setv a 12) 100)
6.975599899305962e-05
=> (time-it (inc 1) () 100)
6.572400161530823e-05
=> (time-it (inc 1) :round 100)  ;; works by accident, :round by itself is a valid expression
6.490799933089875e-05

Another approach is using read-str

Caveat

I've been fighting the macro system for the last hour, as we cannot use &kwargs with defmacro, so unless we use &rest and parse the argument ourselves (I have a kind-of-working prototype, but it is so ugly that I do not dare to share), the form of the macro is a bit rigid, I skipped the timer argument for simplicity's sake, and right now, one have to pass in the setup code to specify the round of iteration, see above.

gilch commented 7 years ago

For maximum flexibility, a macro has to be able to interpret a keyword as as keyword instead of a kwarg pair. This way you can write macros that contain keywords as part of the syntax, even if there are two in a row or one at the end. I think you could simply have the macro call a function that accepts &kwargs. No need to parse it yourself.

pyx commented 7 years ago

Revise, remove excessive do

(defmacro/g! time-it [expr &optional setup round]
  `((fn []
      (import [timeit [timeit :as ~g!timeit]])
      ~(when setup setup)
      (defn testee [] ~expr)
      (setv kwargs {})
      ~(when round `(assoc kwargs "number" ~round))
      (apply ~g!timeit [testee] kwargs))))

In the beginning, I did not wrap everything inside a function, thus the do form, when I switched to function (to have local scope), I forgot to remove the do

pyx commented 7 years ago

@gilch Ah, that's a nice idea, a helper function will do, indeed. Thanks. I will come back to this when I have time, I need some fresh air now.

gilch commented 7 years ago

A simple example:

=> (defmacro foo [x &rest xs] `((fn [s &kwargs kws] (, s kws)) '~x ~@xs))
=> (foo norf :bar 1 :baz 2)
('norf', {'bar': 1, 'baz': 2})

Note that the macro can quote the first symbol and still parse the remaining kwargs.

pyx commented 7 years ago

Okay, this is what I have so far

(defmacro/g! time-it [expr &rest options]
  (if
    (not (keyword? (first options)))
      (setv setup (first options) args (rest options))
    (in :setup options)
      (do
        (setv index (.index options (keyword "setup")))
        (when (= index (dec (len options)))
          (macro-error None "Keyword argument :setup needs a value."))
        (setv skipped (, index (inc index))
              setup (get options (inc index))
              args (list-comp exp
                              [(, i exp) (enumerate options)]
                              (not-in i skipped))))
    (setv setup None args options))
  `((fn []
      (import [timeit [timeit :as ~g!timeit]])
      ~(when setup setup)
      (~g!timeit (fn [] ~expr) "pass" ~@args))))

Test drive:

hy 0.11.0+353.gca6fd66 using CPython(default) 3.5.2 on Linux
=> (defmacro/g! time-it [expr &rest options]
...   (if
...     (not (keyword? (first options)))
...       (setv setup (first options) args (rest options))
...     (in :setup options)
...       (do
...         (setv index (.index options (keyword "setup")))
...         (when (= index (dec (len options)))
...           (macro-error None "Keyword argument :setup needs a value."))
...         (setv skipped (, index (inc index))
...               setup (get options (inc index))
...               args (list-comp exp
...                               [(, i exp) (enumerate options)]
...                               (not-in i skipped))))
...     (setv setup None args options))
...   `((fn []
...       (import [timeit [timeit :as ~g!timeit]])
...       ~(when setup setup)
...       (~g!timeit (fn [] ~expr) "pass" ~@args))))

=> (time-it (inc 1))
0.6054470939998282

=> (time-it (inc answer))
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 1, in _hy_anon_fn_2
  File "/usr/lib64/python3.5/timeit.py", line 213, in timeit
    return Timer(stmt, setup, timer, globals).timeit(number)
  File "/usr/lib64/python3.5/timeit.py", line 178, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 6, in inner
  File "<input>", line 1, in _hy_anon_fn_1
NameError: name 'answer' is not defined

=> (time-it (inc answer) (setv answer 42))
0.6117291800001112

=> answer
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'answer' is not defined

=> (time-it (print 'Hit) :number 3)
Hit
Hit
Hit
0.00010172399925068021

=> (time-it (print 'Hit) () :number 3)
Hit
Hit
Hit
9.308399967267178e-05

=> (time-it (print 'Hit) :number 3 :setup (print 'Setup...))
Setup...
Hit
Hit
Hit
7.982400347827934e-05

=> (time-it (print 'Hit) :setup (print 'Setup...) :number 3)
Setup...
Hit
Hit
Hit
8.413199975620955e-05

=> (time-it (inc 1) :setup (print 'Setup...))
Setup...
0.5945940750025329

=> (time-it (inc 1) :setup)
  File "<input>", line 1, column 1

  (time-it (inc 1) :setup)
  ^----------------------^
HyMacroExpansionError: b'Keyword argument :setup needs a value.'

=> (time-it (inc 1) :i-dont-know-what-i-am-doing 42)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 1, in _hy_anon_fn_2
TypeError: timeit() got an unexpected keyword argument 'i_dont_know_what_i_am_doing'