What can be accomplished with procedures that is not possible or difficult to do with macros?

Procedures can get their attributes through parameters. Parameters give you an extra feature: use computed default values.

  • Got to tiddlywiki.com
  • Create a new tiddler and try the below examples
\procedure uta()
\parameters (p0: 0, p1: 10, p2: 20)
Hi, I am uta with p0=<<p0>> and p1=<<p1>> and p2=<<p2>>.
\end

\procedure uta2()
<$parameters
p0=0
p1={{{ [tag[Learning]count[]subtract[5]] }}}
p2=<<now>>
>
Hi, I am uta with p0=<<p0>> and p1=<<p1>> and p2=<<p2>>.
</$parameters>
\end

Example i

* <<uta>>
* <<uta 3 p1:4>>
* <<uta 3 p2:1000>>

Example ii

* <<uta2>>
* <<uta2 p1:123>>
* <<uta2 12 200 "May 18th, 2021">>

NOTE i: The below macro is equivalent to the uta1 procedure.

\define uta (p0: 0, p1: 10, p2: 20)
Hi, I am uta with p0=<<__p0__>> and p1=<<__p1__>> and p2=<<__p2__>>.
\end

NOTE ii: You may use the old school macro for uta2 but with some scripting and checking input argument!

5 Likes

Big Warning

  • Don’t use more than one parameters (e.g. \parameters or <$parameters...>) inside a procedure
  • Don’t use parameters with attributes (e.g. a procedure has attributes and also use parameters)
  • Passing parameters by name is the best practice for clarity/readability and maintenance.

NOTE: If you are curious, see detailed discussions here: [BUG] The First Attribute of a Parameters Pragma is Overwritten when Is used Inside a Procedure · Issue #7458 · Jermolene/TiddlyWiki5 (github.com)

2 Likes

Mohammad,

Ultimately I don’t think there is anything that "can be accomplished with procedures that is not possible or difficult to do with macros?

I would just add, if you want to call a macro or procedure with $macrocall or $transclude $variable the parameters can also have variables and transclusions, or triple curly braces (filtered transclusions) and the new backtick attributes (Substituted Attribute Values).

  • What I value about the parameters widget is how easy it is to read, and by defining parameters you kind of do a group of parameter variable definitions.
    • It is a bit like the procedures initialisation step.
  • You can use parameters in other places including in a transcluded tiddler, and there are more tricks to be discovered.
  • You can use parameters in the procedure/macro definition AND the parameters widget and as long as you deal with the “intersection of the two” it can be used as a feature.
  • Both macros and procedures may need wikifying if their result is to be converted to a variable before the final display.
    • Thus if a value can be evaluated using a filter, in a function or custom filter operator it is much easier to use.
\define my-macro()
<$parameters a b>
<<a>> <<b>>
</$parameters>
\end my-macro

<<my-macro a:1 b:2>>
  • Parameters works in macros and Transclusions

Thank you for this! I’ve been ignoring the ParametersWidget as it seemed just syntactic sugar that was less sweet than the more familiar

\procedure uta(p1: 0, p2: 10, p3: 20)

This one line explained why they are more powerful. Thank you!

1 Like

Very cool! I expect to use these quite a bit. With your help, I understand that I can design a repeatable pattern (procedure) whose various parameter-values automatically follow whatever I can specify using wikitext and filter expressions (etc.)… PLUS I retain the option to override any of those parameters “on the fly” as needed when I invoke it. Perfect!

I’m one of those people who struggle to unpack nonsense variables (uta, p0 p1 p2) while I’m still learning. My brain gets distracted by wondering, “Wait, what is “uta” supposed to mean?”

So, for anyone else out there like me in this way, I offer below a simple variant on @Mohammad’s example, using words and code that might intuitively make sense for someone trying to see how they can put this power to use.

In the example below, the procedure specifies parameter values around common-sense conditionals with the new conditional shortcut syntax: I want to add modified date only if it differs from create date; I want to include a nudge to upgrade, iff TiddlyWiki version is not 5.3.3, etc.:

\procedure tiddler_overview()

<$parameters 

name=
"""<% if [{!!title}prefix[Draft]] %> @@oops, still a draft@@ <% else %> <$link/> <% endif %>""" 

date-summary=
"""//<$view field=created format=date template="DDth mmm YYYY" />//
<% if [{!!modified}!match{!!created}] %> (then updated on <$view field=modified format=date template="DDth mmm YYYY" />)
<% endif %>"""

version=<<version>>

vers=
"""<% if [<version>match[5.3.3]] %> ✅ <<version>> (current version) <% else %> version <<version>> (Please [[update here|https://tiddlywiki.com/]]) <% endif %>"""

>

!!!This tiddler demonstrates ''<<name>>''. It was created on ''<<date-summary>>'' using TiddlyWiki ''<<vers>>''.

</$parameters>

\end

<<tiddler_overview name:"The Power of Procedures">>

<<tiddler_overview date-summary:"a cloudy day long ago" version:"5.3.1">>

Result:

1 Like

Thank you. Another nice example!

Yeah, those really should be named foo, bar, and baz!

(Running quickly away. :laughing: )

There is an interesting point to note about using this technique. The variable you might not want to be publicly available, vers is still available for public usage:

<<tiddler_overview "Something" "a long time ago" "5.2.4-prerelease" "a fw lins of potry">>

yields:

This tiddler demonstrates Something. It was created on a long time ago using TiddlyWiki a fw lins of potry.

Note that “5.2.4-prerelease” is ignored, and instead our mini-definition of verse (“a few lines of poetry”) ends up appearing where you want to show the version number and its current(ness)1 status.

When we’re using named parameters, this would not be particularly surprising, but when we’re using positional ones, it could lead to hard-to-find bugs. This might sometimes be a chosen behavior. (I often do the equivalent in JS.) But it can also cause problems. (I tend to do this mostly when I’m the only one who’s going to use it.)


1 Sorry, “currency” is probably the right word. But sometimes the right word is downright wrong!

2 Likes

image

@Springer Just to be clear, 5.3.1 wasn’t released long ago, it was actually a quite recent release. And I’m sure you know that, in actuality, the sky has been blue and sunny since the advent of 5.3.0.

Yes, I know it’s only a demo, but really, you could try to be a little more accurate. tut, tut.


Yes, I know the correct spelling of "tut, tut" is tsk, tsk, but I hate it.

:stuck_out_tongue:

Hm, I don’t see any cause for concern here. The fourth parameter is functioning just as it should. (With all due appreciation for your pun on the parameter vers!)

In fact, one thing I found powerful, here, is the ability for one parameter to work directly with the output of a prior one, and for the procedure call to override defaults at any point in the “assembly chain” as needed. This will be important for things like refactoring bibliographic citations. A citation procedure could plug in lots of default values, build a complex string based on those component parts, and allow the end-user to overwrite any of the component parts, or even the whole, as needed.

(One of my current challenges with refnotes is that if a citation is assembled inaccurately (for example an NGO, or maybe just its acronym, needs to be listed as an author for a report, but hard-coded patterns insist on extracting a surname), it’s really hard to just make a local fix. Yes, I’d love to have time to solve for each anomaly until we have a foolproof citation algorithm, but handling all bibliographic irregularities is an ever-not-quite problem; often one just needs to move on with actual work. With the procedure version, I would call <<citation>> with no parameters most of the time, I could specify just <<citation author:"W.H.O.">>, or I could even overwrite the whole string while keeping the citation’s link and popup-preview behavior <<citation result:"Ibid">>.)

Certainly, whenever a procedure is complex — especially if it has more than 2 parameters — a user is wise to name the parameters, rather than to specify them sequentially. I think folks will learn that pretty quickly! And with a complex procedure that’s designed well, the first parameter(s) may be the most frequently in need of direct specification, but we’ll often want to skip over to specify, say, only the third.

It’s not a problem if you are planning on it and expecting it. But it’s easy enough to overlook. In JS, I mostly use the equivalent technique for internal functions and not those I expose to others, for precisely this reason.

JavaScript Example

One classic example in JavaScript involves the parseInt function and array.map method. JS has parseFloat, which converts a string like "867.5309" into the floating-point (decimal-ish) number 867.5309. There’s a similar parseInt function which converts, say "42" to 42. When we combine parseFloat with map, we get:

["1.1", "2.2", "3.3"].map(parseFloat) //=> [1.1, 2.2, 3.3]

just as expected, because map runs the supplied function on each element of the array, and returns a new array with all those results.

But if we try the equivalent with parseInt, we have a real problem:

["1", "2", "3"].map(parseInt) //=> [1, NaN, NaN]

where NaN is the special value NotANumber, used where a number is expected but a non-numeric value is found.

The reason is two-fold.

First, the signature of parseInt is (string, radix?) -> number; there is an optional small-integer parameter radix, which defaults to 10 if not supplied. This allows us to parse binary or hexadecimal number, like so

parseInt('cab123', 16) //=> 13283619

But since base 10 is by far the most common, it is assumed if you don’t supply an argument, or, for some weird reason, if you supply 0. This parameter defaults to 10

Second, map is slightly more complex than assumed. The function you pass to map is called each time not just with the item, but with two other arguments. We also get the index in the array of the item, and the whole array as well. So,

['a', 'b', 'c'].map((value, index, array) => name + index) //=> ['a0', 'b1', 'c2']
// (We declare `array` for demonstration purposes, but we ignore it here.)

Combining the map behavior with parseInt, we get that ['1', '2', '3'].map(parseInt) is equivalent to

[parseInt('1', 0), parseInt('2', 1), parseInt('3', 2)]
  • parseInt('1', 0) for an odd reason is treated as if it were parseInt('1, 10), which is just 1`.
  • parseInt('2', 1) is illegitimate, as 1 is an illegal base for number. So we get NaN.
  • parseInt('3', 2) has a legitimate radix, but 3is an illegal digit in binary. Again we getNaN`.

This, of course, is just an example. But his behavior has perplexed any people.

I’m not even slightly suggesting that people don’t use this; the problems are less likely than in raw JS, I believe. I would just suggest that users of this technique be aware of the possibility.

(With all due appreciation for your pun on the parameter vers !)

Well, you know what they say, “A good pun is its own reword.”