An array library for Guile, inspired by the rank conjunction from the array language J.

Daniel Llorens 1a58d5db85 Fix uses of Guile array functions accepted for 2.2 1 year ago
ploy 1a58d5db85 Fix uses of Guile array functions accepted for 2.2 1 year ago
test 1a58d5db85 Fix uses of Guile array functions accepted for 2.2 1 year ago
LICENSE 3eb5b877dd Include copy of license 4 years ago
README 1a58d5db85 Fix uses of Guile array functions accepted for 2.2 1 year ago
TODO aff9747c1e Rely on array primitives in Guile's lloda-array-support branch 2 years ago
test-me.scm 6b5718a261 Some demo code for functions in (ploy slices) 4 years ago

README


guile-ploy is an array library for Guile. Guile's array functions are low-level
and barebones. If you need basic stuff such as a reshape function or a usable
subscript operation, you'll find them here.

guile-ploy doesn't use macros (other than some assert macros that you don't need
to use), doesn't use GOOPS, and it's all Guile Scheme [1]. This is not by design,
but perhaps a sign of immaturity.

[1] guile-ploy requires a version of Guile that provides the functions
array-cell-ref, array-slice, array-cell-set! and array-slice-for-each. These
were added to Guile in 2.2 (2.1.6).

A (slower) replacement for these functions is commented out in ploy/basic.scm,
in case you want to try the library without rebuiliding Guile.

What follows is a description of the library itself. If you aren't interested in
internals you can look up examples of use in test/test-*.scm.


0. (ploy basic)

(ploy basic) Some definitions to bootstrap the library.


1. array-map/frame! in (ploy ploy)

This is a version of array-map! that is able to iterate over array slices
(cells), not just over array elements. Now that (array-slice-for-each) is
available, this has become a thin wrapper that handles rank-0 arrays and
return values, since (array-slice-for-each), just as the other Guile array
functions, operates exclusively by side effect.


2. make-verb in (ploy ploy)

Define a new 'verb', a type of function that applies over arrays and knows the
rank of its arguments. The name is from the J language.


3. nested-op-frames in (ploy ploy)

array-map/frame! needs the frames of all its arguments to match
exactly. nested-op-frames is a rank extension / rank matching mechanism, modeled
after J. This is so one can do obvious things like

(+verb 1 #(1 2 3 4)) -> #(2 3 4 5)
(+verb #(1 2) #(2 4)) -> #(3 6)

This is similar to 'broadcasting' in other languages/libraries, like
Numpy. There's a good explanation in

http://www.jsoftware.com/help/jforc/loopless_code_i_verbs_have_r.htm#_Toc191734341

nested-op-frames also supports the 'rank conjunction' (w/rank) which can be used
to write the equivalent of nested loops, as explained in that link.

This matching of ranks is done using shared arrays, with no copies.


4. ply, ply/type in (ploy ploy)

This is an interface to the functionality above that takes also regular Scheme
functions, asigning rank 0 to all of their arguments. So you can do

(ply + 1 #(1 2 3 4)) -> #(2 3 4 5)

The result will have the same array type as the first argument. ply/type allows
you to give an explicit output type.

The verb type includes an oshape field that is used to compute the shape of the
output cell. If this is available, it's used to preallocate the whole
destination array. If the oshape is #f, the result is first computed as an
array-of-arrays and then 'collapsed'.

A difference with J is that the output cells must all have the same shape; no
padding is done. Therefore, it should be possible in principle to compute the
first cell, then use its shape for the whole output array. If the output rank is
0, collapse is not needed, since the array-of-whatever is already the result we
seek.


5. (from A . i) and (amend! A val . i) in (ploy ploy)

This is a rectangular selection/assignment operator. The semantics are after
APL. Each element of i is an array of indices into the corresponding axis of A,
and the shape of the result is the concatenation of the shapes of all the
i. This is rather like Octave's or Numpy's subscripting, except that the
arguments can be of any rank.

It is a regular Scheme function, not a verb as defined above.

There are optimizations for 'J-vectors', (like a:b:c in Octave or Numpy slices),
just plain integers, or for taking a full axis at once. These are used to avoid
copying the result when possible. Array indices always cause a copy to be made.

In Numpy or J, negative indices are interpreted as indices from the end of the
axis. This cannot work with general Guile arrays, where the lower bounds can
(unfortunately, since I don't think it's useful) be different from 0. The Octave
feature of using 'end' for the last index of the axis could be replicated for
(from) through the use of macros.

There are examples of use in test-ploy.scm.


6. ravel, reshape in (ploy ploy)

This is the reshape function of every array language. The semantics are a mix of
J and Octave. It clips or repeats, like J but unlike Octave, but it unravels its
arguments, like Octave but unlike J. Unraveling is in C order.

It does some effort to avoid copying, but I might not have covered every case.

Like (from A . i), these are regular Scheme functions, not verbs.


7. cat, icat in (ploy cat)

To concatenate arrays over arbitrary axes. Like Octave's cat. (cat) concatenates
over a given axis, while (icat) (= item-cat) concatenates items of the given
rank.


8. folda, foldb in (ploy reduce)

These are very simple reductions based on J over (/). They operate over the
items of the array:

(folda + 0 #2((1 3 2) (10 20 30))) => #2((11 23 32)) .

folda and foldb are semantically identical. The difference between them is that
folda performs the fold 'above' the frame of its array op, while foldb does it
'below'. In the example above, the fold is along rows, and the array op is along
columns. In folda, the outer loop is over rows and the inner loop over columns;
in foldb it's the opposite. See also 5.2 of the Repa paper.

They perform very differently depending on the shapes of the arguments, although
a lot of this difference is just an artifact of how crude the implementation is
(e.g. the apply-map-from burden).

I'd prefer the order to be left unspecified, so that fold would decide by itself
whether to fold above, below or even in the middle, if the frame allows. This is
similar to how ply is supposed to work: it resolves to an array-map! call, and
in principle traversal order is decided there.

This functions are a bit of an experiment, I'm not sure they should work as they
do.


9. as-array in (ploy as-array)

A function to convert array-like objects into arrays. It's a generalization of
Guile's list->array or list->typed-array, similar to Numpy's asarray. Array-like
objects are nested vectors, nested lists, lists of vectors, arrays of lists, and
so on.

This function is needed in two typical cases. One is when interfacing with other
Scheme code that has been written for those array-like types. (Look in
test/test-as-array for ways to do the converse operation.) The other is when
interfacing with other languages/libraries that expect arrays to use a specific
memory layout (e.g. C) and do not accept general strides.


10. (ploy slices)

This module contains a disorganized list of other array functions: out explode
raze reverse. tile sort. every. any. index are some that I use
often. Unfortunately most of these don't work as verbs but as regular Scheme
functions, since the ply/verb as implemented here mechanism is either too slow,
too inconvenient, or not well-thought enough to handle these functions that
generally apply over full arrays (would have rank -1 in J). This is a sad
situation and I hope to improve on it in the future.


That's it! Thanks for reading.

- lloda