A macro trap in case of side effect involved

I coded the following function. The idea is to changed the text if the tiddler is of tw2.5 type.

\define _adapt()
  \define _transform() <$action-setfield changed=<<transform>> text={{{ [{!!text}search-replace:g<sourceRoot>,<destRoot>]" }}}/><$action-log $$message=_transform $$filter="currentTiddler source dest original target" newText={{!!text}}/>
  <$let transform = {{{ [{!!type}match[text/vnd.tiddlywiki]then[_transform]] }}}>
	 <$macrocall $name=<<transform>>/>
	  <$action-log $$message=_adapt $$filter="transform source dest" text={{!!text}} type={{!!type}}/>
	</$let>
\end

It turns out that whatever the text type, the changed it made. The changed was made by the _transform macro, which was only called if of the proper type.

First, a code that do what I want to do:

\define _adapt()
  <$let transform = {{{ [{!!type}match[text/vnd.tiddlywiki]then[_transform]] }}}
		text = {{{ [{!!text}search-replace:g<sourceRoot>,<destRoot>] }}}
		newText = {{{ [<transform>!is[blank]then<text>else{!!text} }}}
	>
	  <$action-setfield text=<<newText>>/>
	  <$action-log $$message=_adapt $$filter="transform source dest" text={{!!text}} type={{!!type}}/>
	</$let>
\end

Now, a question. I have concluded that the macro is always invoked when first encountered. Since mine had a side effect, it did its job even when not called. Am I right?

If I’m right, would it not be better to alert people on this? Could this behavior be corrected?

How are you invoking the macro? Just typing the name?

I’ve never seen a macro defined inside a macro. Is that a new thing or … ??

Macro’s should only do substitution. So I’m skeptical that there are side effects. That is why I ask how you are triggering the macro.

1 Like

Look at my first code. I’m building a transform variable and then I invoke the <$macrocall> widget with that variable as $name parameter. Thus, the transform macro is called or not called.

But how are you calling _adapt() ?

It’s not all that new, and is actually quite useful.

For example, suppose you are writing a popup and want to have “ok” and “cancel” buttons, as well as handling keyboard “enter” and “escape”. You could write something like this:

\define showpopup()
\define done() <$action-setfield $tiddler=<<target>> text={{{ [<popid>get[someinput]] }}}/>
\define close() <$action-deletetiddler $tiddler=<<popid>>/>
<$reveal state=<<popid>> type="popup"
   class="tc-block-dropdown tc-popup-keep" style="padding:0.5em;">
   <$keyboard key="escape" actions="<<close>>">
   <$keyboard key="enter" actions="<<done>><<close>>">
      Enter some text: <$edit-text tiddler=<<popid>> field="someinput" focus="yes"/>
   </$keyboard>
   </$keyboard>
   <$button tooltip="cancel" actions="<<close>>">{{$:/core/images/cancel-button}}</$button>
   <$button tooltip="ok" actions="<<done>><<close>>">{{$:/core/images/done-button}}</$button>
</$reveal>
\end

<$let popid={{{ [[$:/state/popup/mypopup]addsuffix<qualify>] }}} target="SomeTiddler"
   init="<$action-setfield $tiddler=<<popid>> someinput={{{ [<target>get[text]] }}} />">
<$button popup=<<popid>>>set somefield <<init>></$button><<showpopup>>
</$let>

Note that the definitions of <<done>> and <<close>> are local to the <<showpopup>> macro and are invoked for both <$keyboard> and <$button> actions. Similarly, <<init>> (defined using a $let widget) is scoped only surrounding the $button for invoking the popup.

Using these “macro definition inside another macro” and “macro defined as a variable” techniques avoids repeating code as well as keeping the code nice and compact. However, there are some limitations to using these code patterns:

  • The macro definitions must be one-liners, since you can’t use \end within the containing macro, or it would prematurely terminate that macro.
  • The macro definitions can’t use $param$ or $(variable)$ syntax, since those would be automatically substituted when the containing macro is processed. However, they can use <<variable>> or <<__param__>> syntax, since those are interpreted “on-the-fly” when the inner macros are invoked.

enjoy,
-e

5 Likes

Thanks Eric! If that’s in the docs, I totally missed it. It would indeed have been useful on many occasions.

We learn something new every day.

  • I don’t think this is documented anywhere (except in @EricShulman head), and thus may easily break in a future release?
  • @EricShulman could you give a simpler example?
  • Or perhaps a generalised solution so we could make use of this nifty user interface element in your example?
    • Eg macro called with tiddlername/fieldname defaulting to current tiddler, to edit named tiddlername/fieldname
  • I now see how the use of the \whitespace trim works in this way.
  • As other may know things can break if you have empty macros of this form
\define dummy()

\end

or

\define dummy()
\end

Here is my minimal example of this pattern;

\define topmacro()
\define submacro() submacro output
\define submacro2() submacro output in <<currentTiddler>>
Internal of topmacro using '<<submacro>>' and '<<submacro2>>'
\end

<<topmacro>>

'<<submacro>>' not valid outside topmacro
  • It is important to note this could easily give rise to macro edits that break the code because we do not normally think that the following represents two different macros
\define submacro2() submacro output in <<currentTiddler>>
Internal of topmacro using '<<submacro>>' and '<<submacro2>>'

Thanks for your wisdom @EricShulman

The various pragmas are also not documented.

And where documented too brief and hard to search for.

The parameterised transclusion docs updates includes new docs for all the pragmas:

https://tiddlywiki.com/prerelease/parameterised-transclusions/

6 Likes