Using Scheme For Max

This page details all functionality offically released in the v0.2. The s4m help file includes example patches for most of what is here.

The s4m object

The s4m object is the core patcher object for Scheme For Max. It works fairly similarly to the js object, and can have a source file argument to load. The interpreter is thread safe, so we can have multiple objects in a patch, with each running its own isolated Scheme environments.

On creation of an s4m object, the interpreter first loads s4m.scm, which boostraps the S4M environment with any housekeeping that has been implemented in Scheme. This includes loading several other files, all of which must be present in the Max file path. If you have installed S4M from a package, this should all “just work”. If you’re getting errors about missing .scm files, you have likely installed the package incorrectly, have moved somethings, or have an error with your Max filepaths settings. Max must be able to find both the compiled s4m external and the scm files for bootstrapping.

One of the files loaded by s4m.scm is s74.scm. This holds pure Scheme functions that have nothing to do with Max, but augment S7 with convenience functions you may want to use. If you want to run pure S7, you can comment out this inclusion. It’s mostly functions that are common in more batteries-included Schemes, such as Racket or Clojure.

You can change the s4m.scm file if you want to change bootstrap behaviour across all your s4m objects as well, such as to always load a personal library of Scheme definitions.

Arguments & Attributes

{filename.scm}

An (optional) source filename as the first argument in the s4m object box will be loaded automatically (after the bootstrap files) searching the Max file search path. Changing this file in the box will always wipe the Scheme environment and reloads the file, as this recreates the s4m object. Reset messages will also reload this file.

@ins {num}

Sets the number of inlets to num. This can only be set in the object box. Changing will reset the interpreter as it recreates the object.

@outs {num)

Sets the number of outlets to num. This can only be set in the object box. Changing will reset the interpreter as it recreates the object.

@thread { h | l | a }

Sets the thread in which Scheme code is executed. Setting to h will ensure all messages are promoted to the high-priority scheduler thread, while l defers all inputs to the low priority main thread. Setting to a (any) allows execution in whichever thread the incoming message is in, with no promotion or deferal. (a is meant for testing/debugging and is unstable.) This attribute can only be set in the object box. Changing will reset the interpreter as it recreates the object.

@log-repl {1 | 0}

Sets whether the interpreter prints the output from the REPL to the Max console. When log-repl is enabled, any Scheme expression evaluated will have its return value printed. This can be changed anytime in both the inspector and by sending a log-repl 1 message.

@log-null {1 | 0}

Sets whether the interpreter prints the output from the REPL to the Max console when the output is the null list in Scheme, returned by many Scheme functions that are called for their side effect. It is often useful to turn this off when running scheduled Scheme functions on a timer, for example. This can be changed anytime in both the inspector and by sending a log-null 1 message.

@heap {integer in KB}

Sets the initial heap-size for s7. The default is 64KB, and lowest possible is 8KB (set with @heap 8). The heap-size changes how fast the gc runs - it takes longer to run over a large heap. The interpreter will resize the heap as needed, but this does introduce some latency every time it’s done, so you may find performance increases if you set this to a value close to what you will need. To watch the gc-stats, run (set! (*s7* ‘gc-stats) #t), and you’ll see information on the heap-size and the gc runs in the console.

When a source file has been listed in the object box, double clicking the object will display the full path to the file in the console. We can reboot the interpreter at any time with the reset message, which will also reload this file.

Reserved Words and Naming Conventions

In Max, messages to an object that consist of the name of an attribute and a value are (typically) executed automatically to set that attribute, if the object permits it. This means that we can’t have Scheme functions with names that collide with attributes of s4m if we want these functions to be callable as automatically evaluated s-expressions on inlet 0 - the message will never get to the Scheme interpreter. A further complication is that in Max, the messages int, float, symbol, and list are implicit - if you send an object the message int 4 or list a b c, they are treated by the receiving object as being the exact same as the message 4 or a b c.

In addition, S4M has some internal private functions used where the public API function calls a private helper implemented in C. These begin with s4m-.

The easiest solution is to avoid using the following as names in Scheme unless you know exactly why you are doing it:

Max reserved words: int, float, symbol, get, set

S4M attributes: thread, ins, outs, log-repl, log-null, heap-size

S4M commands: reset, scan, read, eval-string

S4M internal functions: anything starting with s4m-

The astute reader will have noticed that list is missing above. This is because list is already a Scheme function. This is also why our handlers for bang, int, float, and list are called f-bang, f-int, f-float, and f-list - to avoid a weird inconsistency as we can’t call the Scheme list hander list.

S4M does not yet use the get and set message for anything special, but it’s common in Max to use these to set state of an object, so consider them reserved for future use. Fortunately for us, in Scheme the set command is set!, so we can happily have set foo bar messages implemented in the object (but not in Scheme) in future without collision.

With regard to naming conventions, Scheme variables and functions can accept special characters and normally separate words with hyphens. Reader functions are usually (foobar-ref {key}) and setter functions are usually (foobar-set! {key} {value}). We are following Scheme and Max naming conventions where possible, with Scheme as the priority when in doubt. Predicates end in ? and functions with side effects end in !, so we have for example: dict-ref, dict-set!, and dict?. Some people also use the convention of ear-muffs for globals. The special globals *s7* and *s4m* follow this.

S4M also includes short form aliases for most function names to simplify live coding and building s-expressions in Max messages where space may be at a premium. These are 4 to 6 character abbreviations and do not follow Scheme conventions (e.g. tabr, tabs, etc.) Aliases do not create an extra stack frame; they are registered at the C level so there is no speed penatly in using them.

There are various functions defined in the bootstrap files. If you want to know whether your function name is going to shadow anything already defined, just evaluate it in the REPL and see if you get anything. You should get an ‘unbound variable’ error in the console if there is no naming collision.

Messages to inlet 0

Messages to inlet 0 are either handled directly by the s4m object as a Max message, or evaluated as Scheme code if no direct handling exists. If the incoming message starts with a ( character, S4M assumes this is code and evaluates it. If it doesn’t, there are two possibilities.

If the message is not handled by the s4m object as a Max message, the interpreter will essentially pretend the message is enclosed in parentheses, evaluating the message as a Scheme s-expression to be called, with any symbols evaluating to their value in the Scheme environment. For example, the message foobar 1 2 3 will be evaluated in Scheme as (foobar 1 2 3). This also means that the message out 0 a will only execute properly if the variable a has been defined. To have a treated as a Max symbol, not a Scheme variable, we need to quote it in the standard Lisp fashion. For example, out 0 ‘a will send the symbol “a” out output 0, having been evaluated as (out 0 ‘a) in Scheme.

The messages reset, read, scan, and eval-string are handled directly by the s4m object, without passing to the interpreter (see Message reference below).

To evaluate a string as a code (for example, if received over the network from a udpreceive object), we prepend the message by using a prepend eval-string object. This will result in s4m receiving something like eval-string “(define a 99)”, which will execute as (eval-string “(define a 99)”) in the Scheme interpreter.

Practically speaking, there is no difference in execution between sending inlet 0 of an s4m object either define a 99, (define a 99), or eval-string “(define a 99)”.

Messages reference for inlet 0:

reset

Resets the Scheme intepreter, wiping all active definitions, and reloading any sourcefile specified in the s4m object box itself. Also scans the current patcher for scripting names (see entry for scan).

read {filename}

Loads the file {filename} from the Max search path. Executes the Scheme load function internally, after finding the full filepath by searching the Max filepaths. Loading does not erase any already active definitions unless they are redefined. Rereading a file will redefined any definitions.

scan

Scans the current patcher and all descendents for objects with scripting names, adding them to an internal registry so that they can receive messages with the send scheme function.

{bang | int | float | list}

Evaluates the function f-bang, f-int, f-float, or f-list, with the respective max atom(s) as argument(s). I.E. the int 4 sent to inlet 0 will executes in Scheme as (f-int 4). If no f-type function defined, prints an error message to console. Return value of evaluation is printed to the Max console.

eval-string {string}

Evaluates {string} in scheme. Return value of evaluation is printed to the Max console.

{symbol …}

Evalues the symbol or list of symbols as a Scheme s-expression. If sent the list my-fun 1 2 3, scheme will evaluate (my-fun 1 2 3). Any symbols will be evaluated as Scheme variables unless quoted. For example, on my-fun 1 a, there will be an error if a has not been defined. Return value of evaluation is printed to the Max console. If the first symbol is not callable as a Scheme procedure, will produce an error (as if a symbol is evaluated surrounded with parentheses).

Messages to inlet 1+

Messages to inlet 1+ are treated as plain Max values or lists of Max atoms; they are not evaluated as Scheme code. Symbols will become quoted symbols in scheme, with no variable evaluation. As these are not evaluated as code, the messages may begin with any type. Keywords are useful here as they indicate visually that the message is not a function call, becauses keywords can not be used as function names in Scheme. Thus (:foobar 1 2) is always an error in Scheme. This means :foobar 1 2 will always be an invalid message to inlet 0, but may be valid in inlet 1+ depending on what we have defined. It won’t do anything unless we have registered a listener function on the inlet receiving the message, with the keyword :foobar. See Registering Listeners below. (If all this is confusing, skip it for now - you could use s4m productively without ever using inlets 1+)

Messages reference for inlet 0:

{bang | int | float | list}

Evaluates Scheme function (s4m-dispatch {inlet} {:bang | :int | :float | :list} {arg}), which will call registered listener functions, with inlet as arg 1, and data as arg 2. For example, the message 4 on inlet 1 will call (s4m-dispatch 1 :int 4), which will in turn call a listener function if a matching listener is registered. If no listener function is registered for the inlet used and the associated keyword (:int, :bang, etc), this will produce an error. See Registering Listeners.

{symbol …}

Evaluates scheme function (s4m-dispatch {inlet} {symbol} arg), dispatching to listener functions registered with the symbol. Any arguments after the first symbol will be bound up in a list passed as arg, which may be empty. For example, the message :foobar 1 2 3 on inlet 2 will call (s4m-dispatch 2 :foobar arg), where arg is the Scheme value (list 1 2 3). This will in turn call a listener function if registered. If no listener function is registered for the inlet used and the associated symbol, this will produce an error. See Registering Listeners for more details.

Registering Listeners for Max messages

Inlet 0

For inlet 0 to respond to bang, int, float, or list, we define functions named as below:

 ;; respond to bang messages by logging to console and sending bang out
 (define (f-bang)
    (post "f-bang got the bang!")
    (out 0 'bang))

 ;; respond to int messages by logging to console and sending int + 1
 (define (f-int num)
    ;; log and output num + 1
    (post "f-int got the int: ", num)
    (out 0 (+ 1 num)))

;; respond to float messages by sending out arg * 0.5
 (define (f-float num)
    (post "f-float got the float: ", num)
    (out 0 (* 0.5 num)))

 ;; respond to lists by sending out in reverse the list elements as sequential messages
 (define (f-list list-arg)
    (map out-0 (reverse list-arg)))

Note that the f-list function will not respond to lists starting with a symbol. Max doesn’t consider those to be list messages, they are the message of the first symbol.

Any message starting with a symbol that is not alread reserved by S4M will be called as a Scheme function.

;; responds to max message "foobar 99" by outputing 99
;; if sent max message "foobar my-var", will output the value of variable my-var
;; if sent max message "foobar 1 2 3", will be "too many arguments" error
(define (foobar value)
   (post "foobar exectuting, value:" value)
   (out 0 value))

;; responds to max messages "foobaz ..." with any number of args
;; . args bundles variable list of optional args into a list
(define (foobaz . args)
   ;; log and output num + 1
   (post "foobaz executing, num args: " (length args))
   ;; output the first arg if there is one, or null list if not
   (cond
      ((> (length args) 0) (out 0 (args 0)))
      (else (post "no arg"))))

Note that in the above example we need to explictly check length args is > 0, because in Scheme anything except #false is #true - there is no automatic cast from 0 to #f.

Inlet 1+

For inlet 1+, we need to explictly register listener functions. The listener functions registered with listen should always take one argument, expecting it to be a list that may be of length zero. This allows the s4m-dispatch to be generic.

(listen {inlet} {symbol} {function})

Register the function to listen on inlet {inlet} for messages starting with {symbol}. Listeners are called by s4m’s s4m-dispatch function, which will dispatch calls with the keyword symbols :bang, :int, :float, and :list for non symbolic messages.

;; define a listener for bangs, note that it takes an arg of a list
;; even though this will in practise be empty on bang messages
(define (my-bang-func args)
    (post "got the bang!"))
;; register it to listen for bangs on inlet 1
(listen 1 :bang my-bang-func)

;; define a listener for int messages, using an anonymous function
(listen 1 :int (lambda (args)
   (out 1 (args 0))))

;; the same function can listen for multiple messages
(define (num-listener args)
   (out 0 (+ 1 (args 0)))
(listen 1 :int num-listener)
(listen 1 :float num-listener)

;; a listener using a let to hide the signature weirdness
(define (my-listener args)
   (let ((num (args 0)))
      (post "num is:" num)
      (out 0 (+ 1 num))))

Listeners are stored internally in the s4m-listeners registry, a nested hashtable of {inlet} {symbol}. To remove a listener, you can put an empty function in:

;; remove the listener by registering false
(listen 1 :int #f)

Note that if you redefine a named listener function, it will not change what happens on the listened-to message until you re-register it, by virtue of how Scheme functions work. (We are registering the actual function, not the symbol of the function!)

s4m.repl patcher

The s4m.repl object is intended to be put in a bpatcher and then hooked up. The left outlet sends the output as a single text symbol. To evaluate as Scheme, we send it to a prepend eval-string object and send to inlet 0. This makes it the equivalent of:

(eval-string "(define a 99)")

The right outlet sends out a bang on each output to let you know it went out.

The s4m.repl patcher wraps the textedit box, which has some quirks/bugs. It wants to send out a bang when one bangs or hits enter in an empty box. In order to prevent Scheme error messages on this instance, s4m.repl filters these out.

If you select Control Keys on it, the s4m.repl object is listening to all key strokes, no matter where your focus is. So if you use this feature, be sure to turn it off when you’re done. This can be especially confusing if you have multiple REPLs in different Max windows!

Future plans include making a proper terminal GUI object with history.