Length(r.name), length(r.name[*]), or length(r.name[*].id)

All of the following see to work in tf 12:

length(r.name), length(r.name[*]), or length(r.name[*].id)

I’m wondering if there is a best practice?
Are any of these relying on some undefined behavior or something that is planned to go away?

Hi @yruss972,

I think for this let’s ignore the length function for the moment and talk about the meanings of those expressions you used as arguments.

  • r.name is the most straightforward: it literally produces the value of r.name, unchanged. The attribute name “name” makes me expect that this is a string value, in which case r.name would be a string and so length(r.name) would return the length of that string. If r.name were instead a collection (list, map, etc) then length(r.name) would return the number of elements in that collection.

  • r.name[*] is applying the splat operator to the value of r.name. What this does again depends on the type of r.name.

    If r.name were just a string then this would activate the splat operator’s special behavior of implicitly turning a non-list value into a list: if r.name were "foo", for example, then the result would be ["foo"] and so length(r.name[*]) would be 1.

    If r.name were a list then applying just [*] to it would be redundant: the result would be the same as just r.name alone. To say that another way, r.name == r.name[*] if r.name is a list, and therefore it follows that length(r.name) == length(r.name[*]) too. There’s no reason to use [*] alone with a list value, because it does nothing.

  • r.name[*].id is a more typical use of the [*] operator. As in the previous point, the behavior depends on what the type of r.name is, though because of the .id part we know we must be working with objects rather than strings in this case.

    If r.name is a single object, like { id = "foo" }, the [*] operator will first convert it to a single-element list [{ id = "foo" }]. It will then visit each element of that list (in this case, the one element) and the .id part will return the value of its id attribute, giving the result ["foo"]. length(["foo"]) == 1.

    If r.name is instead a list of objects, like [{ id = "foo" }, { id = "bar" }], the conversion to list isn’t needed (it’s already a list) and so the [*] operator will skip straight to accessing .id on each of the elements of the list, giving the result ["foo", "bar"] in this case, and thus the length would be 2.

    If you intend to pass the result to length(...) anyway, there’s never any difference between [*] and [*].id because no matter how many attribute accesses you put after [*] the result will always have the same number of elements.

With all of this said, if you know that your r.name is a list anyway, there’s no reason to do anything other than length(r.name) to get its length. The [*] operator will always produce a list with the same number of elements as the input, regardless of what attributes you access (if any at all), so using it with a list inside length adds nothing and is redundant.

However, none of these things are deprecated or undefined: they are all natural consequences of the defined behaviors of the individual operations, but these particular combinations of those operations isn’t very useful. As an analogy, consider that n and n + 0 are both the same value as long as n is a number: the second one isn’t wrong, but it’s stylistically weird to add additional notation that doesn’t change the meaning.

Thanks for the deep dive.
To clarify, my use case, I was thinking about some kind of resource with a count.
so r.name is a list and I will get the same value for the length() in every case.
I think I’ve settled on using length(r.name[*]) for clarity’s sake since

  1. as you mentioned, it’s not immediately clear whether r.name is a list or a string just from looking.
  2. if I use the longer form of length(r.name[*].id), I’m getting the same information but I’m not really interested in the id and there may be cases where there is no id attribute or it has some other name, so using r.name[*] seems to make for more uniform code.