Skip to content

Iterators and for-of loops #6280

Open
Open
@cometkim

Description

@cometkim

This proposal introduces new syntax and semantics for improving for loops.

Motivation

Currently, ReScript has very limited for loops support.

In particular, the lack of support for iterators makes interacting with data structures on the JS side difficult. ReScript libraries provide function-style utilities such as forEach and reduce to make it possible. However, it potentially has several problems.

  1. Poor performance: in many cases forEach and reduce are slower than the native for loop. This is a problem in hot paths where micro-optimization is required.
  2. Memory usage: The ReScript core has an explicitly typed Core__Iterator, but this is practically useless. To use it in ReScript user has to convert it to an array via toArray, which adds unnecessary memory footprints.

These can be solved with hard-written for/while statements, but it is not a recommended solution.

@chenglou mentioned that we need some ergonomic improvements on it.
https://twitter.com/_chenglou/status/1653739852347879424

Presumption

Iterators are not compatible with the semantics of records.

ReScript itself has no class instances and it's always coming from the JS side.

Converting to iterators

First, add opaque types Js.iterator<'t> and Js.asyncIterator<'t> to core. This means the object available for the for-of loop and its entry type is 't.

Worth noting that Iterators are not always Array-like, such as Map

let map = new Map();
map.set(1, 1);
map.set(2, 2);

for (const [key, val] of map) {
  console.log(key, val);
}
// 1 1
// 2 2

ReScript types are:

// Map.res

type t<'key, 'val>

type iterator<'key, 'val> = Js.iterator<('key, 'val)>

There are two paths to getting a map's iterator: the map instance itself and Map.prototype.entries. ReScript needs to be able to express both of these as bindings.

@iterator
external iter: t<'key, 'val> => iterator<'key, 'val> = "%identity"

@iterator @get
external entries: t<'key, 'val> => iterator<'key, 'val> = "entries"

the @iterator tag if it helps the compiler implementation, to validate if the return value is Js.iterator.

Using iterators in for-of loops

Where possible, avoid adding new keywords here. Especially of as it is already a popular name in existing ReScript codebases.

Maybe add Js.iterator support to existing for ... in loops.

for entry in map->iter {
  ...
}

Also add destructuring

for (key, val) in map->iter {
  ...
}

Syntax looks similar to popular languages like Python or Kotlin, except we call iter function here.

It might be confused with js for-in syntax. We need to specify/teach that this compiles with for-of

Almost same for async iterators, but with await syntax. For example Deno HTTP server:

let server = Deno.listenWithOptions({ port: 8080 })
for await conn in server->Server.asyncIter {
  let httpConn = conn->Deno.serveHttp
  for await event in conn->HttpConn.asyncIter {
    // handle request event...
  }
}

Further improvements

Data-first syntax

The traditional for loop syntax is not data-first. We might also consider making additional syntax changes to better interact with ReScript's type inference.

example:

for map->iter to (key, val) {
  ...
}

So entries can be properly type-checked at input time

Break, continue

Since we don't share semantics with OCaml anymore, I think there's no reason not to introduce it.

One possible problem is if the user tries to break in a pattern match statement. break can be nested in compiled switch-case statements, destroying their semantics.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions