foreach.inc — array "walker" macro

the 'foreach.inc' file provides the Foreach() macro for the relatively common task of iterating an array and processing the current element in a macro. usually this means writing #while() or #for() loops, nested for multi-dimensional arrays. many loops can now be replaced with a simple call to Foreach(). for instance, given some array 'A' and an associated macro 'foo', it can be as simple as:


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

the 'dictionary {}' contains all the variable, user-defined "stuff". there are several key/value pairs to adjust the macro's behaviour, but only one, the name of your macro (aka the "payload"), is required.

Foreach() 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.

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 "visible" identifiers: one macro, one variable. there are however others, and all visible identifiers except Foreach() are prefixed 'fore_'.

fore_debug

optionally declare this boolean value to make Foreach() print some extra information, including a summary of the macro's arguments. 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. it returns (expands to) nothing.

as mentioned above, the dictionary requires a '.Macro' key – there is no default. all other keys are given "sensible" 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 each element in the '.From' dimension, also visit every 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 a value ("void"). all payload macros are called with two or more arguments, depending on a combination of '.Extra', '.Indices' and '.Arg', and of course '.Walk'.
payload macro signatures
macro arguments extra arg walk indices comment
(first,elem) no no 0, 1, 2 no 'first' is the current '.From' index
'elem' is the current array element
(first,elem,arg) yes yes 0, 1, 2 no 'arg' is always the last argument
(first,last,elem) yes no 1, 2 no 'last' is the current '.To' index
(first,last,elem,arg) yes yes 1, 2 no
(first[,..,last],elem) yes no 1, 2 yes arguments for all dimensions of array
(first[,..,last],elem,arg) yes yes 1, 2 yes
.Arg: "name"
'.Arg's role is to provide the (string) name of a dictionary to the payload macro when it gets called. the key requires the '.Extra' flag, and it does not have a default. see '.Macro' above.
.Indices: t/f
this flag controls which of the array indices will be passed as arguments to the payload macro. when enabled, all the array's dimensions indices are included in the payload's parameters, in order. otherwise the '.From' dimension index, and perhaps '.To', get used. '.Indices' can be used in conjunction with '.Extra' and a '.Walk' mode only. default 'false', see '.Macro' above.
.Boolean: t/f
when enabled indicates that the payload macro returns a boolean value; ie a zero return means false, any other (numeric) value means true. default 'false'.
.Strict: t/f
this flag controls whether Foreach() will terminate immediately when a 'bool' type payload macro returns 'false'. the default is 'false', that is, keep running.
note 'void' type payloads can use #break to terminate early, irrespective of '.Strict'. there are, however, two differences. first, a #break terminates one run of the payload, not Foreach() as '.Strict' does. second, there is no corresponding "break after call" message.
.Extra: t/f
this flag indicates a changed payload macro signature when enabled, see '.Macro' above. default 'false'.
.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, own key names should be captialised, like the above.

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

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. further to the functionality demonstrated here, the included 'fore_demo.pov' scene contains examples covering the basic (usage) options.

the following array is used in all examples below:


  #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 simple ('void' type) payload macro is used, which displays the values it is passed.
the default walk is '0', so a "minimalist" call will do:


  #macro foo(i_,n_)
    #debug concat("foo i = ",str(i_,0,0),", n = ",str(n_,0,0),".\n")
  #end

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

  -----[Foreach]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: no
             first dim: 1
              n'th dim: 1
         payload macro: foo
      signature change: no
           all indices: no
           payload arg: no
          returns bool: no
      break on 'false': no
  foo i = 0, n = 1.
  foo i = 1, n = 25.
  ------------------------------------------------------------------------

writing the dictionary inline as above is a very convenient way of calling the macro, if is to be used but once. otoh, declaring a dictionary is more convenient for reuse, as often only one or two of the keys will need changing:


  #declare D = dictionary {
    .Macro:   "foo",
    .Walk:    1,
    .From:    2,
    .To:      4,
    .Verbose: on
  };

  Foreach(A, D)

  #declare D.Walk = 2;

  Foreach(A, D)

  -----[Foreach]----------------------------------------------------------
         walk n'th dim: yes
     walk thru to n'th: no
             first dim: 2
              n'th dim: 4
         payload macro: foo
      signature change: no
           all indices: no
           payload arg: no
          returns bool: no
      break on 'false': no
  foo i = 0, n = 1.
  foo i = 0, n = 4.
  foo i = 1, n = 13.
  foo i = 1, n = 16.
  ------------------------------------------------------------------------
  -----[Foreach]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 2
              n'th dim: 4
         payload macro: foo
      signature change: no
           all indices: no
           payload arg: no
          returns bool: no
      break on 'false': no
  foo i = 0, n = 1.
  foo i = 0, n = 4.
  foo i = 0, n = 7.
  foo i = 0, n = 10.
  foo i = 1, n = 13.
  foo i = 1, n = 16.
  foo i = 1, n = 19.
  foo i = 1, n = 22.
  ------------------------------------------------------------------------

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)
            foo(i1, A[i1][i2][i3][i4][i5])
          #end
        #end
      #end
    #end
  #end

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

  foo i = 0, n = 1.
  foo i = 0, n = 2.
    ...
  foo i = 1, n = 47.
  foo i = 1, n = 48.
  -----[Foreach]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 1
              n'th dim: 5
         payload macro: foo
      signature change: no
           all indices: no
           payload arg: no
          returns bool: no
      break on 'false': no
  foo i = 0, n = 1.
  foo i = 0, n = 2.
    ...
  foo i = 1, n = 47.
  foo i = 1, n = 48.
  ------------------------------------------------------------------------

the '.Walk' examples showed that in many cases the payload macro does not need to know the exact location of the 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]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 1
              n'th dim: 3
         payload macro: m_2arg
      signature change: no
           all indices: no
           payload arg: no
          returns bool: 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]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 1
              n'th dim: 3
         payload macro: m_3arg
      signature change: yes
           all indices: no
           payload arg: no
          returns bool: 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]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 1
              n'th dim: 3
         payload macro: m_6arg
      signature change: yes
           all indices: yes
           payload arg: no
          returns bool: 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 that sense a payload using this option becomes much like a "traditional" callback in other languages; again, the '.Extra' flag and a non-default '.Walk' mode are required.
the example demonstrates this "IO mechanism" with a sum() type macro that can be switched to double its result:


  #macro m_sum(i_,j_,elem_,arg_)
    #if (arg_.double_)
      #local elem_ = 2 * elem_;
    #end
    #local arg_.sum_ = arg_.sum_ + elem_;
  #end

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

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

  Foreach(A, D)

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

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

  #declare D.Verbose = off;

  Foreach(A, D)

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

  -----[Foreach]----------------------------------------------------------
         walk n'th dim: no
     walk thru to n'th: yes
             first dim: 1
              n'th dim: 5
         payload macro: m_sum
      signature change: yes
           all indices: no
           payload arg: yes
          returns bool: no
      break on 'false': no
  ------------------------------------------------------------------------
  sum of elements is 1176.
  twice sum of elements is 2352.

bool