sort([x, y, Z, a1, a1]);
[Z, a1, a1, x, y]
That approach does not work if there are indexed names in the list.
sort([x, y, Z, a[1], a[2]]);
[x, y, a[1], a[2], Z]
The elements are sorted, but by machine address rather than alphabetically.
This can be shown using the addressof command:
map(addressof,%);
[135350720, 141574716, 143418428, 143418444, 143451544]
The explanation can be found from a careful reading of the sort
help page. A list of symbols are, by default, sorted
lexically. However, an indexed name is not a symbol.
Consequently, the list is sorted by machine address,
which is session dependent and hence not repeatable.
SortNames[1] := proc(names :: list)
sort( names
, (a,b) -> convert(a,string) <= convert(b,string)
);
end proc:
SortNames[1]( [x, y, Z, a[1], a[2]] );
[ Z, a[1], a[2], x, y ]
Note the use of the inequality `<=' in the comparison
rather than strict inequality, `<'.
That prevents an unnecessary swap if the strings are equal,
and achieves a stable sorting, that is, one in which
equal elements maintain their original order.
This procedure has two problems. First, it is rather slow, at least in comparison to later techniques. Second, because they map to the same string, it cannot distinguish the indexed variable x[1] and the symbol `x[1]`. We can correct this latter problem by using sprintf with the %a format string to convert names to strings:
SortNames[2] := proc(names :: list)
sort( names
, (a,b) -> sprintf("%a",a) <= sprintf("%a",b)
);
end proc:
SortNames[2]( [x, y, Z, `a[1]`, a[1], a[2], `b[1]` ] );
[Z, `a[1]`, `b[1]`, a[1], a[2], x, y]
Here is the basic routine
SortNames[3] := proc(names :: list)
local n;
map(attributes, sort([seq](setattribute(sprintf("%a",n),n)
, n = names)
, 'string'));
end proc:
If you have not read the previously mentioned
article this may need a bit of explanation.
The [seq](...) expression creates a list of strings,
using sprintf as previously described.
As each string is created it is passed to setattribute
which assigns the original name as the attribute to the string.
The resulting list of strings is then sorted lexically.
Finally the attributes of the sorted strings are extracted
and returned.
Because the attributes are the original names of the strings,
the returned list is the sorted list of names.
This procedure has two flaws: one minor, one major.
The minor flaw is that it has global side-effects due to the way that Maple handles strings. Each unique string in Maple exists in memory in one location. Adding an attribute to a string does not change this location. Calling setattribute replaces any preexisting attributes, so if some other procedure was using attributes with strings, and one of the strings matched a name passed to this procedure, its attribute would be changed.
The major flaw is subtle but potentially fatal. Suppose two distinct names mapped to the same string. When the attributes are extracted from the sorted strings, both strings are mapped to the same name, the second one in the original list. The returned list of names, then, does not contain one of the original names. Using sprintf rather than convert/string eliminates some of the collisions, however, it is still possible for two names to map to the same string. A simple example is escaped local variables, which can be readily generated with convert/local:
lst := map(convert, [a,a], `local`):
convert(lst, set);
{a,a}
To demonstrate that this really occurs
convert the sorted list to a set.
convert(Sort[3](lst), set);
{a}
lst := [1,2]:
lst1 := setattribute(lst,1):
lst2 := setattribute(lst,2):
lst3 := setattribute(lst,2):
map(evalb, [lst1=lst2, lst2=lst3]);
[ false, true ]
Normally, enclosing elements of a list in lists
requires providing a user-defined order function to sort
to unpack and compare them, which reduces the advantage of this technique.
However, sort takes the optional symbol lexorder[n]
which tells it to lexically compare the n'th element of each sublist.
The following procedure does just that, using n=1.
SortNames[4] := proc(names :: list)
local n;
map(attributes, sort([seq](setattribute([sprintf]("%a",n),n)
, n = names)
, 'lexorder'[1]));
end proc:
Demonstrate that this fixes the collision problem.
convert(SortNames[4](lst), set);
{a,a}
SortNames[5] := proc(names :: list)
local n;
map(attributes, sort([seq](setattribute(convert(n,'`local`'),n)
, n = names)));
end proc:
convert(SortNames[5](lst), set);
{a,a}
$ tmaple sort-names.mpl
Compare performance of sorting names.
Each test sorts the same list of 50 names of 5 characters 500 times.
time (s) used (MB) alloc (MB) gctimes proc
-------- --------- ---------- ------- ----
2.28 54.70 36.12 2 SortNames[1]
3.73 47.19 35.93 2 SortNames[2]
0.40 4.90 3.44 1 SortNames[3]
0.46 6.25 3.50 1 SortNames[4]
0.60 10.63 6.56 1 SortNames[5]
The table was generated using custom tools
for accurately and repeatably measuring the time
and memory usage of Maple procedures.
They shall be the subject of another article.