Generators
This page covers jsGenerator { } and jsAsyncGenerator { }, which compile to native function* and async function* generators, and how F# use works with JavaScript's Symbol.dispose and Symbol.asyncDispose. You need them when JavaScript code expects a generator, sends values back through next, or iterates your values with for await.
jsGenerator
jsGenerator { } compiles to a native generator. Its type is JsGenerator<'Yield, 'Return, 'Next>: the values it yields, the value it returns when it finishes, and the values the caller sends back with next. yield yields a value, yield! yields every value of a sequence, and return finishes the generator with its terminal value.
open Fickl.Core
let countdown (start: int) : JsGenerator<int, string, unit> = jsGenerator {
let mutable n = start
while n > 0 do
yield n
n <- n - 1
return "liftoff"
}
let total () =
let mutable sum = 0
for n in countdown 3 do
sum <- sum + n
sum
import { disposeSafe, getEnumerator } from "@fable-org/fable-library-js/Util.js";
export function countdown(start) {
return (function* () {
let n = start;
while (n > 0) {
yield n;
n = ((n - 1) | 0);
}
return "liftoff";
}).call(this);
}
export function total() {
let sum = 0;
const enumerator = getEnumerator(countdown(3));
try {
while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
const n = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]() | 0;
sum = ((sum + n) | 0);
}
}
finally {
disposeSafe(enumerator);
}
return sum | 0;
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { type IDisposable, disposeSafe, type IEnumerator, getEnumerator } from "@fable-org/fable-library-ts/Util.ts";
export function countdown(this: any, start: int32): Generator<int32, string, void> {
return (function* (this: any): Generator<int32, string, void> {
let n: int32 = start;
while (n > 0) {
yield n;
n = ((n - 1) | 0);
}
return "liftoff";
}).call(this);
}
export function total(): int32 {
let sum = 0;
const enumerator: IEnumerator<int32> = getEnumerator(countdown(3));
try {
while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
const n: int32 = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]() | 0;
sum = ((sum + n) | 0);
}
}
finally {
disposeSafe(enumerator as IDisposable | null);
}
return sum | 0;
}
The body does not run until the first next call, as with any JavaScript generator. A JsGenerator is also an F# sequence, so for and the Seq functions iterate it.
Receiving values from next
In a jsGenerator, let! reply = value yields value and binds reply to the value the caller passes to the following next. The result of next is a JsGeneratorResult, which you match as Yielded or Returned.
open Fickl.Core
let accumulate () : JsGenerator<int, unit, int> = jsGenerator {
let mutable sum = 0
while true do
let! added = sum
sum <- sum + added
}
let run () =
let gen = accumulate ()
gen.next () |> ignore
gen.next 5 |> ignore
match gen.next 10 with
| Yielded result -> result.value
| Returned _ -> 0
export function accumulate() {
return (function* () {
let sum = 0;
while (true) {
const _arg = (yield sum) | 0;
sum = ((sum + _arg) | 0);
}
}).call(this);
}
export function run() {
let unionValue;
const gen = accumulate();
gen.next();
gen.next(5);
const matchValue = gen.next(10);
if ((unionValue = matchValue, !!unionValue.done)) {
return 0;
}
else {
return (matchValue.value) | 0;
}
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
export function accumulate(this: any): Generator<int32, void, int32> {
return (function* (this: any): Generator<int32, void, int32> {
let sum = 0;
while (true) {
const _arg: int32 = (yield sum) | 0;
sum = ((sum + _arg) | 0);
}
}).call(this);
}
export function run(): int32 {
let unionValue: IteratorResult<int32, void> = undefined as any;
const gen: Generator<int32, void, int32> = accumulate();
gen.next();
gen.next(5);
const matchValue: IteratorResult<int32, void> = gen.next(10);
if ((unionValue = matchValue, !!unionValue.done)) {
return 0;
}
else {
const result = matchValue as IteratorYieldResult<int32>;
return (result.value) | 0;
}
}
let! added = sum compiles to const _arg = (yield sum) | 0;. The generator also has return, which closes it and runs its active finally blocks, and throw, which raises an exception at the suspended yield; try ... with inside the body catches it.
[<GeneratorFunction>]
Without the attribute, a function returning jsGenerator is an ordinary function that creates and returns a generator. Put [<GeneratorFunction>] on a module function returning jsGenerator and the function itself is the function*.
open Fickl.Core
[<GeneratorFunction>]
let conversation (request: string) : JsGenerator<string, string, string> = jsGenerator {
let! reply = request
yield reply
return "done:" + reply
}
export const conversation = function* (request) {
const reply = yield request;
yield reply;
return "done:" + reply;
};
export const conversation = function* (this: any, request: string): Generator<string, string, string> {
const reply: string = yield request;
yield reply;
return "done:" + reply;
};
The compiled value is a const bound to a function* expression, typed Generator<string, string, string> in TypeScript. The return type annotation gives the yielded, terminal and sent types.
jsAsyncGenerator and jsYield
jsAsyncGenerator { } compiles to a native async generator, a JsAsyncGenerator<'Yield, 'Return, 'Next>. In it, let! awaits a promise, and yield! yields every value of a sequence or of a JS.AsyncIterable.
open Fable.Core
open Fickl.Core
let pages (fetchPage: int -> JS.Promise<string[]>) : JsAsyncGenerator<string, unit, unit> = jsAsyncGenerator {
let mutable page = 0
let mutable more = true
while more do
let! items = fetchPage page
yield! items
more <- items.Length > 0
page <- page + 1
}
export function pages(fetchPage) {
return (async function* () {
let page = 0;
let more = true;
while (more) {
const items = await fetchPage(page);
yield* items;
more = (items.length > 0);
page = ((page + 1) | 0);
}
}).call(this);
}
import type { MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
export function pages(this: any, fetchPage: ((arg0: int32) => Promise<MutableArray<string>>)): AsyncGenerator<string, void, void> {
return (async function* (this: any): AsyncGenerator<string, void, void> {
let page = 0;
let more = true;
while (more) {
const items: MutableArray<string> = await fetchPage(page);
yield* items;
more = (items.length > 0);
page = ((page + 1) | 0);
}
}).call(this);
}
Because let! awaits, receiving a value from next needs its own form: let! reply = jsYield value yields value and binds reply to the value sent by the following next. Yield and return resolved values, not promises.
[<AsyncGeneratorFunction>]
[<AsyncGeneratorFunction>] on a module function returning jsAsyncGenerator emits it as an async function*. A JsAsyncGenerator is a JS.AsyncIterable, so a jsAsync body loops over it with for await.
open Fable.Core
open Fickl.Core
[<AsyncGeneratorFunction>]
let conversation (request: JS.Promise<string>) : JsAsyncGenerator<string, string, string> = jsAsyncGenerator {
let! ready = request
let! reply = jsYield ready
yield reply
return "done:" + reply
}
[<AsyncFunction>]
let consume (source: JS.AsyncIterable<string>) = jsAsync {
let mutable result = ""
for value in source do
result <- result + value
return result
}
export const conversation = async function* (request) {
const _arg = await request;
const reply = yield _arg;
yield reply;
return "done:" + reply;
};
export const consume = async function (source) {
let result = "";
for await (const _arg of source) {
result = (result + _arg);
}
return result;
};
export const conversation = async function* (this: any, request: Promise<string>): AsyncGenerator<string, string, string> {
const _arg: string = await request;
const reply: string = yield _arg;
yield reply;
return "done:" + reply;
};
export const consume = async function (this: any, source: AsyncIterable<string>): Promise<string> {
let result = "";
for await (const _arg of source) {
result = (result + _arg);
}
return result;
};
See Async functions for jsAsync and [<AsyncFunction>].
Native disposal
JavaScript's explicit resource management uses Symbol.dispose and Symbol.asyncDispose, with the using and await using declarations. Fickl connects them to F# disposal in both directions:
- F#
usedisposes an object that implementsSymbol.dispose, as well as one that implementsIDisposable. Auseof anIAsyncDisposablein atask, and ausein ajsAsyncGenerator, awaitsSymbol.asyncDisposewhen the object has noDisposeAsync. - An F# class or object expression implementing
IDisposablegets a[Symbol.dispose]method that callsDispose, so JavaScript can hold it withusing. One implementingIAsyncDisposableworks withawait using.
open System
open Fable.Core
[<Global>]
let openFile (path: string) : IDisposable = jsNative
let describe (path: string) =
use file = openFile path
string file
type Lock() =
interface IDisposable with
member _.Dispose() = printfn "released"
import { disposeSymbol, disposeSafe } from "@fable-org/fable-library-js/Util.js";
import { toStringOrEmpty } from "@fable-org/fable-library-js/Types.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";
export function describe(path) {
const file = openFile(path);
try {
return toStringOrEmpty(file);
}
finally {
disposeSafe(file);
}
}
export class Lock {
constructor() {
}
Dispose() {
console.log("released");
}
[disposeSymbol]() {
return this.Dispose();
}
}
export function Lock_$reflection() {
return class_type("Example6.Lock", undefined, Lock);
}
export function Lock_$ctor() {
return new Lock();
}
import { disposeSymbol, disposeSafe, type IDisposable } from "@fable-org/fable-library-ts/Util.ts";
import { toStringOrEmpty } from "@fable-org/fable-library-ts/Types.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
export function describe(path: string): string {
const file: IDisposable = openFile(path);
try {
return toStringOrEmpty(file);
}
finally {
disposeSafe(file as IDisposable | null);
}
}
export class Lock implements IDisposable, Disposable {
constructor() {
}
Dispose(): void {
console.log("released");
}
[disposeSymbol](): void {
return this.Dispose();
}
}
export function Lock_$reflection(): TypeInfo {
return class_type("Example6.Lock", undefined, Lock);
}
export function Lock_$ctor(): Lock {
return new Lock();
}
The use compiles to a try ... finally that calls disposeSafe, which calls Dispose if the object has one and [Symbol.dispose] otherwise. When an object has both, Dispose runs. Lock compiles with a [disposeSymbol]() method, the runtime library's name for Symbol.dispose.
In a jsAsyncGenerator, use takes a resource that implements IDisposable, IAsyncDisposable or JsAsyncDisposable, the Fickl.Core binding of an object with Symbol.asyncDispose, and awaits its disposal when the generator leaves its body. The compiled finally calls disposeAsyncSafe, which tries DisposeAsync, then [Symbol.asyncDispose], then synchronous disposal.
open Fable.Core
open Fickl.Core
[<Global>]
let connect () : JsAsyncDisposable = jsNative
let rows () : JsAsyncGenerator<int, unit, unit> = jsAsyncGenerator {
use db = connect ()
yield 1
yield 2
}
import { disposeAsyncSafe } from "@fable-org/fable-library-js/Util.js";
export function rows() {
return (async function* () {
const _arg = connect();
try {
yield 1;
yield 2;
}
finally {
await disposeAsyncSafe(_arg);
}
}).call(this);
}
import { disposeAsyncSafe } from "@fable-org/fable-library-ts/Util.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
export function rows(this: any): AsyncGenerator<int32, void, void> {
return (async function* (this: any): AsyncGenerator<int32, void, void> {
const _arg: any = connect();
try {
yield 1;
yield 2;
}
finally {
await disposeAsyncSafe(_arg);
}
}).call(this);
}