Skip to content

Commit

Permalink
Implement double-ended iterators (#1)
Browse files Browse the repository at this point in the history
Implements iterators which can be traversed both forwards and backwards.

Tasks:
- [x] Add support for double-ended iterators
- [x] Make source iterators double-ended (e.g. repeat, once, over_array)
- [x] Make transformative iterators double-ended (where possible)
- [x] Document double-ended features in README
- [x] Add tests for double-ended features
  • Loading branch information
kdkasad committed May 14, 2024
2 parents 47b1627 + 028271e commit acb4a3e
Show file tree
Hide file tree
Showing 21 changed files with 500 additions and 39 deletions.
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ MODULES = \
chunked \
collect \
inspect \
zip
zip \
reverse

EXAMPLES = \
repeat_take \
Expand All @@ -53,7 +54,9 @@ EXAMPLES = \
sum \
inspect \
skip_take_while \
zip
zip \
reverse \
double_ended
EXAMPLES_BIN = $(addprefix examples/,$(EXAMPLES))

STATICLIB = lib$(NAME).a
Expand Down
111 changes: 84 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,35 +88,83 @@ If you don't like this, try Rust.
Iterators are created using the `citer_<iterator>(...)` functions.
These functions take parameters as necessary and return a pointer to a heap-allocated iterator.

Going forward, assume `it` is of the type `iterator_t *`, a pointer to an iterator.

To free an iterator, call `citer_free(it)`.
`citer_free(it)` should not free any memory which was not allocated by `citer_*()` functions.
This means that if you pass heap-allocated memory to iterator functions, you must free it yourself.
`citer_free(it)` will not free any memory which was not allocated by `citer_*()` functions.
This means that if you pass heap-allocated memory to iterator constructor functions, you must free it yourself.

Calling `citer_next(it)` will return the next item in the iterator, or `NULL` if the iterator is exhausted.
If the iterator is double-ended, `citer_next_back(it)` will return the next item from the back of the iterator.

#### Double-ended iterators

Some iterators can iterate both forwards and backwards.
These are called double-ended iterators.

The table of iterators below shows which iterators are double-ended.
To check at runtime if an iterator is double-ended, use `citer_is_double_ended(it)`.

Double-ended iterators should only return each item once.
In other words, `citer_next(it)` should not return the same item as `citer_next_back(it)`.
See the example below for a demonstration.

<details>
<summary>Example</summary>

Here is an example using an iterator over a small array:

#include <assert.h>
#include <stdio.h>
#include <citer.h>

int main() {
int arr[] = { 1, 2, 3, 4, 5 };
iterator_t *it = citer_over_array(arr, sizeof(int), 5);

assert(citer_next_back(it) == &arr[4]);
assert(citer_next(it) == &arr[0]);
assert(citer_next(it) == &arr[1]);
assert(citer_next(it) == &arr[2]);
assert(citer_next(it) == &arr[3]);
assert(citer_next(it) == NULL);

return 0;
}

As you can see, the iterator is exhausted after returning the 4th item,
because the 5th was already returned by a call to `citer_next_back()`.

</details>

### Implementing your own iterators

An iterator is very easy to implement.
Take a look in any of the files in the `src/` directory to see how they are implemented.
`repeat.c` is especially simple, and `map.c` is a good example of a very powerful iterator
which still doesn't require much code.
`repeat.c` is especially simple,
and `citer_map()` in `map.c` is a good example of a very powerful iterator which still doesn't require much code.

The `iterator_t` type is defined as follows:
```c
typedef void *(*citer_next_fn)(void *);
typedef void (*citer_free_data_fn)(void *);

typedef struct iterator_t {
void *data;
void *(*next)(void *data);
void (*free_data)(void *data);
citer_next_fn next;
citer_next_fn next_back;
citer_free_data_fn free_data;
} iterator_t;
```
The `data` field can be anything you want.
It is passed to the `next` function and is not used anywhere else (except `free_data()`).
It is passed to the `next` and `next_back` functions but is not used anywhere else (except `free_data()`).
The `next` function should return the next item of this iterator, or `NULL` if the iterator is exhausted.
The `next_back` function is the same as `next`,
but should return the next item from the back (i.e. end) of the iterator.
This is only implemented for double-ended iterators.
For single-ended iterators, the `next_back` field should be set to `NULL`.
The `free_data` function should deallocate any heap-allocated memory in the `data` field.
If `data` is a pointer to a struct, and that struct was heap-allocated when the iterator was created,
`free_data` should free that struct.
Expand All @@ -130,24 +178,30 @@ All iterators and functions below are prefixed with the name `citer_` to avoid c
### Iterators
| Iterator | Description |
| --- | --- |
| chain | Chains two iterators. Iterates over all items of the first, then all items of the second. |
| chunked | Iterates over N-item chunks of an iterator at a time. |
| enumerate | Enumerates the items of an iterator. Each new item is a `citer_enumerate_item_t` containing the index and the item. |
| filter | Filters items of an iterator using a predicate function. |
| flat_map | Maps each item of an iterator to an iterator, then iterates over the items of each result iterator consecutively. Equivalent to `citer_flatten(citer_map(it, fn))`. |
| flatten | Flattens an iterator of iterators into a single iterator. |
| inspect | Calls a callback function on each item of an iterator, without modifying the returned items. |
| map | Maps each item of an iterator using a callback function. |
| once | Iterator which returns a given item once. Equivalent to `citer_take(citer_repeat(item), 1)`. |
| over_array | Iterates over the items in an array. Returns a pointer to each item in the array as the item. |
| repeat | Iterator which repeatedly returns the same item. |
| skip | Skips the first N items of another iterator. |
| skip_while | Skips the items of another iterator until a given predicate function returns false. |
| take | Iterates over the first N items of another iterator. |
| take_while | Iterates over items of another iterator until a given predicate function returns false. |
| zip | Zips two iterators together, returning pairs of items, one from each input iterator. |
| Iterator | Double-ended | Description |
| --- | --- | --- |
| chain | I | Chains two iterators. Iterates over all items of the first, then all items of the second. |
| chunked | N | Iterates over N-item chunks of an iterator at a time. |
| enumerate | N | Enumerates the items of an iterator. Each new item is a `citer_enumerate_item_t` containing the index and the item. |
| filter | I | Filters items of an iterator using a predicate function. |
| flat_map | I | Maps each item of an iterator to an iterator, then iterates over the items of each result iterator consecutively. Equivalent to `citer_flatten(citer_map(it, fn))`. |
| flatten | I | Flattens an iterator of iterators into a single iterator. |
| inspect | I | Calls a callback function on each item of an iterator, without modifying the returned items. |
| map | I | Maps each item of an iterator using a callback function. |
| once | Y | Iterator which returns a given item once. Equivalent to `citer_take(citer_repeat(item), 1)`. |
| over_array | Y | Iterates over the items in an array. Returns a pointer to each item in the array as the item. |
| repeat | Y | Iterator which repeatedly returns the same item. |
| reverse | Y | Iterator which reverses a double-ended iterator. |
| skip | I | Skips the first N items of another iterator. |
| skip_while | N | Skips the items of another iterator until a given predicate function returns false. |
| take | N | Iterates over the first N items of another iterator. |
| take_while | N | Iterates over items of another iterator until a given predicate function returns false. |
| zip | N | Zips two iterators together, returning pairs of items, one from each input iterator. |
\*
Y: Yes,
N: No,
I: Inherited (i.e. double-ended if all input iterators are double-ended)
### Functions
Expand All @@ -162,7 +216,10 @@ All iterators and functions below are prefixed with the name `citer_` to avoid c
| fold | Accumulate all items of an iterator into a single value using a given function. |
| free | Frees (de-allocates) an iterator and its associated data. |
| free_data | Frees the data associated with an iterator, but not the iterator structure itself. |
| is_double_ended | Checks if an iterator is double-ended. |
| max | Returns the maximum item of an iterator, comparing using a given comparison function. |
| min | Returns the minimum item of an iterator, comparing using a given comparison function. |
| next | Returns the next item of the iterator. |
| next_back | Returns the next item from the back of a double-ended iterator. |
| nth | Returns the Nth item of an iterator. |
| nth_back | Returns the Nth item from the end of a double-ended iterator. |
75 changes: 75 additions & 0 deletions examples/double_ended.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* CIter - C library for lazily-evaluated iterators.
* Copyright (C) 2024 Kian Kasad <kian@kasad.com>
*
* This file is part of CIter.
*
* CIter is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software
* Foundation, version 3 of the License.
*
* CIter is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CIter. If not, see <https://www.gnu.org/licenses/>.
*/

#include <stdio.h>
#include <stdlib.h>

#include <citer.h>

#define UNUSED(x) ((void) (x))

static void *map_deref(void *item) {
return *((void **) item);
}

static bool is_even(void *item, void *fn_data) {
UNUSED(fn_data);
return ((unsigned long) item) % 2 == 0;
}

static void print_item(void *item, void *fn_data) {
printf("%s: %lu\n", (const char *) fn_data, (unsigned long) item);
}

int main(int argc, char *argv[]) {
if (argc != 1) {
fprintf(stderr, "Usage: %s\n", argv[0]);
return 1;
}

unsigned long arr[] = { 1, 2, 3, 4, 5 };
iterator_t *it;

/*
* Double-ended iterators which must be tested:
* - [x] over_array
* - [ ] repeat (but we can't test this without take)
* - [x] chain
* - [x] filter
* - [x] map
* - [x] once
* - [x] flatten
* - [x] inspect
* - [x] reverse
*/

it = citer_over_array(arr, sizeof(*arr), sizeof(arr) / sizeof(*arr));
it = citer_map(it, map_deref);
it = citer_chain(it, citer_flatten(citer_once(citer_once((void *) 6ul))));
it = citer_inspect(it, print_item, "Before map");
it = citer_filter(it, is_even, NULL);
it = citer_reverse(it);

void *item;
while ((item = citer_next(it)))
printf("Got: %lu\n", (unsigned long) item);

citer_free(it);

return 0;
}
42 changes: 42 additions & 0 deletions examples/reverse.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* CIter - C library for lazily-evaluated iterators.
* Copyright (C) 2024 Kian Kasad <kian@kasad.com>
*
* This file is part of CIter.
*
* CIter is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software
* Foundation, version 3 of the License.
*
* CIter is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CIter. If not, see <https://www.gnu.org/licenses/>.
*/

#include <stdio.h>

#include <citer.h>

int main(int argc, char *argv[]) {
if (argc <= 1) {
fprintf(stderr, "Usage: %s <args...>\n", argv[0]);
return 1;
}

iterator_t *it = citer_reverse(citer_over_array((void **) argv + 1, sizeof(*argv), argc - 1));

char **itemptr;
unsigned long count = 0;
while ((itemptr = (char **) citer_next(it))) {
printf("Got: %s\n", *itemptr);
count++;
}
printf("Count: %lu\n", count);

citer_free(it);

return 0;
}
2 changes: 2 additions & 0 deletions examples/run_all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ run ./sum {1..5}
run ./inspect {1..5}
run ./skip_take_while {1..100}
run ./zip {1..5} {a..e}
run ./reverse {a..f}
run ./double_ended
15 changes: 15 additions & 0 deletions src/chain.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ static void *citer_chain_next(void *_data) {
return citer_next(data->second);
}

static void *citer_chain_next_back(void *_data) {
citer_chain_data_t *data = (citer_chain_data_t *) _data;;
void *item;
if ((item = citer_next_back(data->second)))
return item;
else
return citer_next_back(data->first);
}

static void citer_chain_free_data(void *_data) {
citer_chain_data_t *data = (citer_chain_data_t *) _data;
citer_free(data->first);
Expand All @@ -52,7 +61,13 @@ iterator_t *citer_chain(iterator_t *first, iterator_t *second) {
*it = (iterator_t) {
.data = data,
.next = citer_chain_next,
.next_back = NULL,
.free_data = citer_chain_free_data,
};

/* Make chain double-ended if both inputs are. */
if (first->next_back && second->next_back)
it->next_back = citer_chain_next_back;

return it;
}
3 changes: 3 additions & 0 deletions src/chunked.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#include <stdlib.h>

/* TODO: Once size reporting is implemented, make chunked double-ended. */

typedef struct citer_chunked_data {
iterator_t *orig;
size_t chunksize;
Expand Down Expand Up @@ -60,6 +62,7 @@ iterator_t *citer_chunked(iterator_t *orig, size_t chunksize) {
*it = (iterator_t) {
.data = data,
.next = citer_chunked_next,
.next_back = NULL,
.free_data = citer_chunked_free_data,
};
return it;
Expand Down
3 changes: 3 additions & 0 deletions src/enumerate.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

#include <stdlib.h>

/* TODO: Once length reporting is implemented, make enumerate double-ended. */

typedef struct citer_enumerate_data {
iterator_t *orig;
size_t index;
Expand Down Expand Up @@ -54,6 +56,7 @@ iterator_t *citer_enumerate(iterator_t *orig) {
*it = (iterator_t) {
.data = data,
.next = citer_enumerate_next,
.next_back = NULL,
.free_data = citer_enumerate_free_data,
};
return it;
Expand Down
11 changes: 11 additions & 0 deletions src/filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ static void *citer_filter_next(void *_data) {
return NULL;
}

static void *citer_filter_next_back(void *_data) {
citer_filter_data_t *data = (citer_filter_data_t *) _data;
void *item;
while ((item = citer_next_back(data->orig))) {
if (data->predicate(item, data->predicate_data))
return item;
}
return NULL;
}

static void citer_filter_free_data(void *_data) {
citer_filter_data_t *data = (citer_filter_data_t *) _data;
citer_free(data->orig);
Expand All @@ -100,6 +110,7 @@ iterator_t *citer_filter(iterator_t *orig, citer_predicate_t predicate, void *ex
*it = (iterator_t) {
.data = data,
.next = citer_filter_next,
.next_back = citer_is_double_ended(orig) ? citer_filter_next_back : NULL,
.free_data = citer_filter_free_data,
};
return it;
Expand Down
Loading

0 comments on commit acb4a3e

Please sign in to comment.