Working with lists in TiddlyWiki

I am trying to understand how would a piece of Python code look in wikitext and I’m struggling to grok the “TiddlyWiki way” of thinking.

First, two reference facts.

    • A variable is a snippet of text that can be accessed by name.

Can I use the (more familiar to me) term “string” instead here as synonym? What if I want to hold a list (I think in a JavaScript context, the term “array” is preferred?) in a variable? Is this kind of code possible in TiddlyWiki wikitext, or resorting to writing JavaScript modules is necessary? I’ll go into details below showing a code example.

  1. The ListWidget is used to display a list of items.

While it has a “programming” feel by exposing currentTiddler and having the variable and counter attributes, allowing to loop over items in a list, just because those items have to be immediately displayed rather than collected into variables for further use - this makes me treat the list more like the View part in context of the MVC pattern.

Below is a small Python program that I fail to understand how would it look in wikitext and if it’s possible at all or if this kind of things have to be done in JavaScript:

#!/usr/bin/env python3

linput1 = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
linput2 = ["a", "b", "c", "d", "e", "f", "g", "h", "i"]

# insert an "X" element after each 3 elements 
loutput1 = []
for i in range(len(linput1)):
    loutput1.append(linput1[i])
    if (i + 1) % 3 == 0:
        loutput1.append("X")
print(loutput1)
# ['1', '2', '3', 'X', '4', '5', '6', 'X', '7', '8', '9', 'X']

# combine two lists
loutput2 = []
for i in range(len(linput1)):
    loutput2.append(linput1[i])
    loutput2.append(linput2[i])
print(loutput2)
# ['1', 'a', '2', 'b', '3', 'c', '4', 'd', '5', 'e', '6', 'f', '7', 'g', '8', 'h', '9', 'i']

Tiddlywiki is a mvc type of thing, I would say that wikitext is the last or outer part of the controller, and it is also the foundation of the view… The store (of tiddlers) is the model. It has its design based on the view reacting to changes to the store, i.e. changes to tiddlers due to user interactions with the view. Filters in tiddlywiki manipulate lists (primarily of tiddlers), with lots of functionality to read tiddlers, with some functional programming type operations, it is not designed to do imperative types of manipulations.

Having said that I like writing javascript as well as wikitext, and choose to use both in my wikis.

I do find it hard to think of how to map you python example to wikitext, because wikitext is used mainly to convert tiddlers to the view. However it can also be used to sometime manipulate data. Would linput1 and linput2 be contents of tiddlers and the loutput1 be another tiddler, and your procedure run when a button is pressed?

Let’s try and reduce the problem even more, by assuming that no tiddlers are involved at all, it’s just variables as input data. Something like this:

<$vars linput1str="123456789" linput2str="abcdefghi">

I can produce the list from a string

<<list-links "[<linput1str>split[]]">>

I can do the reverse

{{{ [<linput1str>split[]join[]] }}}

---

Or:

<<list-links "[<linput2str>split[]]">>

Having the `linput1str` and `linput2str` variables as input data, how to produce the output lists from the Python example?

</$vars>

I would say the wikitext is nothing like python, and something like that would be hard for me to solve in wikitext, but probably possible.

Here are several approaches:

  1. with a $set widget
<$set name="list1" filter="a b c d e f g h i j">
... <<list1>>
</$set>

<$set filter= is the only option that automatically saves a title list (adding whitespace and square brackets between its outputs as necessary). The other options all make use of +[format:titlelist[]join[ ]] (essentially, the inverse of enlist-input[]) to save the list as a single string.

  • format:titlelist[] wraps each of its inputs in square brackets if that input contains a space. If you are sure that none of your list items will contain spaces (as in these examples) you can technically omit it and use join[ ] only… but it doesn’t hurt to include it even if it’s not needed.
  1. with a $let widget
<$let list2={{{ A B C D E F G H I J +[format:titlelist[]join[ ]] }}}>
... <<list2>>
</$let>
  1. with a function
\function list3() 1 2 3 4 5 6 7 8 9 10 +[format:titlelist[]join[ ]]

... <<list3>>
  1. with a $list variable
<$list filter="0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 0.10 +[format:titlelist[]join[ ]]" variable="list4">
... <<list4>>
</$list>

Demoing all four techniques:


Note that because we’ve saved all the results of the list4 example as a single title list, this $list widget only loops once, rather than the ten times we’d expect without +[format:titlelist[]join[ ]].

Re: your Python example: @rmunn proposed a new filter operator that would simplify interleaving lists (original GitHub issue; more recent Talk discussion), though it seems to have stalled after debate about what sort of behavior users would expect. However, we can currently do this sort of thing with a helper function:

1 Like

Try this:

\define list1() [[123456789]split[]]
\define list2() abcdefghi

<$list filter=<<list1>> counter="cnt" >
<<currentTiddler>> <$text text={{{ [<cnt>remainder[3]match[0]then[X]] }}}/>
</$list>

<$list filter=<<list1>> counter="cnt" >
<<currentTiddler>> <$text text={{{ [<list2>split[]nth<cnt>] }}}/>
</$list>
1 Like
\function .insertAfter(marker)
[all[]]  [<index>add[1]remainder[3]match[0]then<marker>] 
\end .insertAfter

\function .interleave()
[enlist<list1>zth<index>] [<currentTiddler>]
\end

<$let 
	list1="1 2 3 4 5 6 7 8 9"
	list2="a b c d e f g h i"
>

<!-- insert an X element after each 3 elements -->
{{{ [enlist<list1>] :map:flat[.insertAfter[x]] }}}

---

<!-- combine two lists -->
{{{  [enlist<list2>] :map:flat[.interleave[]]   }}}

</$let>

You can use the same let widget to assign those outputs to variables for further processing instead of printing them out.

4 Likes

Related question:

The documentation for title lists states they are the simplest case of a filter (because they just list items explicitly), so it’s ok to mention this when introducing filters.

Is it correct to first mention title lists in context of introducing variables (right after introducing variables with text/string values) as a way to store lists/arrays rather than strings?

I am asking because I am trying to structure my TiddlyWiki knowledge as I learn things in a strict tree-like way (unlike the official docs, which seem to be more oriented towards being used like a reference; for example see https://tiddlywiki.com/#Concepts where concepts are ordered alphabetically), and in my mental model variables seem like a simpler and more basic concept than filters, so I introduce them earlier.

This is the approach that I found easiest to understand, because it follows the logic of my Python example script closely :+1:

There’s just one detail I’d like to ask about. The .insertAfter function seems to be designed to be used with a :map call, which sets the index variable. So inside the function, this index variable feels like a global one, because it is missing from the function signature, thus making the function look a bit too tightly coupled to the rest of the code. I’d rather prefer to parametrize it like this:

\function .insertAfter(i, marker)
[all[]]  [<i>add[1]remainder[3]match[0]then<marker>] 
\end .insertAfter

\function .interleave()
[enlist<list1>zth<index>] [<currentTiddler>]
\end

<$let 
	list1="1 2 3 4 5 6 7 8 9"
	list2="a b c d e f g h i"
    x="x"
>

<!-- insert an X element after each 3 elements -->
{{{ [enlist<list1>] :map:flat[.insertAfter<index>,<x>] }}}

---

<!-- combine two lists -->
{{{  [enlist<list2>] :map:flat[.interleave[]]   }}}

</$let>

Is this equivalent code and does it have any drawbacks compared to your version?

None that I can think of.

Now I know how to use <index> :slightly_smiling_face: Maybe these would make good examples for the docs.

I am not sure how to interpret [all[]] in .insertAfter. It seems it can be replaced by [<currentTiddler>] i.e

\function .insertAfter(marker)
 [<currentTiddler>] [<index>add[1]remainder[3]match[0]then<marker>]  
\end .insertAfter

Using <currentTiddler> makes the function reliant on being called in a context where that variable is properly defined, such as inside of a map filter run prefix. Otherwise it is functionally equivalent here. The all operator with no parameter just passes through the input, see https://tiddlywiki.com/#all%20Operator

1 Like

Warning: Geeky comment on the way! Feel free to ignore.

As a big fan of functional programming, I’ve struggled a lot with this across Tiddlywiki. After years of casual engagement, I’ve been deeply engaged with TW for two and a half years, and I’m only now starting to accept this. The mental model I’m beginning to develop is not one of action-at-a-distance global hell, but rather of nested contexts.

All <$let>, <$set>, <$wikify>, and <$list> widgets—and many others—introduce new contexts that your code runs in. So too does every function, macro, and procedure call. These are recursively nested, so unless you shadow a variable, all the outer variables are available to you in the inner context.

I have not looked at the code to confirm this, but I assume that variable resolution simply proceeds from the innermost context to the outermost one. (This may use JS’s prototype chain for a context object, but could be done many ways.) Thus when looking for <currentTiddler>, we don’t start with some global value. We find the nearest enclosing context that defines this variable. So too inside :map. It’s documented to supply the index variable, as well as revIndex, length, currentTiddler and ..currentTiddler ones.

It’s not quite as frightening as it once was, although still a bit spooky!

Personally, I find it difficult to draw a hard line between title lists and strings: to include more than one string/title in a stored variable, you have to insert spaces (and bounding brackets as needed), thereby converting your list of strings into… a new, longer string. Essentially, a title list only exists as a collection of discrete strings (rather than a single string than can be converted to smaller strings) when it appears in the context of a filter — and a filter like GettingStarted [[Discover TiddlyWiki]] Upgrading is really a shortcut for the series of filter runs [title[GettingStarted]] [title[Discover TiddlyWiki]] [title[Upgrading]]. So if you want to introduce the concept of title lists before you introduce filters/$lists, you’ll probably want to move on to filters immediately after, because it’s otherwise unclear why you’d want such a list or what you’d do with it.

A few factors you might also want to consider if you’re trying to construct a linear learning sequence:

  • The simplest possible way to enlist a title list stored as a variable is by using it as the value of a $list filter attribute, e.g <$list filter=<<myVariable>> />. But this is almost certainly not the first way you’ll encounter a $list, as both the docs and the TW architecture most commonly use $lists with literal strings as their filter attributes — that is, filter="[[one or more filter runs]] [[all wrapped in quote marks]]".
  • Within a filter, you’ll typically use
    • enlist, a selection constructor which expects a title list (often stored as a variable or a field transclusion) as its parameter, e.g. [enlist{!!tags}])
    • or enlist-input[], which must be directly preceded by such a title list.
  • The first title lists many users are likely to construct (even if inadvertently) are the ones in the tags field (modified when adding/removing tags). We have some special operators just for working with tag lists:
    • tags[] = get[tags]enlist-input[] or enlist{!!tags}
    • tag[A] = contains:tags[A]
    • untagged[] = !has[tags]
    • [A]tagging[] = tag[A]
  • For working with title lists stored in fields/indexes, we also have the list operator (another selection constructor).
    • [list[HelloThere]] = [list[HelloThere!!list]] = [enlist{HelloThere!!list}]
1 Like

That’s exactly, how it is implemented. Whenever a widget is instantiated, the .variables object from the parent-widget is copied over. So all variables from all parents are available.