TiddlyWiki / TiddlyWiki5

A self-contained JavaScript wiki for the browser, Node.js, AWS Lambda etc.
https://tiddlywiki.com/
Other
7.97k stars 1.18k forks source link

[IDEA] Add a `:then` filter run prefix #7385

Closed yaisog closed 1 year ago

yaisog commented 1 year ago

Let me start with two relevant quotes by @saqimtiaz

I find filters written inline far easier to read, comprehend and maintain.

and @Jermolene

Having a profusion of filter prefixes doesn't feel bad because we can already be confident that the vast majority of users need never learn about their existence.

Both of which I wholeheartedly agree with.

With this in mind, I would like to propose adding a :then filter run prefix, akin to :else and the then filter operator.

An example usage would be a subfilter defined via pragma:

\define search-subfilter() [{$:/state/searchFields}match[all]] :then[search:*<searchTerm>] :else[search:title,caption<searchTerm>]

{{{ [subfilter<search-subfilter>] }}}

I believe it is hardly possible (at least without text substitution) to create something like that without :then. The then operator won't work, because the "parameter" would be a filter run. Other combinations of subfilters are similarly fruitless, and become increasingly difficult to read. I could probably wrap the subfilter call in a $set to define the subfilter, but I don't want to do that in this case, as I'd like to import and reuse the \defines. There are many such \defines, which makes using $importvariables with $sets impractical.

The implementation would be straightforward: If there is input, replace it with the output of this filter run. The input to the filter run should be all[tiddlers], or for subfilters the input to the subfilter, like :else. A debatable point might be if an input of a single empty string should count as empty input, but this must be handled the same as :else, so there is no debate.

@kookma: Feel free to describe your use cases if you have good ones.

PS: I realized after the fact that the example subfilter is unintentionally clever. 🤓 If the :then run is executed but yields no results, the :else path will also be executed. But since it checks only a subset of the fields, it must also come up empty and so give the correct (empty) result. It wouldn't have worked the other way around where title and caption were checked first. Upon an empty result the search through all fields would have automatically executed and might have yielded false positives.

saqimtiaz commented 1 year ago

A :then filter run prefix was in fact the first one I wrote and that eventually provided the impetus to introduce filterrunprefix modules, allowing the addition of prefixes without needing to override core code. You can find an implementation here, though it needs updating with some of the improved handling for variables. Note that it also provides two variables to the run __input as a stringified representation of the previous runs output and __count.

Since no one else has previously expressed the need for this prefix, and I have had some doubts regarding these variables and how necessary this is for the core, I have never created a PR.

I believe it is hardly possible (at least without text substitution) to create something like that without :then.

You can use :map as a stand in for :then if you write your first filter run such that it produces only a single output item or none.

yaisog commented 1 year ago

You can find an implementation here

Hi @saqimtiaz, I remember you hinted at an implementation that you already had, but I never found it and was too shy to ask. 😝 But as with most things TiddlyWiki, I just rolled my own. It's a straight adaptation of the :else FRP, without the additional variables that yours provides. I can see how they might be useful, though I wouldn't need them here.

You can use :map as a stand in for :then if you write your first filter run such that it produces only a single output item or none.

For the search example above I need the input to the :then filter run to be the same as the input to the subfilter operator (a list of tiddlers to search). I don't think this can be accomplished with :map, unless I put that list into another variable that the :map filter run then enlists as first operation.

pmario commented 1 year ago

Did you try it with the :filter prefix instead of :then

yaisog commented 1 year ago

Hi @pmario, no I didn't. I think the input to the :filter run would be the output of the preceding run and thus either nothing or "all". The tiddler "all" does not exist, and searching its title for searchTerm is not going to give me the intended result.

kookma commented 1 year ago

@kookma: Feel free to describe your use cases if you have good ones.

I highly support having :then filter run prefix. You may achieve the result you want by other complex and lengthy filter runs, BUT having :then helps to write more readable/understandable/maintainable wikitext (script) here.

One of the best example is the one given by @saqimtiaz answering my question in the Talk

<$list filter="[<__userinput__>match[1]] :then[subfilter<noteFilter>] :else[subfilter<journalFilter>]">

I provide more examples below.

kookma commented 1 year ago

Another example

The old way:

\define select-between-two-filters(cond:yes)
<$list filter='
[<__cond__>match[yes]]
:map:flat[all[tiddlers]tag[Learning]first[10]] :else[tag[HelloThere]first[3]]
'>

</$list>
\end

<<select-between-two-filters no>>

---

<<select-between-two-filters yes>>

New way: This is much more readable if it can use :then

\define select-between-two-filters(cond:yes)
<$list filter='
[<__cond__>match[yes]]
:then[all[tiddlers]tag[Learning]first[10]] :else[tag[HelloThere]first[3]]
'>

</$list>
\end
kookma commented 1 year ago

Note that it also provides two variables to the run __input as a stringified representation of the previous runs output and __count.

This is a clever implementation and looks a TW way of if-then-else.

What I learned in other language like Python/Fortran/Julia the then and else are independent from condition checked by if-clause

if x>3 then   y=10
       else   y=-15

So I though likewise in Tiddlywiki

{{{ [tag[Tasks]] :then[all[tiddlers]prefix[Task]get[due]!sort[]] :else[all[tiddlers]tag[Done]]

But what your implementation of :then not only covers this case but also acts on input from upstream run (a TW way)

kookma commented 1 year ago

@kookma: Feel free to describe your use cases if you have good ones.

A general form can be

<$list filter="[conditional-filter] :then[subfilter<trueFilter>] :else[subfilter<falseFilter>]">
...
</$list>

The first filter output if is not empty, :then part shall be run in all other cases (e.g. empty in TW) the :else part shall be run.

This is an example of above syntax in the current release (without :then)

<$let decisionMacro= {{{ [<currentTiddler>prefix[who]then[trueMacro]else[falseMacro]] }}}>
   <$macrocall $name=<<decisionMacro>> />
</$let>

each macro has its own list and filter to do the job! This syntax may look clear and understandable, but it has duplication of code and lengthy!

yaisog commented 1 year ago

The first filter output if is not empty, :then part shall be run in all other cases (e.g. empty in TW) the :else part shall be run.

There is the one caveat that if the :then run results in an empty list, the :else part will also run, unless we design :then in a way that the filter run only replaces its input if it has a non-empty output and otherwise passes the input along, so that :else will not trigger. This might not be what subsequent runs expect, though. Also, one could put an else operator at the end of the :then run to guard against this... @saqimtiaz: How does your implementation handle this case?

yaisog commented 1 year ago

Here is another application in which a :then prefix will make the filter code look nicer: When or-combining two conditions that should result at maximum in one execution of a $lists content, we would normally do somehing like

<$list filter="[«condition1»] [«condition2»] :and[then[yes]]" variable="void">

or maybe :and[first[]]. This would look better if we could use :then[[yes]] in the final run, wouldn't it?

Jermolene commented 1 year ago

I'd be happy to have a :then filter run prefix in the core.

AnthonyMuscio commented 1 year ago

I support this initiative as well because If then else is an important structure even for new users.

The special values Infinity and -Infinity can be used to represent positive and negative infinity respectively

See here https://tiddlywiki.com/#Mathematics%20Operators