- Feature Name: array_private_classes
- Start Date: 2022-01-08
- RFC PR: (leave this empty)
- Pony Issue: (leave this empty)
Rendered RFC: https://github.com/pmetras/rfcs/blob/rfc-array/text/0000-array-private-classes.md
Collection classes should not expose internal classes through iterators functions.
Following information hiding design principle, the builtin classes of collection
data structures must not be made visible through iterator functions like ArrayKeys,
ArrayValues and ArrayPairs. These classes can be made private as they are
only used as return types for Array functions keys, values and pairs.
The return values for these functions are changed to the more general interface
Iterator.
A new interface CollectionIterator is defined to allow for rewindable iterators,
like it is the case for Array values.
This design principle is applied to the other collection classes that expose internals too like:
ListMap- persistent
Map Vec- persistent
Vec SetItertools
This is a breaking change for collections' client code that use now internal classes but a search on Github repositories shows that the impact should be limited.
This change brings:
- Applying the design principle of hiding implementation details but offer a general and stable interface. Returning interfaces instead of concrete classes allows changing the implementation. Usually, one must return the most general type that fullfils the contract of the function (in the case of the functions discussed in this RFC, iteration).
- Collections' functions
keys,valuesandpairsdefinitions are made more general. Iterators implementation details are not public. Internal classes used by implementation like*Keys,*Valuesand*Pairsare now opaque data types. Generally, when using these collection classes, clients are not interested by the iterators implementation, but by the types these iterators return and that is provided by the generic parameters. - The generic return signature of these 3 iterating functions is simpler to understand for clients of collection classes.
- Reduces the number of public classes in the standard library by hiding 18
specialised classes (iterators implementations) of which 3 are from the
builtinmodule. - The interface
CollectionIteratoris added to create rewindable iterators (can be re-start from first value).
This change remains compatible with the existing code base but for client code
that is directly using the classes *Keys, *Values and *Pairs. A search on
Github shows that the impact is very limited.
To quote Antoine de Saint-Exupéry:
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
Currently the API makes a guarantee that the return value of each of these functions is a specific concrete class with a specific public name. That particular guarantee does not actually serve any practical needs of users (except, arguably, the virtual calls caveat that was mentioned on Github discussion). If it is not helpful to users in practice, it can be removed. And if it can be removed, then it should be removed, in the name of taking another step toward stabilizing the Pony standard library's API.
It's worth mentioning that if this change was being debated in reverse (the current standard library had minimal interfaces for the return values, and somebody proposed to make them public concrete classes instead, because they found some tangible benefit in doing so), then it would not be a breaking change at all, because the new proposed type would be a subtype of the existing return types.
It'll always be easier to get more specific with these return types than more general. So we should default to offering the most general return type that is still useful to users, and ratchet down to more specific subtypes as needed (as tangible use cases for more specific return types are discovered). Ratcheting down will always be possible without a breaking change, but ratcheting up will be a breaking change each time. It's best to make such "ratchet up" breaking changes like this RFC before we reach Pony 1.0.0 as part of a general effort to make Pony and its standard library stable.
Iterating functions in collections keys, values and pairs are changed to
return Iterator and the classes that implement these iterators are made private.
Here are the full implementation of these functions for the Array class (changes
in other collection classes are identical).
As the function values of class Array uses an iterator with a rewind function
that is not part of the Iterator interface, a new interface CollectionIterator
is added to enable creation of rewindable iterators.
fun keys(): CollectionIterator[USize]^ =>
"""
Return an iterator over the indices in the array.
"""
_ArrayKeys[A, this->Array[A]](this)
fun values(): CollectionIterator[this->A]^ =>
"""
Return an iterator over the values in the array.
"""
_ArrayValues[A, this->Array[A]](this)
fun pairs(): CollectionIterator[(USize, this->A)]^ =>
"""
Return an iterator over the (index, value) pairs in the array.
"""
_ArrayPairs[A, this->Array[A]](this)Note: To remain consistent with Array behaviour, functions keys and pairs
will return a CollectionIterator.
interface CollectionIterator[A] is Iterator[A]
"""
A `CollectionIterator` is an iterator that can be rewinded, that is start
again from first item. The data structure being iterated on can't change the
order it return iterated items.
"""
fun has_next(): Bool
"""
Return `true` when function `next` can be called to get next iteration item.
"""
fun ref next(): A ?
"""
Return the next item of the iteration or an error in case there are no other
items. A previous call to `has_next` check if we can continue iteration.
"""
fun ref rewind(): Iterator[A]^
"""
Start the iterator over again from the beginning.
"""The code of the standard library is adapted to remove use of these now private classes, mainly in tests. Here are the files that must be changed:
packages/builtin/array.ponyas shown abovepackages/itertools/iter.ponyin functioncyclepackages/collections/heap.ponyin functionvaluespackages/collection/builtin/_test.ponyin class_TestArrayValuesRewindpackages/collections/list.ponypackages/collections/map.ponypackages/collections/persistent/map.ponypackages/collections/persistent/vec.ponypackages/collections/set.ponytest/libponyc/util.ccto change the name of the class to_ArrayValues
In order to judge how the API becomes simpler to understand for clients of the
collections classes, here are the changes in the functions' signatures. The -
line shows the old signature while the + one is the new:
// Array
- fun keys(): ArrayKeys[A, this->Array[A]]^ =>
+ fun keys(): Iterator[USize]^ =>
- fun values(): ArrayValues[A, this->Array[A]]^ =>
+ fun values(): CollectionIterator[this->A]^ =>
- fun pairs(): ArrayPairs[A, this->Array[A]]^ =>
+ fun pairs(): Iterator[(USize, this->A)]^ =>
// Heap
- fun values(): ArrayValues[A, this->Array[A]]^ =>
+ fun values(): Iterator[this->A]^ =>
// List
- fun nodes(): ListNodes[A, this->ListNode[A]]^ =>
+ fun nodes(): Iterator[this->ListNode[A]]^ =>
- fun rnodes(): ListNodes[A, this->ListNode[A]]^ =>
+ fun rnodes(): Iterator[this->ListNode[A]]^ =>
- fun values(): ListValues[A, this->ListNode[A]]^ =>
+ fun values(): Iterator[this->A]^ =>
- fun rvalues(): ListValues[A, this->ListNode[A]]^ =>
+ fun rvalues(): Iterator[this->A]^ =>
// Map
- fun keys(): MapKeys[K, V, H, this->HashMap[K, V, H]]^ =>
+ fun keys(): Iterator[this->K]^ =>
- fun values(): MapValues[K, V, H, this->HashMap[K, V, H]]^ =>
+ fun values(): Iterator[this->V]^ =>
- fun pairs(): MapPairs[K, V, H, this->HashMap[K, V, H]]^ =>
+ fun pairs(): Iterator[(this->K, this->V)]^ =>
// Persistent Map
- fun val keys(): MapKeys[K, V, H] =>
+ fun val keys(): Iterator[K] =>
- fun val values(): MapValues[K, V, H] =>
+ fun val values(): Iterator[V] =>
- fun val pairs(): MapPairs[K, V, H] =>
+ fun val pairs(): Iterator[(K, V)] =>
// Persistent Vec
- fun val keys(): VecKeys[A]^ =>
+ fun val keys(): Iterator[USize]^ =>
- fun val values(): VecValues[A]^ =>
+ fun val values(): Iterator[A]^ =>
- fun val pairs(): VecPairs[A]^ =>
+ fun val pairs(): Iterator[(USize, A)]^ =>
// Set
- fun values(): SetValues[A, H, this->HashSet[A, H]]^ =>
+ fun values(): Iterator[this->A]^ =>For instance, in Array
- fun keys(): ArrayKeys[A, this->Array[A]]^ =>
+ fun keys(): Iterator[USize]^ =>now the client user knows now that she gets an iterator over USize.
Another more complex example with Map,
- fun keys(): MapKeys[K, V, H, this->HashMap[K, V, H]]^ =>
+ fun keys(): Iterator[this->K]^ =>
- fun values(): MapValues[K, V, H, this->HashMap[K, V, H]]^ =>
+ fun values(): Iterator[this->V]^ =>we see that we iterate over keys (K type) or values (V). The initial signature
gave complex generic classes that the user has to find in the documentation to
understand that they are iterators, and then dive deeper to understand on what
they iterate.
This change keeps the code compatible in the vast majority of cases. When client
classes are defining objects of these now private types, the reason is usually
to get access to the function rewind that was not defined in Iterator. By
adding the interface CollectionIterator, client code can easily be adapted,
replacing ArrayValues[A] by CollectionIterator[A].
Also, client code generally uses these functions to iterate on the returned types and does not try to access the iterator directly but is interested by the iterated items. When client code refers to the iterator type, that's generally useless and the code can be rewritten to be made shorter and more future proof.
A search on Github Pony code
finds 24 files using the class ArrayValues, of which 6 are copies of array.pony file.
For instance, in xml2xpath.pony, the code can be changed from
fun values(): ArrayValues[Xml2node, this->Array[Xml2node]]^ ? =>
if (allocated) then
ArrayValues[Xml2node, this->Array[Xml2node]](nodearray)
else
error
endto
fun values(): CollectionIterator[Xml2node]^ ? =>
if (allocated) then
nodearray.values()
else
error
endIn this sample, the developer was not really concerned by the type of the iterator
but that the values function must return an CollectionIterator over Xml2node.
The new version makes the code simpler to understand.
This change in array.pony and other collections will break such code but it
can be easily adapted to use the new API. And it will make the standard library
easier to learn by reducing the number of public types.
Pony tests must continue to pass. No additional tests are need as after review the existing coverage in Pony standard library tests is sufficient
Will break any existing code that uses any of the classes that are currently public and will be made private by this RFC.
-
Stay as is. Continue the discussion on Zulip.
-
Update the existing concrete classes to include
rewindwhere needed. -
Instead of defining a new type of iterator with the
CollectionIteratorinterface, we can consider only the rewindable part of it in anRewindableinterface:
interface Rewindable[A]
fun ref rewind(): A^
"""
Rewind the type `A`.
"""Then one can create a rewindable iterator by combining these two interfaces into a intersection type:
type CollectionIterator[A] is (Iterator[A] & Rewindable[Iterator[A]])Perhaps, a more general interface name instead of Rewindable would be Resetable
to define the traits of a type that can be reset to its initial state, as in the
case of iterators reset == rewind.
The rewindable type can be used with other types than Iterator, like a data
structure that would implement a rewindable property. This alternative was
put aside to
prevent name colisions in builtin with user-named types.
- Do nothing: return public concrete classes.
- Update the existing return classes and add
rewind. - Add a
CollectionIteratorinterface for collections. - Create intersection type by defining a
Rewindableinterface and combine withIterator. - Change
Iteratorto addrewind.
| Justification / Alternative # | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| a. Simpler stdlib API | --- | --- | + | +++ | +++ |
| b. Simpler function signatures | --- | --- | + | +++ | +++ |
| c. Stay compatible with existing stdlib | +++ | +++ | + | +++ | + |
| d. Evolutive | --- | + | +++ | +++ | + |
| e. No impact on compiler | +++ | +++ | - | +++ | - |
| f. No impact on performance | +++ | +++ | +++ | +++ | +++ |
| g. Limit stdlib pollution with interfaces | +++ | +++ | + | + | +++ |
| g. Limit stdlib pollution with classes | --- | --- | +++ | ++ | +++ |
- Must analyze how
RangeandReversewould be impacted if defined asCollectionIterator. Particularly, impact on existing client code. - Possible candidates to be analyzed:
StringBytes,StringRunes,ByteSeqIterand probably others will be found in stdlib code. - Understand why the intersection type proposal is not considered as it seems to provide a better flexibility and is easier to understand.