Foreach() — array "walker" macro

the 'foreach.inc' file provides the Foreach() macro for the relatively common task of iterating through an array, and processing the current element using a macro.

one usually needs to write #for() or #while() loops, nested several deep, to process multi-dimensional arrays. the purpose of the "foreach" include is to replace many such loop constructs with simple calls to the Foreach() macro. for instance, given some array 'A' and an associated macro 'doXYZ', and relying on the code's defaults, you can write:


  Foreach(A, dictionary {.Macro: "doXYZ"})

the 'dictionary {}' contains all of the variable, user-defined "stuff". there are several key/value pairs provided to adjust the macro's behaviour, but only one, the name of your macro (aka the "payload"), is required. what said macro actually does is, as far as I can tell, limited only by the imagination (and the data!). the various examples shown merely scratch the surface.

another small ("fringe") benefit derives from using Foreach(): whether you add a couple of elements to your array, or delete ten, the supporting code (ie the payload) will, in all likelihood, never need adapting to the new number of elements.

the Foreach() macro works with all POV-Ray pre version 3.8 arrays of up to five dimensions. it also works for new-style, unsized arrays, for the first dimension; support for nested unsized arrays may be added when the need arises.

the code is written for the Persistence of Vision Raytracer, version 3.8 or later.

note – the code examples mostly use short, lowercase identifiers i_, j_ etc for index variables, with an underscore to avoid name clashes like 'x'. the current array element is always elem_. A and D are used for arrays and dictionaries, respectively.

foreach.inc

the file is a regular POV-Ray include, and stand-alone, ie does not depend on other includes. seen from the user's perspective, it provides two globally "visible" identifiers: one macro, one variable. there are however others, and all visible identifiers except Foreach() are prefixed 'fore_'.

fore_debug

this optional variable can be declared to make Foreach() print some extra information, including a header summarising the macro's arguments. fore_debug must be declared before calling the macro.


  #declare fore_debug = on;

note this means one additional line of "called some element" output per (payload) macro invocation.

Foreach(array, dictionary)

the macro can be used to process sized, one to five-dimensional arrays, and basic unsized arrays. Foreach() returns nothing, unless your 'void'-type payload macro returns some value. note – the choice of 'void', with hindsight, is very unfortunate because it is misleading; I have decided to stick with the term however, hoping this explanation will .. make good.

as mentioned above, the dictionary requires a '.Macro' key – it has no default. all other keys are given "sensible" default values when omitted, except '.Arg'.

.Walk: n
determines how elements in the array will be visited, there are three distinct modes:
  • 0 - the payload macro is called for every element in the '.From' dimension. '.To' need not be given, as it is not used.
  • 1 - for every element in the '.From' dimension, visit each element in the '.To' dimension.
  • 2 - visit all elements in the '.From' dimension through to the '.To' dimension, inclusive.
the default walk is '0'. note walks '1' and '2' are identical when the dimensions are immediate neighbours.
.From: n
selects the first, leftmost dimension to use. default '1'.
.To: n
specifies the last, rightmost dimension to use. defaults to the value of '.From'.
.Macro: "name"
the (string) name of a macro you supply. there are basically two types of payload macro: ones which return/expand to a boolean value, and ones which do not return such a value ("void"). all payload macros are called with two or more arguments ("parameters"), depending on a combination of '.Extra' and '.Indices', '.Arg', and '.Walk'.
payload macro signatures
macro arguments extra indices arg walk comment
(first,elem) no no no 0, 1, 2 'first' is the current '.From' index
'elem' is the current array element
(first,elem,arg) no no yes 0, 1, 2 'arg' is always the last argument
(first,last,elem) yes no no 1, 2 'last' is the current '.To' index
(first,last,elem,arg) yes no yes 1, 2
(first[,..,last],elem) yes yes no 1, 2 index parameters for all dimensions of array
(first[,..,last],elem,arg) yes yes yes 1, 2
note – modifying elem_ modifies the underlying array.
.Arg: "name"
'.Arg's role is to provide the (string) name of a dictionary to the payload macro when it gets called. the purpose of the dictionary is to enable a separate "channel of communication" in the payload; see '.Macro' above. this key does not have a default.
.Extra: t/f
when enabled, this flag indicates the addition of one (usually) or more array index arguments in the payload macro's signature; see '.Macro' above. default 'false'.
.Indices: t/f
this flag controls which of the array indices will be passed as arguments to the payload macro. when enabled, all of the array's dimensions indices are included as the payload's parameters, in order. without, the '.From' dimension index, and perhaps '.To', get used. '.Indices' can be used in conjunction with '.Extra' and a '.Walk' mode only, see '.Macro' above. default 'false'.
.Boolean: t/f
when enabled indicates that the payload macro returns a boolean type value, where a zero means 'false' and any other (numeric) value stands for 'true'; the POV-Ray built-in 'yes'/'no' etc keywords work too, of course. default disabled.
.Strict: t/f
this flag controls how Foreach() reacts when a '.Boolean' payload macro returns 'false'. when enabled, Foreach() halts execution of the payload immediately on a 'false' return, and an info message will be printed. when disabled, the default, execution of the payload continues.
note 'void' type payloads can use #break to terminate early, irrespective of '.Strict'. there are, however, two differences. first, a #break terminates the current run of the payload only, not Foreach() as '.Strict' does. second, there is no corresponding "break after call" message.
.Verbose: t/f
when enabled, Foreach() will print summary information to POV-Ray's debug stream. in addition, when a 'bool' type payload macro returns false, a "false return from" style message will be emitted. default 'false'.

it is ok to add your own keys to the dictionary. to avoid potential name clashes, your own key names ought to be capitalised, like the above.

when running Foreach(), a number of errors can occur. all usage error type messages are prefixed 'oops', with a couple of exceptions. when the supplied array is empty, or when the payload macro returns a value but the '.Boolean' flag is not enabled (or vice versa), the errors are caught by POV-Ray and you'll see "normal" messages.

lastly, every time Foreach() runs it leaves behind a file named 'parse_fore.tmp', in the current working directory. the code uses an adapted version of POV-Ray's Parse_String() macro (from 'strings.inc'). I recommend adding the path of a suitable "temp" directory (see 'fn_' in 'foreach.inc:fore_exec()').

(tutorial) examples

the snippets below show code and the corresponding outputs, using an "unofficial" 3.8.0.alpha version of POV-Ray. in addition to the functionality demonstrated here, the included 'fore_demo.pov' scene contains examples covering the basic (usage) options.

the same five-dimensional array is used in all examples below. it is as small as possible while still allowing .. varied examples; the "data" is simply the integer sequence from 1 to (2*2*2*2*3):


  #declare A = array [2][2][2][2][3] {
    {{{{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}}},
    {{{13, 14, 15}, {16, 17, 18}}, {{19, 20, 21}, {22, 23, 24}}}},
    {{{{25, 26, 27}, {28, 29, 30}}, {{31, 32, 33}, {34, 35, 36}}},
    {{{37, 38, 39}, {40, 41, 42}}, {{43, 44, 45}, {46, 47, 48}}}}
  };

to demonstrate the three walk modes, a one-line ('void' type) payload macro is used, which displays the values it is passed.
the default walk is '0' (visit all elements in '.From'), so a "minimalist" call will do:


  #macro m_echo(i_,elem_)
    #debug concat("m_echo i = ",str(i_,0,0),", elem = ",str(elem_,0,0),".\n")
  #end

  Foreach(A, dictionary {.Macro: "m_echo", .Verbose: on})

  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 1
     walk n'th dimension: no
    walk through to n'th: no
           payload macro: m_echo
             extra index: no
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_echo i = 0, elem = 1.
  m_echo i = 1, elem = 25.
  ------------------------------------------------------------------------

writing the dictionary inline as above is a very convenient way of calling the macro, if is to be used but once. on the other hand, declaring a dictionary is more convenient for reuse, as often only one or two of the keys will need changing. this snippet compares walk modes '1' and '2', using the first three dimensions of array:


  #declare D = dictionary {
    .Macro:   "m_echo",
    .Walk:    1,
    .To:      3,
    .Verbose: on
  };

  Foreach(A, D)

  #declare D.Walk = 2;

  Foreach(A, D)

  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 3
     walk n'th dimension: yes
    walk through to n'th: no
           payload macro: m_echo
             extra index: no
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_echo i = 0, elem = 1.
  m_echo i = 0, elem = 7.
  m_echo i = 1, elem = 25.
  m_echo i = 1, elem = 31.
  ------------------------------------------------------------------------
  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 3
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_echo
             extra index: no
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_echo i = 0, elem = 1.
  m_echo i = 0, elem = 7.
  m_echo i = 0, elem = 13.
  m_echo i = 0, elem = 19.
  m_echo i = 1, elem = 25.
  m_echo i = 1, elem = 31.
  m_echo i = 1, elem = 37.
  m_echo i = 1, elem = 43.
  ------------------------------------------------------------------------

the last '.Walk' mode example contrasts the "conventional way of doing" and the Foreach() equivalent:


  #for (i1, 0, dimension_size(A,1)-1)
    #for (i2, 0, dimension_size(A,2)-1)
      #for (i3, 0, dimension_size(A,3)-1)
        #for (i4, 0, dimension_size(A,4)-1)
          #for (i5, 0, dimension_size(A,5)-1)
            m_echo(i1, A[i1][i2][i3][i4][i5])
          #end
        #end
      #end
    #end
  #end

  Foreach(A, dictionary {
    .Macro:   "m_echo",
    .Walk:    2,
    .To:      5,
    .Verbose: on
  })

  m_echo i = 0, elem = 1.
  m_echo i = 0, elem = 2.
    ...
  m_echo i = 1, elem = 47.
  m_echo i = 1, elem = 48.
  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 5
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_echo
             extra index: no
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_echo i = 0, elem = 1.
  m_echo i = 0, elem = 2.
    ...
  m_echo i = 1, elem = 47.
  m_echo i = 1, elem = 48.
  ------------------------------------------------------------------------

the '.Walk' examples show that in many cases the payload macro does not need to know the exact location of an element in the array to "do its thing". when that knowledge becomes useful or necessary, you can use the '.Indices' option to have Foreach() add parameters, for the complete set of current indices, to the payload macro call. as mentioned above, '.Indices' requires both the '.Extra' flag and a '.Walk' mode other than the default.
walking the first three dimensions, and as before, the payloads simply echo their arguments:


  #macro m_2arg(i_,elem_)
    #debug concat("m_2arg i = ",str(i_,0,0),", elem = ",str(elem_,0,0),".\n")
  #end

  #macro m_3arg(i_,j_,elem_)
    #debug concat("m_3arg i = ",str(i_,0,0),", j = ",str(j_,0,0),
            ", elem = ",str(elem_,0,0),".\n")
  #end

  #macro m_6arg(i_,j_,k_,l_,m_,elem_)
    #debug concat("m_6arg i = ",str(i_,0,0),", j = ",str(j_,0,0),
            ", k = ",str(k_,0,0),", l = ",str(l_,0,0),
            ", m = ",str(m_,0,0),", elem = ",str(elem_,0,0),".\n")
  #end

  #declare D = dictionary {
    .Macro:   "m_2arg",
    .Walk:    2,
    .To:      3,
    .Verbose: on
  };

  Foreach(A, D)

  #declare D.Macro = "m_3arg";
  #declare D.Extra = on;

  Foreach(A, D)

  #declare D.Macro = "m_6arg";
  #declare D.Indices = on;

  Foreach(A, D)

  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 3
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_2arg
             extra index: no
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_2arg i = 0, elem = 1.
  m_2arg i = 0, elem = 7.
  m_2arg i = 0, elem = 13.
  m_2arg i = 0, elem = 19.
  m_2arg i = 1, elem = 25.
  m_2arg i = 1, elem = 31.
  m_2arg i = 1, elem = 37.
  m_2arg i = 1, elem = 43.
  ------------------------------------------------------------------------
  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 3
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_3arg
             extra index: yes
             all indices: no
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_3arg i = 0, j = 0, elem = 1.
  m_3arg i = 0, j = 1, elem = 7.
  m_3arg i = 0, j = 0, elem = 13.
  m_3arg i = 0, j = 1, elem = 19.
  m_3arg i = 1, j = 0, elem = 25.
  m_3arg i = 1, j = 1, elem = 31.
  m_3arg i = 1, j = 0, elem = 37.
  m_3arg i = 1, j = 1, elem = 43.
  ------------------------------------------------------------------------
  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 3
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_6arg
             extra index: yes
             all indices: yes
             payload arg: no
         returns boolean: no
        break on 'false': no
  m_6arg i = 0, j = 0, k = 0, l = 0, m = 0, elem = 1.
  m_6arg i = 0, j = 0, k = 1, l = 0, m = 0, elem = 7.
  m_6arg i = 0, j = 1, k = 0, l = 0, m = 0, elem = 13.
  m_6arg i = 0, j = 1, k = 1, l = 0, m = 0, elem = 19.
  m_6arg i = 1, j = 0, k = 0, l = 0, m = 0, elem = 25.
  m_6arg i = 1, j = 0, k = 1, l = 0, m = 0, elem = 31.
  m_6arg i = 1, j = 1, k = 0, l = 0, m = 0, elem = 37.
  m_6arg i = 1, j = 1, k = 1, l = 0, m = 0, elem = 43.
  ------------------------------------------------------------------------

the dictionary provided via the '.Arg' key is available for both input and output. in a sense a payload macro using this option can act much like a "traditional" callback in other computer languages.
the example demonstrates this "IO mechanism" with a payload that can be made to double its result:


  #macro m_add(i_,elem_,arg_)
    #local arg_.sum_ = arg_.sum_ + elem_ + (arg_.double_ ? elem_ : 0);
  #end

  #declare D = dictionary {
    .Macro:   "m_add",
    .Walk:    2,
    .To:      5,
    .Arg:     "R",
    .Verbose: on
  };

  #declare R = dictionary {.double_: off, .sum_: 0};

  Foreach(A, D)

  #debug concat("sum of elements is ",str(R.sum_,0,0),".\n")

  #declare D.Verbose = off;

  #declare R.double_ = on;
  #declare R.sum_ = 0;

  Foreach(A, D)

  #debug concat("twice sum of elements is ",str(R.sum_,0,0),".\n")

  -----[Foreach]----------------------------------------------------------
         first dimension: 1
          n'th dimension: 5
     walk n'th dimension: no
    walk through to n'th: yes
           payload macro: m_add
             extra index: no
             all indices: no
             payload arg: yes
         returns boolean: no
        break on 'false': no
  ------------------------------------------------------------------------
  sum of elements is 1176.
  twice sum of elements is 2352.

up to this point, none of the payload macros explicitly returned a value. the final tutorial example presents the other type macro, one returning a boolean value which, in this case, indicates whether the current array element divides by some number. on the first run, execution of Foreach() is not stopped when the payload returns 'false'. '.Strict' then is set for the second run, leading to an immediate halt. '.Verbose' is not used because of the many "false return from ..." messages otherwise.


  #macro m_divby(i_,elem_,arg_)
    #local r_ = (mod(elem_,arg_.n_) ? false : true);
    #if (r_)
      #debug concat(str(elem_,0,0)," divides by ",str(arg_.n_,0,0),".\n")
    #end
    r_
  #end

  #local D = dictionary {
    .Macro:   "m_divby",
    .Walk:    2,
    .To:      5,
    .Boolean: on,
    .Arg:     "R"
  };

  #declare R = dictionary {.n_: 11};

  Foreach(A, D)

  #debug "\n"

  #declare D.Strict = on;

  Foreach(A, D)

  11 divides by 11.
  22 divides by 11.
  33 divides by 11.
  44 divides by 11.

  break after call 'm_divby(0,a_[0][0][0][0][0],R)'.

the concluding (code) example is a complete copy'n'paste scene. it shows nested Foreach() use, and is based on a different data array from the previous snippets. the message text is deliberately obscured until rendered, for added fun. (can you work out the text before running the code?)


  #version 3.8;

  global_settings {assumed_gamma 1}

  #include "foreach.inc"
  #include "logo.inc"

  camera {
    location <3.25,0,-4.5>
    direction z
    right x * (4/3)
    up y
    angle 75
    look_at <3.25,0,0>
  }

  light_source {<-1.5,1,-1> * 10e3 color srgb 1 parallel}

  /* the data.
   * elem {line begin, scale, font index, array char data}.
   */
  #declare array_ = array [4] {
    array mixed {<.1, 1.6, 0>, .85, 2,
            array {70, 112, 116, 104, 101, 104, 110, 47, 49}},
    array mixed {<0, .2, 0>, .75, 0,
            array {109, 98, 102, 104, 36, 122, 121, 112, 118, 112}},
    array mixed {<0, -1, 0>, 1, 1,
            array {80, 80, 88, 48, 86, 102, 127}},
    array mixed {<0, -2, 0>, .75, 0,
            array {51, 47, 58, 49, 52, 50, 103, 115, 120, 113, 107, 57}}
  };

  #declare font_ = array [3] {"comic.ttf", "comicbd.ttf", "cour.ttf"};

  /* et voilĂ  */
  object {
    Povray_Logo_Prism
    texture {
      pigment {color rgb <1,1,0>}
      normal {wrinkles scale .2}
      finish {specular .4}
    }
    scale 1.5
    translate <5.3, 0, 0>
  }

  #macro m_ch(i_,elem_) chr(elem_ - i_) #end

  #macro m_text(i_,elem_)
    #local s_ = concat(Foreach(elem_[3], dictionary {.Macro: "m_ch"}));
    text {
      ttf font_[elem_[2]] s_, .1, 0
      texture {
        pigment {color rgb <1,1,0>}
        normal {dents scale .0005}
        finish {specular .8}
      }
      scale (<1,1,1> * elem_[1])
      translate elem_[0]
    }
  #end

  Foreach(array_, dictionary {.Macro: "m_text"})

"The End". enjoy.