VibeLang

Warning: VibeLang is v0. The language will change in major, breaking ways. It is not suitable for production use.

Producers

A producer yields values one at a time and can run Effects between yields. Use function* to declare one and for...of to consume it.

Yield values on demand

VibeLang
// run: Console.log(firstAlert())
function* readings() {
  try {
    for (const celsius of [18, 21, 25, 19, 28, 30]) {
      yield celsius
    }
  } finally {
    Console.log("sensor closed")
  }
}

function firstAlert() {
  for (const celsius of readings()) {
    if (celsius >= 25) return celsius
  }
  return undefined
}
// number | undefined!!
// Output: sensor closed
// Output: 25

Types in this example

readings
function readings(): Stream<number, never, never>
celsius
const celsius: number
Console
(alias) module "effect/Console"
log
const log: (...args: ReadonlyArray<any>) => void!!Runs here and returns void.Logs a general-purpose message to the console.
firstAlert
function firstAlert(): number | undefined!!
undefined
(property) undefined: undefined

Calling readings() constructs a lazy source. Its body starts when a consumer asks for a value. Each yield hands one value to the loop, and the producer waits until the consumer continues.

Here the consumer returns at 25. Before firstAlert returns, the producer's finally logs sensor closed. break, a return, or a failure in the loop body closes the source and runs its cleanup. Iterating the source again starts a new run.

In .vibe, function* declares a producer. Do not use it to write Effect.gen or other generator-based APIs. Generator methods on objects remain JavaScript generators.

Call services between yields

VibeLang
// run: provide NodeServices.layer { const fs = Effect.service(FileSystem); using dir = fs.makeTempDirectoryScoped(); fs.writeFileString(`${dir}/a.txt`, "short"); fs.writeFileString(`${dir}/b.txt`, "x".repeat(1001)); Console.log(firstLargeFile(dir) === `${dir}/b.txt`) }
import { FileSystem } from "effect/FileSystem"
import { Path } from "effect/Path"
import { NodeServices } from "@effect/platform-node"

function* fileTexts(directory: string) {
  const names = FileSystem.readDirectory(directory).sort()
  for (const name of names) {
    const path = Path.join(directory, name)
    const info = FileSystem.stat(path)
    if (info.type === "File") {
      yield { path, text: FileSystem.readFileString(path) }
    }
  }
}

function firstLargeFile(directory: string) {
  for (const file of fileTexts(directory)) {
    if (file.text.length > 1000) return file.path
  }
  return undefined
}
// Output: true

Types in this example

FileSystem
(alias) const FileSystem: Service<FileSystem, FileSystem> (alias) interface FileSystemService tag for platform file-system operations.
Path
(alias) const Path: Service<Path, Path> (alias) interface Path (alias) namespace PathService tag for accessing the current Path implementation.
NodeServices
(alias) module "@effect/platform-node/NodeServices"
fileTexts
function fileTexts(directory: string): Stream<{ path: string; text: string; }, PlatformError, FileSystem | Path>
directory
(parameter) directory: string
names
const names: string[]
FileSystem
(alias) const FileSystem: Service<FileSystem, FileSystem>Service tag for platform file-system operations.
readDirectory
(property) FileSystem.readDirectory: (path: string, options?: { readonly recursive?: boolean | undefined; }) => Array<string>!PlatformError!Runs here and returns string[].List the contents of a directory.
sort
(method) Array<string>.sort(compareFn?: ((a: string, b: string) => number) | undefined): string[]Sorts an array in place. This method mutates the array and returns a reference to the same array.
name
const name: string
path
const path: string
Path
(alias) const Path: Service<Path, Path>Service tag for accessing the current Path implementation.
join
(property) Path.join: (...paths: ReadonlyArray<string>) => string
info
const info: File.Info
stat
(property) FileSystem.stat: (path: string) => File.Info!PlatformError!Runs here and returns File.Info.Get information about a file at path. See File.Info for metadata limits.
type
(property) File.Info.type: File.Type
path
(property) path: string
text
(property) text: string
readFileString
(property) FileSystem.readFileString: (path: string, encoding?: string) => string!PlatformError!Runs here and returns string.Read the contents of a file.
firstLargeFile
function firstLargeFile(directory: string): string | undefined!PlatformError!FileSystem | Path
file
const file: { path: string; text: string; }
length
(property) String.length: numberReturns the length of a String object.
undefined
(property) undefined: undefined

Service calls run as they do in an ordinary function. The producer's failures and requirements join the consuming function's contract. Provide NodeServices.layer at the application boundary to supply FileSystem and Path, as in examples/file-tree.

A producer can acquire resources with using. They stay open between yields and close when consumption finishes or stops early. This example reads one whole file at a time; stopping avoids reading the remaining files.

Use a producer as a Stream

VibeLang
// run: Console.log(JSON.stringify(alerts()))
function alerts() {
  const selected = readings().filter((value) => value >= 25).map((value) => `${value} C`).take(2)
  const result: string[] = []
  for (const alert of selected) result.push(alert)
  return result
  // ["25 C", "28 C"]; sensor closed
}
// Output: sensor closed
// Output: ["25 C","28 C"]

Types in this example

alerts
function alerts(): string[]!!
selected
const selected: Stream<string, never, never>
readings
(alias) function readings(): Stream<number, never, never>
filter
(property) filter: <number, never, never>(self: Stream<number, never, never>, predicate: Predicate<number>) => Stream<number, never, never>Method syntax: calls Stream.filter(readings(), …).Filters a stream to the elements that satisfy a predicate.
value
(parameter) value: number
map
(property) map: <number, never, never, string>(self: Stream<number, never, never>, f: (a: number, i: number) => string) => Stream<string, never, never>Method syntax: calls Stream.map(readings().filter((value) => value >= 25), …).Transforms the elements of this stream using the supplied function.
take
(property) take: <string, never, never>(self: Stream<string, never, never>, n: number) => Stream<string, never, never>Method syntax: calls Stream.take(readings().filter((value) => value >= 25).map((value) => ${value} C), …).Takes the first n elements from this stream.
result
const result: string[]
alert
const alert: string
push
(method) Array<string>.push(...items: string[]): numberAppends new elements to the end of an array, and returns the new length of the array.

A producer has type Stream.Stream<Value, Error, Requirements>. Stream operators accept it directly, and TypeScript consumers see a Stream in its declaration file. The examples/pipeline program uses this filter, map, and take pattern.

When Stream operators consume a producer, the runtime starts it on its own fiber and uses a handoff with no buffer. The producer yields only when the Stream pulls; closing the Stream interrupts the producer and runs cleanup. Handle control operations inside that producer before sending it to Stream operators.

Iterate any Stream

VibeLang
// run: Console.log(sumStream())
function sumStream() {
  let total = 0
  for (const value of Stream.fromIterable([1, 2, 3])) {
    total += value
  }
  return total
}
// Output: 6

Types in this example

sumStream
function sumStream(): number!!
total
let total: number
value
const value: number
Stream
(alias) module "effect/Stream"
fromIterable
const fromIterable: <number>(iterable: Iterable<number>, options?: { readonly chunkSize?: number | undefined; }) => Stream<number, never, never>Creates a new Stream from an iterable collection of values.

for...of pulls an Effect Stream and closes it when the loop exits, including on break. Arrays, maps, sets, and strings keep JavaScript's ordinary iteration behavior.

Use for (const value of source) for producers. This preview does not support for (using value of source) over a producer, and Array.from(source) is not a Stream consumer. Use Stream.runCollect(source) when you need all the elements as an array.

Coming from Effect

A generator wrapped in Stream.unwrap becomes a function*. Write calls between yield statements; reserve yield for the values the producer emits. Use Stream's existing operators whenever they express the transformation you need.