Implementing the iterable protocol (Symbol.iterator)

I want to implement the iterable protocol on a rescript type so that consuming js code can use it in for..of loops for example.

I have access to the symbol, but I’m not sure how best to set this on the object.

@scope("Symbol") @val external symbolIterator: Js.Types.symbol = "iterator"

Do I need to use %%raw to set this property on the relevant object? Is that the only way?

1 Like

It may be doable with %%raw or other bindings, but it’s not really idiomatic, imho. It would be simpler to just provide a forEach(data, func) function in the module that iterates over the data using func. Yes, it won’t be usable with for...of, but it won’t look that bad in JS: MyType.forEach(data, el => ...). And it will be way simpler to implement than embedding a method in your custom type with strange externals or %%raw.

@tom-sherman Did you find a way to use Symbols for the iterator protocol? I’m interested in doing something similar with Symbols but I have no clue where to start, do you mind linking some docs or examples?

Unfortunately not. I’ve resorted to implementing half in rescript and half in plain JS (the part that actually sets the Iterable symbol). It’s not pretty though and depends on the JS representation of some enums, which feels wrong.

We could introduce iterator<'a> and iterator_result<'a> types and then use these to define a function for creating an iterator as well as map, filter, forEach, etc. for actually iterating over the iterator.

I was able to get this approach working in playground.

Here’s the code:

type iterator<'a>;

type iterator_result<'a> = {done: bool, value: 'a};

let make: (unit => iterator_result<'a>) => iterator<'a> = %raw(`
  (nextFn) => {
    return {
      [Symbol.iterator]() {
        return {
          next: nextFn,
        }; 
      },
    };
  }
`)

let forEach: ('a => unit, iterator<'a>) => unit = %raw(`
  (fn, iter) => {
    for (const val of iter) {
      fn(val);
    }
  }
`)

let i = ref(0)
let iter = make (() => {
  let res = {done: i.contents > 5, value: i.contents}
  i.contents = i.contents + 1
  res
})
forEach(val => Js.log(val), iter)

I ran the JavaScript output by the playground and it works.

The types are working as expected as well. Changing the last line to:

forEach(val => Js.log(val +. 1.), iter)

results in the following error:

[E] Line 31, column 34:
This has type: iterator<int>
  Somewhere wanted: iterator<float>
  
  The incompatible parts:
    int vs float
  
  You can convert int to float with Belt.Int.toFloat.
  If this is a literal, try a number with a trailing dot (e.g. 20.).
3 Likes

Nice! It get’s a little more tricky when you start working with a Seq type, as you end up having variant representations to deal with. However I read in the docs that this is OK to rely on as it’s a shared type, so what I said before about it feeling wrong was unfounded. I’ll try to open source something and circle back and put a link here.

Here it is: https://github.com/tom-sherman/seq-res

It’s mostly a port of Ocaml’s Seq module with data-first ReScript semantics and functions renamed to be match Array methods more closely.

The conversion from Seq → Iterable happens here in JS: https://github.com/tom-sherman/seq-res/blob/main/util/InternalIterator.js

Usage:

let seq = Seq.fromArray([1,2,3])
let iterator = Iterator.fromSeq(seq)
// Or
// let iterable = Iterable.fromSeq(seq)

// `raw` is used here for example's sake, it could be in a JS module importing the outputted JS
%%raw("
  for (const val of iterator) {
    console.log(val)
  }
")
4 Likes

It would be nice if we were able to use Symbol.iterator as a field name in records and objects so that we could make iterables containing other properties than just Symbol.iterator. The parser supports syntax like:

let p = {Point.x: 20, Point.y: 30}

so it should be possible to do something like:

type customIteratable = {
  foo: string,
  bar: bool,
  Symbol.iterator: iterator<int>,
}

let iterable = {foo: ..., bar: ..., Symbol.iterator: () => { ... } }

we’d have to tweak the field name lookup code and the code generation a bit. It would also be nice to have a way to enforce that all records/objects that define a Symbol.iterator field set its value type to be an iterator<int>. I’m not sure how such a constraint could be enforced though.

I think ideally I’d want to set arbitrary symbols as property keys, although I suspect this would require changes to the type system to support unique symbols?

@val external makeSymbol: string => Js.Types.symbol = "Symbol"

let id = makeSymbol("id")

type foo = {
  [id]: int
}

OTOH, most of the solutions that symbols solve (eg. information hiding) is solved in ReScript with opaque types so maybe a specific solution for well known symbols is needed.

You can already have arbitrary field names by using ReScript’s identifier quoting feature:

type customIteratable = {
  foo: string,
  bar: bool,
  \"Symbol.iterator": iterator<int>,
}

But that’s not the main issue. Having this in a record type would mean doing a bunch of hacks and workarounds to create an actual iterator which would track its iteration state internally, deciding whether and how to hide that from ReScript consuming code, while also exposing it to JavaScript code.

It all just seems a very hacky way to avoid using a simple ‘foreach’ function and trying to get the for...of syntax sugar.

I was thinking of enabling the writing of iterators in ReScript as a way of enhancing interop with JS so that large JS codebase could consume ReScript collections as iterators. Beyond support for...of syntax in JS, it would also allow JS code to use [...resIter] to consume ReScript iterators.

I was hesitant to introduce new syntax, but I can see the benefit of it. In addition to supporting arbitrary symbols, it also makes it more explicit that a symbol is being used as a key as opposed to something in scope that just happens to be name Js.Symbol.foo.

I wonder if it might make sense to use @as() instead of []. Maybe it could be modified to accept a symbol as well as a string for its payload. :thinking:

type customIteratable = {
  foo: string,
  bar: bool,
  @as(Symbol.iterator) iter: iterator<int>,
}

The parser already handles this, it’s just the code generator doesn’t know what to do when the payload to @as isn’t a string literal and currently ignore the decorator when that’s the case.

1 Like

I made some progress on implementing the proposed solution from my last post, see https://github.com/kevinbarabash/rescript-compiler/pull/3.

let zed = As_identifier_constants.zed

 type point = {
     x: int,
     y: int,
     @as(zed) z: int,
 }

 let p: point = {x: 5, y: 10, z: 25}

 Js.log(p)

is compiled to

'use strict';

 var As_identifier_constants = require("./as_identifier_constants.js");

 var p = {
   x: 5,
   y: 10,
   [zed]: 25
 };

 console.log(p);

 var zed = As_identifier_constants.zed;

 exports.zed = zed;
 exports.p = p;
 /*  Not a pure module */

There are some issues that need to be resolved, in particular:

  • var zed appears after its usage which is wrong
  • using @at(As_identifier_constants.zed) without any other use of As_identifier_constants will result in the dependency not being imported

I’m not sure how we determine in intra and inter module deps so it may take me some time figure out how to resolve these issues. The first issue is highest priority though since once that’s fixed we can assign an import to a local variable.