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

Simplifying Excessive Nesting

Dependent computations often need several intermediate values at once. Effeck gives those values ordinary local names, so sequencing does not require nested callbacks or a record of named bindings. When reading TypeScript Effect code, do notation and generators express the same dependency order.

Recognizing nesting in dependent pipelines

function elapsed<A>(program: () => A) {
  const started = Clock.currentTimeMillis()
  const value = program()
  const finished = Clock.currentTimeMillis()
  log(`completed in ${finished - started} ms`)
  return value
}

function example() {
  return elapsed {
    sleep("40 millis")
    return "complete"
  }
}

The later statements can still refer to started and value. There is no need to nest a callback each time a value must remain in scope. The callback type leaves its contract inferred, so errors and services from program pass through elapsed.

Named bindings with do notation

function readInterval() {
  return Effect.Do
    |> Effect.bind(%, "started", () => Clock.currentTimeMillis())
    |> Effect.bind(%, "value") {
      sleep("40 millis")
      return "complete"
    }
    |> Effect.bind(%, "finished", () => Clock.currentTimeMillis())
}
// Returns { started, value, finished }.

Effect.Do is an Effect value holding an empty record. Each % stands where Effect.bind expects a program, so the pipe passes the bindings so far by name, and each Effect.bind runs its callback and adds the result under a new field name, with previous fields available to later callbacks. Field names must be unique. This style remains useful when maintaining an existing pipeline, while local variables are the usual Effeck spelling.

Introducing derived values

function intervalRecord() {
  return Effect.Do
    |> Effect.bind(%, "started", () => Clock.currentTimeMillis())
    |> Effect.bind(%, "finished") {
      sleep("40 millis")
      return Clock.currentTimeMillis()
    }
    |> Effect.let(%, "millis", ({ started, finished }) => finished - started)
}

Effect.let adds a field computed by a synchronous, non-failing callback. Use bind when computing the field performs Effect work. In an ordinary function, both become const declarations, with the compiler inferring the contract of each expression.

The sequence expressed with generators

function intervalRecord() {
  const started = Clock.currentTimeMillis()
  sleep("40 millis")
  const finished = Clock.currentTimeMillis()
  const millis = finished - started
  return { started, finished, millis }
}

This is the Effeck form of the same sequence. In a TypeScript file, Effect.gen(function* () { ... }) binds each Effect result with yield* and gives it a local name. In an .effeck file, ordinary calls already perform that sequencing. function* declares a value producer, so it is not an alternate syntax for running a single-result Effect.

Coming from Effect

  • Effect.Do and named bind fields usually become local const bindings.
  • A derived Effect.let field becomes an ordinary expression.
  • An Effect.gen body becomes a function body, with calls replacing yield*.