How can we represent C++ style iterators in Val? #742
Unanswered
kyouko-taiga
asked this question in
Q&A
Replies: 1 comment 3 replies
-
Just a few weeks ago I learned about Hylo and find it very interesting. What is the current state of realizing something like a "StringView" in Hylo as of today? |
Beta Was this translation helpful? Give feedback.
3 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
C++ uses iterators as one of its core idioms. A represents a position in a collection and provides access to that position. Because Val upholds value uniqueness, one may wonder how iterators can be expressed in Val or, more generally, any data structure that provides access to another object.
Here are my thoughts on this topic. I will use
std::string_view
as a running example.Disclaimer: I am not a C++ expert and am therefore likely to overly generalize. Feel free to correct me in the answers.
As projections:
The first approach uses remote parts and is the closest to what (AFAIU) one does in C++. It roughly matches how I implemented Iterator in this gist. More generally, I think this is how we should implement slices in Val. In that sense, a string view is just a slice of a string.
A lot of the heavy lifting is done the
Collection
trait, which operates exactly like Swift's.Here's a usage example. Consider the following C++ program:
In Val, we would write this:
In a nutshell, the
Collection
trait describes an index-based API to access the collection's elements. You usefirst_index
andend_index
to get the first and "past the end" positions andindex(after:)
to "advance". Finally, you have a subscript that projects elements at a given position.In these three methods and subscript roughly describe a forward iterator. Consider the following C++ program:
In Val, we would write this:
Because
StringView<a>
(a.k.a.Slice<a, String>
) is also a collection, it has the same API.The problem with this approach is that, as of now, it doesn't let you "store" a string view inside another collection. For example, consider the following C++ program:
In Val, the same program won't compile:
We may find a way to improve on our design to support dynamic collections of projections, but there are many hurdles we need to overcome. I won't delve into the details but feel free to ask if you are interested.
As pairs of indices:
The second approach solves the above storing issue by forgoing the remote part. Instead, you only store a pair of indices in the base collection and use it to project a slice whenever you want.
Let's go back over our examples. First we had this C++ program:
Now we write this in Val:
The second example was illustrating the use of the index API, which is kind of obvious now that we're using indices to represent string views. Let's instead focus on the third example, which was inexpressible in Val. In C++, we had:
Now in Val we can write this:
Now it works, but notice that we always need explicit access to the base collection (here,
s
) every time we want to read its contents. That's fine in this kind of small examples, but becomes quite inconvenient when want to pass things around in functions.For example, if you want to refactor the for loop in a different function, it will have to accept the base string as an additional argument. I call this the "context piping problem".
The "context piping problem" often occurs when we're trying to work on large data structures without reference semantics. For example, it's a common problem in React.js applications. I'm happy to present examples if you're interested.
A second problem (which David Sankel pointed out) is that nothing statically guarantees that we're using indices in the correct collection. For example, this program compiles:
We could solve this problem with path dependent types, as found in Scala, but beleive the complexity cost outweigh the benefit. I'm happy to expand on the details if you are interested.
What Rust can do but Val cannot
Rust can circumvent the storing problem using lifetime annotations. Basically, it has a way to expose the lifetime of a remove part to type system. To understand how that's different from Val, let's look at the first example again:
In Rust,
t
would have a type that preserves its relationship tos
explicitly. One could think of something likeStringView<let s>
(rather than justStringView<let>
). So now if we putt
inside another generic type, the relationship tos
is also preserved, as inArray<StringView<let s>>
for example.In Val, the relationship is kept implicit. That works because if
t
cannot escape, then it's easy for to just track all uses and projections ofs
in the scope; the compiler "sees" everything. Once we cross function boundaries, we either need whole program analysis or a richer type system.Other approaches
We have other ideas to work around the storing problem. Dave and Lucian have explored the use of wrapper types that bundle indices together with a remote access to the base collection. This trick essentially allows us to switch between the range and slice approach whenever we want.
Notice that the collection's API with which we've been playing already supports this approach, because
Slice<a, T>
is essentially a bundle combining a pair of indices in an instance ofT
with an access to that instance. So when we're writingcollection[in: range]
, we're creating a bundle.Another idea is to address the "context piping problem" with implicit parameters. In broad strokes, functions that require access to a whole can accept it as an implicit parameter so that it can be omitted at call sites. Here's how that could work in Val:
This approach is (or at least it was a couple of years ago) popular in React.js. AKAIK, it's also common in Scala.
I am personally getting increasingly convinced that implicit parameters are really interesting, but their design is not high on the priority list at the moment.
Beta Was this translation helpful? Give feedback.
All reactions