`foreach` is broken in `startsub`

0 votes
asked May 8 in Bug by JensHeinrich (120 points)

When using `foreach` inside a `startsub`-`endsub` block, only one loop iteration is done, which can be seen with the following example
```plantuml

@startuml
!$data=[
{"a": 1}
,
{"a": 2}
]

!procedure $test($_data)
!foreach $d in $_data
object o_iterate_outside_sub.o##$d.a
!endfor
!endprocedure

!startsub test

!foreach $d in $data
object o_interate_inside_sub.o##$d.a
!endfor

$test($data)
!endsub
@enduml
```

This example is also available as an editable example here

PNG showing the output (one element for the iteration inside the sub and two for the iteration outside the sub)
 

1 Answer

0 votes
answered May 9 by JimN (900 points)
selected May 14 by JensHeinrich
 
Best answer

Hello, I'm currently studying the implementation code for PlantUML and I do see that startsub-endsub block processing interferes with the way foreach tries to iterate, resulting in only the first pass of the foreach occurring and the loop itself being ignored. (The sub is actually trying to execute its block as its own script, but the foreach execution is still trying to loop back to its start on the original script, so its attempt isn't recognized by the sub block execution.  If that makes sense.)   I'm not clear on how this should be fixed, however.

I do like your example, though, as it shows a current workaround.  You demonstrated that the sub block is able to execute a foreach loop that is inside a procedure it calls.   I've confirmed that such a procedure can be inside the sub block itself.   So, by simply wrapping your inside foreach loop in a procedure and immediately calling it, you get the desired effect -- all within the sub block.

Here is a modified version of your example, removing the outer procedure and just focusing on what happens inside the sub block, demonstrates this:

@startuml
!$data=[
{"a": 1}
,
{"a": 2}
]

!startsub test

!procedure $test_inner($data)

  !foreach $d in $data
  object o_interate_inside_sub.o##$d.a
  !endfor

!endprocedure
$test_inner($data)

!endsub
@enduml

The results are seen here.
PlantUML diagram

I've confirmed that this sub block can be imported by an external file, resulting in the expected two objects.

Wrapping the foreach inside a procedure allows its looping to work correctly inside the sub block, just requiring 3 extra lines of code (removing my whitespace).   Ideally the foreach block would simply do this on its own, but for now, this is a workaround.

Regards,

JimN

commented May 9 by JimN (900 points)
edited May 10 by JimN

A couple more points.  

  1. I've confirmed that this wrapping procedure doesn't even need to be passed the $data as a parameter (unless you want to).  [However, for variables such as $data to be referenced from the sub block and to work when the sub block is imported by another file, the variable must be defined within the sub block.  So, in the example below, you'd want to move the $data array into the sub block, if you're importing it.  This requirements seems to be unrelated to the foreach loop issue.]
  2. This workaround only needs the extra 3 lines to wrap all the code inside the sub block.  So, if there are more than one foreach loop, you don't need to have a new procedure around each of them.   See this example here:
@startuml
!$data=[
{"a": 1}
,
{"a": 2}
]

!startsub test
!procedure $subwrapper()

  !foreach $d in $data
  object o_interate_inside_sub.o##$d.a
  !endfor

  !foreach $d in $data
  object o_second_inside_sub.z##$d.a
  !endfor

!endprocedure
$subwrapper()
!endsub
@enduml
commented May 14 by JensHeinrich (120 points)
  1. I've confirmed that this wrapping procedure doesn't even need to be passed the $data as a parameter (unless you want to).  [However, for variables such as $data to be referenced from the sub block and to work when the sub block is imported by another file, the variable must be defined within the sub block.  So, in the example below, you'd want to move the $data array into the sub block, if you're importing it.  This requirements seems to be unrelated to the foreach loop issue.]

 For my use-case I will keep it as a parameter, as I call it with different data sets

commented May 14 by JensHeinrich (120 points)
Technically this still is a work around and not a fix though
commented May 14 by JimN (900 points)

Yes, unfortunately, it isn't a fix.  I just had wanted to share the limited insights that I gained while exploring the implementation.
 

I'm a very new contributor to the PlantUML open source project.   I hope to eventually learn enough about the preprocessor to understand how this might be fixed.  Given that the foreach can work inside a procedure that is inside a sub block, looking at how that works may tell me enough about how foreach can be changed to do the same on its own.

Regards

...