Part I. The language by example
Each chapter shows something you do in Effect today and the same thing in Effeck. Effeck code uses Effect's own modules unchanged; what changes is that you stop writing generators, yield*, and most combinators.
Three conventions for the snippets. A snippet shown without an enclosing function sits inside one, because a module's top level cannot run a program (Chapter 13). Names such as User, guest, and MailerTest are declared nearby, and helpers such as toUser are Effeck functions. Imports of Effect modules outside the prelude (Schedule, Stream, Fiber, and so on) are left out.
1. A first look
Here is a small but complete program: two services, two typed errors, a function that uses them, and a main that runs it.
The program
import { Context, Data, Effect } from "effect"
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string
}> {}
class MailError extends Data.TaggedError("MailError")<{
readonly reason: string
}> {}
class Users extends Context.Service<Users, {
find(id: string): Effect.Effect<User, UserNotFound>
}>()("app/Users") {}
class Mailer extends Context.Service<Mailer, {
send(to: string, body: string): Effect.Effect<void, MailError>
}>()("app/Mailer") {}
export const welcome = Effect.fn("welcome")(function* (id: string) {
const users = yield* Users
const mailer = yield* Mailer
const user = yield* users.find(id)
yield* mailer.send(user.email, `Welcome, ${user.name}!`)
return user.name
})error UserNotFound {
id: string
}
error MailError {
reason: string
}
service Users {
find(id: string): User!UserNotFound
}
service Mailer {
send(to: string, body: string): void!MailError
}
export function welcome(id: string) {
const user = Users.find(id)
Mailer.send(user.email, `Welcome, ${user.name}!`)
return user.name
}Four things changed:
welcomeis a plain function. NoEffect.fn, no generator, noyield*. CallingUsers.find(id)runs it and gives you theUser; if it fails,welcomefails with the same error.- A service is a
servicedeclaration.service Users, an abstract class, replaces theContext.Serviceclass: no key string and noContextimport. You callUsers.find(id)directly instead of first getting the service withyield* Users(Chapter 4). - The contract is inferred.
welcomeisstring!UserNotFound | MailError!Users | Mailer: it succeeds with astring, can fail with either error, and needs both services. You write the contract only when you want the compiler to check it (Chapter 2). - Errors are the same classes.
error UserNotFound { id: string }declares a class that extendsTaggedError, Effect'sData.TaggedError, in scope without an import.
What is in scope
Every .effeck file starts with a small prelude in scope, so the program above has no imports:
| Name | What it is |
|---|---|
Effect | Effect's Effect module, and the type Effect<A, E, R> |
Schema, Layer | Effect's modules, unchanged |
Scope | Effect's Scope module, and the type Scope, which is Effect's Scope.Scope |
TaggedError | Effect's Data.TaggedError |
Control | The base class of control operations (Chapter 9) |
pipe, wire, fork, handle, resume, resumeWith, abandon | Effeck's built-ins, each introduced where it is first used |
sync, try, callback | Boundary hatches: their first argument is self-contained TypeScript, and values reach it as inputs (D148, D229) |
runPromise, runFork, runSync, runCallback | Effect's runners, the way back in from TypeScript (D150) |
sleep, log, logDebug, logInfo, logWarning, logError, retry, repeat, timeout, all, forEach, race, withSpan, die | The Effect module's functions without the Effect. prefix (D151) |
Every other export of effect's root | Console, Option, Stream, Schedule, Duration, and the rest, plus flow and identity (D146) |
Modules from effect's subpath packages, such as effect/http, are imported as usual. An import or a local declaration with the same name shadows a prelude name, as it would shadow any global. The prelude is what most Effect files import anyway: Effect appears in 77-81% of the Effect files in two large codebases (Smithers, 2,149 files; opencode, 1,031), Schema in 39-44%, Layer in 34-44%, and no other module in more than 22%. Scope is there because contracts name it whenever a resource outlives a function (Chapter 6). JavaScript's own globals keep their names: Array, String, Number, Boolean, Symbol, BigInt, Function, and RegExp mean JavaScript's, so Effect's modules with those names need an import under another name, as in import { Array as Arr } from "effect". A host global does not shadow Effect: in a project whose lib includes the DOM, Option is still Effect's.
Running it
// main.effeck
import { AppLive } from "./app.effeck"
import { welcome } from "./welcome.effeck"
export function main() {
const name = Effect.provide(() => welcome("42"), AppLive)
log(`sent a welcome email to ${name}`)
}Effect.provide supplies the services welcome needs from an application Layer (Chapter 4 builds AppLive). The () => in () => welcome("42") is how you hand a program to Effect without running it yet (Chapter 5). The module exports main without parameters, so its JavaScript ends with if (import.meta.main) NodeRuntime.runMain(main()): node main.js hands main to Node's runner, the only place a program starts, and importing the module starts nothing (D135).
What the compiler produces
Effeck compiles to TypeScript. This is the output for welcome:
export const welcome = Effect.fnUntraced(function* (id: string) {
const users = yield* Users
const user = yield* users.find(id)
const mailer = yield* Mailer
yield* mailer.send(user.email, `Welcome, ${user.name}!`)
return user.name
})export declare const welcome: (id: string) =>
string!UserNotFound | MailError!Users | MailerTypeScript code that imports welcome sees an ordinary function returning an Effect. Each service class compiles to an interface for its methods and a Context.Key class (Chapter 4), so a TypeScript program can provide Users with an ordinary Layer, and Effeck can use services written in TypeScript.