Skip to content

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# use disposes an object that implements Symbol.dispose, as well as one that implements IDisposable. A use of an IAsyncDisposable in a task, and a use in a jsAsyncGenerator, awaits Symbol.asyncDispose when the object has no DisposeAsync.
  • An F# class or object expression implementing IDisposable gets a [Symbol.dispose] method that calls Dispose, so JavaScript can hold it with using. One implementing IAsyncDisposable works with await 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);
}
Edit this page