'''[ycl%|%knit]''', by [PYK], is another [macro] facility for Tcl ** Synopsis ** : '''`knit`''' ''name arguments body'' : '''`knead`''' ''arguments body'' ** Download ** The full implementation is below. `knit` is also available as `[ycl%|%ycl::knit::knit]`, along with unit tests. ** Description ** '''`knit`''' uses `[tailcall]` to provide some of the features that were problematic in earlier macro systems. Each macro is a [proc%|%procedure] that fills out a template according to the arguments it receives, and then `[tailcall%|%tailcalls]` the template. `knit` takes an [EIAS] approach to macros, meaning that it does not try to discern the structure of the template it is filling in, and instead focuses on providing the necessary substitutions that allow a macro author to choose how subtitutions are made. It turns out that just a small number answer most needs. The following three template substitutions are provided: `${var}`: For substituting in a value. The value of `$var` is substituted, properly escaped as a list `#{var}`: For substituting in a script fragment. The value of `$var` is substituted without escaping the value as a list. This is useful for example, to substitute a fragment of an expression into `[expr]`, or to substitute a few lines of code into a routine. Also useful for substituting in a command prefix. `!{varname}`: this is simply for convenience, and is exactly equivalent to `[[set ${varname}]]`. In other words, the value `${varname}` is the name of a variable, and it will be arranged for the value of that variable to be substituted at execution time. In the examples below, `lpop2` does the same thing as `lpop`, but thanks to `!{varname}` is a little more concise. '''`knead`''' can be used to build a macro procedure specification without actually creating the macro procedure. `knit` is implemented as a trivial wrapper around `knead`. `knead` itself is useful for creating '''anonymous macros''': ====== apply [knead x {expr {${x} * ${x}}}] 5 ====== In contrast with [Sugar], `knit` is more interleaved with the running interpreter, as [Lisp] macros are. Where [Sugar] attemps to parse a script and discern macros, `knit` inserts the macro code at runtime when the macro procedure is invoked. In order to do its expansions, [Sugar] must know, for example, that the first argument to `[while]` is evaluated as an [expr%|%expression]. `knit` is oblivious to such things, allowing it to fit more naturally into a Tcl script. Since knit macros are themselves procedures, `knit` eschews the issue that `[{*}]` raises for [Sugar], and in general automatically has the features of a procedure that the merely-procedure-like macros in Sugar have to work hard for. One example is default arguments and another is `$argv` handling. The tradeoff is that `knit` incurs some cost during runtime that Sugar does not, namely the cost of the `[tailcall]`. ** Implementation ** ====== proc knit {name varnames body} { set cmdspec [uplevel [ list [namespace current]::knead $varnames $body]] uplevel [list proc $name {*}$cmdspec] } proc knead {varnames body} { variable knittemplate set mapparts {} foreach varname $varnames { if {[llength $varname] > 1} { set varname [lindex $varname 0] } lappend mapparts [string map [list @varname@ [list $varname]] { \ \${@varname@} "\[lindex [list [set @varname@]]]" \ #{@varname@} [set @varname@] \ !{@varname@} "\[set [set @varname@]]" \ }] } set map \[list\ [join $mapparts { }]] set body [string map [list @map@ $map @template@ [ list $body]] $knittemplate] list $varnames $body } variable knittemplate { set body [string map @map@ @template@] tailcall try $body } ====== ** Examples ** These examples show how the macros presented in [Sugar], along with various other macros are implemented in `knit` ====== knit double x {expr {${x} * 2}} knit exp2 x {* ${x} * ${x}} knit charcount {x {char { }}} { regexp -all ***=${char} ${x} } knit clear arg1 {unset ${arg1}} knit first list {lindex ${list} 0} knit rest list {lrange ${list} 1 end} knit last list {lindex ${list} end} knit drop list {lrange ${list} 0 end-1} knit K {x y} { first [list ${x} ${y}] } knit yank varname { K [set ${varname}] [set ${varname} {}] } knit lremove {varname idx} { set ${varname} [lreplace [yank ${varname}] ${idx} ${idx}] } knit lpop listname { K [lindex [set ${listname}] end] [lremove ${listname} end] } knit lpop2 listname { K [lindex !{listname} end] [lremove ${listname} end] } foreach cmdname {* + - /} { knit $cmdname args " expr \[join \${args} [list $cmdname]] " } knit sete {varname exp} { set ${varname} [expr {#{exp}}] } knit greeting? x {expr {${x} in {hello hi}}} knit until {expr body} { while {!(#{expr})} ${body} } knit ?: {cond val1 val2} { if {#{cond}} {lindex ${val1}} else {lindex ${val2}} } knit finally {init finally do} { #{init} try ${do} finally ${finally} } ====== ** Example: Avoid a Conditional Branch in a Loop ** Sometimes only one or two steps of a routine branch based on some condition. It can be annoying when one of those steps is in a loop, and the condition must be tested on each iteration even though the values affecting the outcome are known prior to entering the loop: ====== proc files {arg1 arg2 arg3} { ## step 1 #step 2 if {$arg1} { #do something } else { #do something else } foreach file $files { #step 3 #step 4 if {$arg1} { #do something else } else { #do something else } #step 5 } #step 6 #return some result } ====== In this situation, `knit` could be used like this: ====== proc files {arg1 arg2 arg3} { ## step 1 if {some condition} { #step 2 } else { #alternate step 2 } if {some condition} files_for { #the script for step 4 } else { files_for { #the alternate for step 4 } } #step 6 return files } knit files_for script { foreach file $files { #step 3 #step 4 is a macro #{script} #step 5 } } ====== Or, if the conditions can be determined from just the parameters to the procedure, multiple variants of the procedure can be generated from a template, and the selector moved to the caller: ====== knit files_macro {name script1 script2} { proc ${name} {arg1 arg2 arg3} { ## step 1 #step 2 is a macro #{script1} foreach file $files { #step 3 #step 4 is a macro #{script2} #step 5 } #step 6 return files } } files_macro files1 { #script1 } { #script2 } files_macro files2 { #script1 } { #script2 } if {$arg1} { files1 $arg1 $arg2 $arg3 } else { files2 $arg1 $arg2 $arg3 } ====== ** See Also ** [http://paste.tclers.tk/3306%|%macro tests from] [Peter Spjuth]: <> macro