Absolutely. It’s not so odd. Whether you use an agent alias or a rule/edict/stamp, that part only identifies one end of the link you wish to create. Then for that object (or each of them if more than one) you need to re-search the whole document via a find()
-based query. In a small doc the query doesn’t matter, but in a bigger, longer-lived doc you want to give thought to scoping the query, for a reason I’ll return to in a moment.
Why is .contains()
? $Tags is a Set-type attribute containing zero or more values. By comparison, a String-type can only have one value. The ==
equality test, when used on on a list (which is stored as a single string with semi-colons delimiting the discrete values) tests the whole list as opposed to discrete list items. Consider:
""=="Bar"
. No match, so correct.
"Bar"=="Bar"
. Match, so correct.
"Foo;Bar;Baz"=="Bar"
. No match, so false negative if trying to test for ‘Bar’ in a list.
Again, using a string literal instead of an attribute, the need for .contains()
becomes clearer:
$MyBoolean = "Foo;Bar;Baz".contains("Bar")
. True!
But, although we’re using a literal test string ‘Bar’ rather than a regular expression (a ‘regex’), it is the case that .contains() is a regex-based and comparatively computationally expensive compared to other actions. Now in a small test doc, the difference is moot. In a mature doc with thousands of notes, you may see some effect.
So, looping back to my first point, try to scope your queries—both in agents and within a find()
—so you test as few notes as possible. So if your doc has 500 notes, 400 use $Tags and ContainerFoo contains only 20 notes, this is a wasteful query:
$Tags.contains("Bar")
…because the query will test $Tags in 500 notes. Better might be something like:
inside("ContainerBar") & $Tags.contains("Bar")
Now, the first term finds 20 out of 500 notes with a simple text and only those 20 are tested for a $Tags value. Of course you are looking for a generalised pattern as the real use case is for items in more than one container. This is where prototypes are really useful. If you use a common prototype for all the notes needing this sort of test, then this is possible:
$Prototype=="pFoo" & $Tags.contains("Bar")
Now the first part of the query finds only those notes using that prototype, etc. See how this makes your queries/finds more efficient at scale.
Now, it may be the notes you want to test already use more than one type of prototype so you could check for the likes of:
($Prototype=="pFoo" | $Prototype=="pBaz") & $Tags.contains("Bar")
But with lots of prototypes in such a context you might do better to use a hierarchical approach, by making sure all your link-testing notes are descended from a single container, allowing us to use a descendedFrom()
test:
descendedFrom("Annotations") & $Tags.contains("Bar")
or perhaps we might further filter only descendants that have a prototype set.
descendedFrom("Annotations") & $Prototype & $Tags.contains("Bar")
On a different tack, if we don’t use an agent to run our action but instead use an edict (easily set via a prototype) then we can make our use of find more efficient:
if(linkedTo("Bar")==false){
linkTo(find($Tags(that).contains($Name)));
}
Note we don’t need linkToOriginal
as we are not acting on the original. In the if()
test we ask the note if it is already linked to ‘Bar’. If it is we ignore the linking action with its associated find()
and .contains()
test. Hopefully you can see how we now avoid unnecessary code being run - and code which at scale can affect performance. If you’re reading along and just starting out with actions - don’t worry - these considerations are moot in small documents and only being to make sense once your TBXs grow. Plus, don’t worry if you’ve got a large doc and want to implement the ideas like above. Tinderbox makes that easy, though some careful work may be needed - i.e. work on a copy of important docs if doing major surgery!
Anyway, I hope that helps explain some of what’s going on and why I’ve taken the approach I have.