common-lisp

How to create timers in a doplist loop?


I'm trying to write a timeline generator that takes a plist with times and functions to execute:

(defun run-timeline (timeline)
  "Timeline is a plist with alternating timecodes and code to execute"
  (alexandria:doplist (time form timeline)
    (sb-ext:schedule-timer
     (sb-ext:make-timer form
                        :thread t)
     time)))

(run-timeline '(0 (print "first")
                    2 (print "second")))

This, however, gives me this error:

The value
  (PRINT "first")
is not of type
  (OR FUNCTION SYMBOL)
   [Condition of type TYPE-ERROR]

Restarts:
 0: [USE-VALUE] Use specified value.
 1: [ABORT] abort thread (#<THREAD tid=203285 "Timer NIL" RUNNING {1001179523}>)

Backtrace:
 0: (SB-VM::CALL-SYMBOL)
 1: ((FLET SB-THREAD::WITH-RECURSIVE-LOCK-THUNK :IN SB-IMPL::MAKE-CANCELLABLE-INTERRUPTOR))
 2: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::CALL-WITH-RECURSIVE-LOCK))
 3: ((FLET "WITHOUT-INTERRUPTS-BODY-1" :IN SB-IMPL::MAKE-CANCELLABLE-INTERRUPTOR))
 4: ((LAMBDA NIL :IN SB-IMPL::MAKE-CANCELLABLE-INTERRUPTOR))
 5: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
 6: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::RUN))
 7: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
 8: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::RUN))
 9: (SB-THREAD::RUN)
10: ("foreign function: call_into_lisp_")

When hard-coding a lambda expression I get no errors, and it works as expected:

(defun run-timeline (timeline)
  "Timeline is a plist with alternating timecodes and code to execute"
  (alexandria:doplist (time form timeline)
    (sb-ext:schedule-timer
     (sb-ext:make-timer (lambda ()
                          (print "hi")
                          (force-output))
                        :thread t)
     time)))

What am I missing here?


Solution

  • The sb-ext:make-timer function expects a function designator, which is either a symbol (see FBOUNDP) or the result of evaluating a (function ...) form (see FUNCTION). The notation #'xyz for example is a shortcut for (function xyz) and refers to the function value currently associated with symbol xyz. Likewise, (lambda () (print :ok)) is a shortcut to (function (lambda () (print :ok))) which evaluates to an anonymous function (or closure).

    In your case, make-timer is given a list, namely (print "first"). You have to make sure that you transform that list to a function, or you may want to change the values that your property list holds. This one is simpler, and for example you can call your current function with:

    (run-timeline (list 0 (lambda () (print "first"))
                        2 (lambda () (print "second"))))
    

    Note that I dropped the quote character, because you need to evaluate the forms to have a function.

    Alternatively, you can keep this syntax:

    (run-timeline '(0 (print "first")
                    2 (print "second")))
    

    But you have to change the implementation so that you can transform any form into a function. For example, you can write the following to produce a form that can be evaluated to a function:

    (eval `(lambda () ,form))
    

    The comma injects the current value of form, for example (print "first"), into the surrounding quasi-quoted form, to obtain the list (lambda () (print "first")). The surrounding call to eval transform this into an actual function object (by compiling it, for example, but this is not necessary).

    There is another way of transforming a lambda form to a function which is as follows (see COERCE):

    (coerce `(lambda () ,form) 'function)