Scala-like for-comprehensions
Compose monadic operations elegantly without nested flatMap calls
Scala-like for-comprehensions for composing monadic operations.
Do-notation provides generator-based monadic comprehensions inspired by Scala’s for-comprehensions. It makes complex monadic chains readable and maintainable by eliminating nested flatMap calls.
import { Do, $ } from "functype/do";
import { Option } from "functype/option";
// Instead of nested flatMaps
const nested = Option(1).flatMap((a) =>
Option(2).flatMap((b) => Option(3).map((c) => a + b + c)),
);
// Use Do-notation
const clean = Do(function* () {
const a = yield* $(Option(1));
const b = yield* $(Option(2));
const c = yield* $(Option(3));
return a + b + c;
}); // Some(6)
The $ function enables TypeScript type inference with generators:
import { Do, $ } from "functype/do";
const result = Do(function* () {
const x = yield* $(Option(42)); // x is number, not unknown
return x * 2;
});
Do-notation automatically propagates None/Left/Failure:
// Option - stops on None
const result = Do(function* () {
const a = yield* $(Option(1));
const b = yield* $(Option.none<number>()); // stops here
const c = yield* $(Option(3));
return a + b + c;
}); // None
// Either - stops on Left
const validated = Do(function* () {
const name = yield* $(validateName(input)); // Left stops chain
const email = yield* $(validateEmail(input));
return { name, email };
});
Do-notation works with any monad in functype:
const result = Do(function* () {
const user = yield* $(findUser(id));
const profile = yield* $(user.profile);
const email = yield* $(profile.email);
return email;
}); // Option<string>
const result = Do(function* () {
const name = yield* $(validateName(input.name));
const age = yield* $(validateAge(input.age));
const email = yield* $(validateEmail(input.email));
return { name, age, email };
}); // Either<ValidationError, User>
const result = Do(function* () {
const config = yield* $(Try(() => readConfig()));
const db = yield* $(Try(() => connectDB(config)));
const users = yield* $(Try(() => db.query("SELECT * FROM users")));
return users;
}); // Try<User[]>
const combinations = Do(function* () {
const x = yield* $(List([1, 2, 3]));
const y = yield* $(List(["a", "b"]));
return `${x}${y}`;
}); // List(["1a", "1b", "2a", "2b", "3a", "3b"])
For async operations, use DoAsync:
import { DoAsync, $ } from "functype/do";
const result = await DoAsync(async function* () {
const user = yield* $(Task.async("getUser", () => fetchUser()));
const posts = yield* $(Task.async("getPosts", () => fetchPosts(user.id)));
return { user, posts };
});
Do-notation is highly optimized:
// Without Do-notation
const result = getUser(id).flatMap((user) =>
getProfile(user.profileId).flatMap((profile) =>
getSettings(profile.settingsId).map((settings) => ({
user,
profile,
settings,
})),
),
);
// With Do-notation
const result = Do(function* () {
const user = yield* $(getUser(id));
const profile = yield* $(getProfile(user.profileId));
const settings = yield* $(getSettings(profile.settingsId));
return { user, profile, settings };
});
See full API documentation at functype API docs