Macros vs procedures

I’ve noticed that macros have been deprecated. I know they won’t be going away any time soon, but I thought - as a learning exercise - I’d convert a macro of mine to a procedure. Sounds simple, but I’ve spent probably hours now unsuccessfully trying to get it to work as smoothly as the macro.

My macro has several parameters, and depending on what I’m doing I’ll either pass in a string to be output as-is, or I need to set a variable to the result of a filter and pass that in. For my use case at least, procedures appear to be a retrograde step.

I have created a simplified test case below:

\define dtest(arg) $arg$

\procedure ptest1(arg) <<arg>>
\procedure ptest2() <<arg>>

* Macro:
** <<dtest pass>>
** <$let test={{{[[]addsuffix[ss]addprefix[pa]]}}}><<dtest $(test)$>></$let>

* Procedure1 (with parameter, called same as macro)
** <<ptest1 pass>>
** <$let test={{{[[]addsuffix[ss]addprefix[pa]]}}}><<ptest1 $(test)$>></$let>

* Procedure1 (with parameter, variable named as parameter)
** <$let arg=pass><<ptest1>></$let>
** <$let arg={{{ [[]addsuffix[ss]addprefix[pa]] }}}><<ptest1>></$let>

* Procedure2 (no parameter, called same as macro)
** <<ptest2 pass>>
** <$let arg={{{[[]addsuffix[ss]addprefix[pa]]}}}><<ptest2>></$let>

* Procedure2 (no parameter, calls adjusted to work)
** <$let arg=pass><<ptest2>></$let>
** <$let arg={{{[[]addsuffix[ss]addprefix[pa]]}}}><<ptest2>></$let>

Which renders as:

  • Macro:
    • pass
    • pass
  • Procedure1 (with parameter, called same as macro)
    • pass
    • $(test)$
  • Procedure1 (with parameter, variable named as parameter)
  • Procedure2 (no parameter, called same as macro)
    • pass
  • Procedure2 (no parameter, calls adjusted to work)
    • pass
    • pass

The $let syntax isn’t too bad, but that implies to me that parameters to procedures are of no use to me - I’m better off just setting variables as needed prior to using the procedure.

After all that, the end result appears to be extra typing for no benefit, meaning I’m better off forgetting procedures and sticking with macros.

Am I doing procedures wrong? Is there something I’ve missed?

Addendum: I was just about to hit “Post”, when I made the mistake of trying out the last example of $let+procedure with my actual macro and absolutely everything broke! Something is up with quotes inside the procedure. I’ll have to spend another hour or so experimenting and creating a new example. I am finding “\procedure” completely frustrating, while “\define” “just works”.

This is certainly an option. But IMO, if you need a non-literal parameter value for a procedure, it’s easiest to set it inline with the $transclude widget. For instance, rather than the following, which fails because $(test)$ isn’t substituted in this context…

<$let test={{{ [[]addsuffix[ss]addprefix[pa]] }}}><<ptest1 $(test)$>></$let>

I’d instead try the following:

<$transclude $variable=ptest1 arg={{{ [[]addsuffix[ss]addprefix[pa]] }}} />

This is obviously longer than a short-form macrocall like <<ptest1>> — but on the other hand, it’s slightly shorter than the $let + macrocall combination. So if you have multiple procedures (or macros, or functions) all using the same variable with the same values, it’s probably more efficient to set the variables with $let (scoping all of them at once) and omit all the relevant parameters from the procedure definition. And conversely, for procedures that use unique variable values, I’d switch to $transclude and set the parameter as a widget attribute.

Alternatively, if you want to specify a default value for a given parameter in a procedure, you can omit it from the named parameter list and instead define it with $parameters, scoped to the procedure definition:

\procedure ptest3()
<$parameters arg={{{ [[]addsuffix[ss]addprefix[pa]] }}}>
<<arg>>
\end

* <<ptest3>>
* <<ptest3 fail>>
* <$transclude $variable=ptest3 arg={{{ PA SS +[join[]] }}} />

image

2 Likes

Andrew,

There are a few differences between macros and procedures. At first it seems if there is simply less ways to access the parameters and this is true on the surface. Where once you could use $name$ and $(name)$ now you can use only a variable reference <<name>>.

However in addition to this simplification there are other changes that support procedures and enable far more than we had with define. In no particular order;

  • Because the parameters are available as variables they can be used directly in filters with <name>
  • They can be used as values to other parameters param=<<name>> and directly in wikitext <<name>> without further delimiting param="$name$" as we had to.
  • Whilst as always we have access to variables defined outside our procedure, if that name is used as a parameter, it replaces the variable outside (gives it a local copy of the variable)
  • procedures can now be called using the <$transclude $variable=procname/> which enables even more to when invoking procedures.
    • Though note you can still use macrocall to call a procedure, it just does not support some new features documented in the $parameters widget
  • Something we often used the old macros for was Substitution, this becomes available using the backticks on values to attributes parameters
param=`literal ${ filter }$ and $(variable)$ $(parameter)$`
  • Beware that the above ${ filter }$ only returns the first value so you may need to join[] or join[ ] it.
  • But we have also being given the substitute Operator which allows this as well within a filter;
    • $replacable$ $(varname)$ literal ${ filter }$
    • and this can use used in functions below
  • All of the above is before we make use of the $parameters widget
    • Although documented for specific reasons the $parameters widget can be used in a number of ways we could describe as off script. I am yet to fully explore that
    • one feature is the $params=varname which is capable of returning all parameter/values
    • And this allows us to also leverage the $genesis widget to generate html tags or override existing widgets.

Further enhancements that have revolutionised tiddlywiki, in ways where once only macros existed, includes

  • Functions which are a great way to use filters in our procedures (or macros for that matter)
    • including custom operators
    • Unless you intervienn functions return the first value when referenced like a variable <<function>> but returns plain text to use without the $text widget.
    • Functions are evaluated in line and not at the end, allowing us to avoid the $wikify widget to get intermediate results.
  • Custom widgets, a great option if you want to pass blocks of text as a parameter
    • also provides an alternative to both \define and \procedure
  • Using parameters or the new ability to pass parameters during transclusion
    • also provides an alternative to both \define and \procedure

I hope this helps?

Of note:

If I look at your examples most are calling a macro/procedure with a variable.

\function auto()  [[]addsuffix[ss]addprefix[pa]] 

<$macrocall $name=dtest arg=`${ [[]addsuffix[ss]addprefix[pa]] }$`/>

<$transclude $variable=dtest arg=`${ [[]addsuffix[ss]addprefix[pa]] }$`/>

<$transclude $variable=dtest arg=<<auto>> />

In this case however `<<auto>>` gives the same result

These should work

It seems odd to me you are using [[] at the beginning of filters, as it returns an empty title but interestingly is[blank] not missing.

Credit where credit is due, this investigation has allowed me to discover that $let can mix literals with filters, meaning that instead of:

<$set name=var2 filter=[filter-here]><<macro-here val1 $(var2)$>></$set>

I can now do:

<$let arg1=val1 arg2={{{[filter-here]}}}><<macro-here>></$let>

Even though:

<$let arg1=val1><<macro-here>></$let>

is longer than:

<<macro-here val1>>

the benefit is a more consistent formatting of parameters, plus they have to be named, which ultimately improves readability of the code.

Back on topic, what I’ve found is that once HTML becomes part of a \procedure variable substitution is no longer possible:

\define dtest(arg) <span style="text-decoration:$arg$">text</span>
\procedure ptest1(arg) <span style="text-decoration:<<arg>>">text</span>
\procedure ptest2() <span style="text-decoration:<<arg>>">text</span>
* Macro:
** <<dtest underline>>
** <$let test={{{[[]addsuffix[line]addprefix[under]]}}}><<dtest $(test)$>></$let>
* Procedure1 (with parameter, called same as macro)
** <<ptest1 underline>>
** <$let test={{{[[]addsuffix[line]addprefix[under]]}}}><<ptest1 $(test)$>></$let>
* Procedure1 (with parameter, variable named as parameter)
** <$let arg=underline><<ptest1>></$let>
** <$let arg={{{ [[]addsuffix[line]addprefix[under]] }}}><<ptest1>></$let>
* Procedure2 (no parameter, called same as macro)
** <<ptest2 underline>>
** <$let arg={{{[[]addsuffix[line]addprefix[under]]}}}><<ptest2>></$let>
* Procedure2 (no parameter, calls adjusted to work)
** <$let arg=underline><<ptest2>></$let>
** <$let arg={{{[[]addsuffix[line]addprefix[under]]}}}><<ptest2>></$let>

only the first lot of text produced using the \define is underlined, as opposed to none of the \procedure output. The literal <<arg>> is output in the HTML without being substituted.

On re-reading the post of @TW_Jones, I’ve noted mention of a $genesis widget. I’ll see what that does…

It allows you to define html tags with the parameters applied within it, or redefine a widget.

Perhaps if you state what you want and we can give you possible solution(s).

Mostly, I’m just wanting to learn how procedures work. So far, my conclusion is “not well” or at least “they’re very much more complicated than macros”. I’ve spent all day so far (it’s now late afternoon) trying to get a procedure to do what I want without success. From memory, my macro was working in pretty much the time it took to type it…

This is the macro I currently have:

\define shield()
<dl class="shield $(class)$">
<dt style="background-color:$(labelcolor)$">$(label)$</dt>
<dd style="background-color:$(color)$">$(status)$</dd>
</dl>
\end shield

With the help of a stylesheet it makes badges like https://shields.io/ . Originally it used parameters, but I now like the variables created by $let so much I dropped the parameters. I’m interested in what the equivalent procedure would be. Examples:

<$let label=label status=status><<shield>></$let>
<$let label="Tiddler Count" status={{{[!is[system]count[]]}}}><<shield>></$let>

Leaving some variables unset and creating invalid CSS is OK as I have a stylesheet providing fallbacks.

I will have to look at it tomorrow. but it’s important for you to know procedures are if anything similar and in your above examples it is dependant on the parameter syntax of define. it is simply a matter of using a different way to substitute in the parameter values.

I will see if I can look later tonight otherwise tomorrow.

Just try;

\define shield()
<dl class=`shield $(class)$`>
<dt style=`background-color:$(labelcolor)$`>$(label)$</dt>
<dd style=`background-color:$(color)$`>$(status)$</dd>
</dl>
\end shield

Both define and procedure will work

However I can see with your use of $let there are other things to learn :nerd_face: .

Thank you very much for your help. I was staring at your code for a while wondering what you’d changed before I noticed the backticks! The variables outside the HTML didn’t need changing (they were being output as-is instead of being substituted). Now there’s a mystery margin around the dt/dd.

… solved! I’d removed \whitespace trim from my example, but it’s clearly important! :slight_smile:

\procedure shield()
\whitespace trim
<dl class=`shield $(class)$`>
<dt style=`background-color:$(labelcolor)$`><<label>></dt>
<dd style=`background-color:$(color)$`><<status>></dd>
</dl>
\end shield

Now it’s working as I expected. Thanks again.

1 Like

To add a little more to this thread, I had dropped parameters from my procedure because they were overriding identically named variables, preventing me from using the $let to set values. The solution turned out to be obvious: use $parameters and have the parameters default to the corresponding variable:

\procedure shield()
\whitespace trim
<$parameters
 label=`$(label)$`
 status=`$(status)$`
 color=`$(color)$`
 labelcolor=`$(labelcolor)$`
 class=`$(class)$`
>
<dl class=`shield $(class)$`>
<dt style=`background-color:$(labelcolor)$`><<label>></dt>
<dd style=`background-color:$(color)$`><<status>></dd>
</dl>
</$parameters>
\end shield

If necessary, default fallbacks are still available using filters - see “color” and “labelcolor” below. Filters could also be used without a fallback, see the “status” parameter.

<$parameters
 label=`$(label)$`
 status={{{[<status>]}}}
 color={{{[<color>!is[blank]]~[[limegreen]]}}}
 labelcolor={{{[<labelcolor>!is[blank]]~[[dimgray]]}}}
 class=`$(class)$`
>