Description
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.
- Poor performance: in many cases
forEach
andreduce
are slower than the native for loop. This is a problem in hot paths where micro-optimization is required. - 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 viatoArray
, 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
Type
Projects
Status