A Real Provider, Before and After
Alchemy deploys cloud resources from Effect programs. This page compares one of its resource providers in both languages: RegistryPolicy, the permissions policy of a private Amazon ECR registry. The TypeScript is Alchemy's own. The VibeLang is the conversion in smithersai/alchemy, which is a fork, with the hand edits listed below.
Both listings show only RegistryPolicyProvider, with Alchemy's comments removed. In the rest of the file, the VibeLang drops the effect/Effect import, which it keeps in scope, and imports ../IAM/Policy.vibe instead of ../IAM/Policy.ts. The props interface and the doc comment read the same.
Effect TypeScript
From packages/aws/src/ECR/RegistryPolicy.ts.
// fragment
export const RegistryPolicyProvider = () =>
Provider.effect(
RegistryPolicy,
Effect.gen(function* () {
const readPolicy = ecr
.getRegistryPolicy({})
.pipe(Effect.catchTag("RegistryPolicyNotFoundException", () => Effect.undefined));
const toPolicyText = (policy: PolicyDocument | string) =>
typeof policy === "string" ? policy : stringifyPolicyDocument(policy);
return {
stables: ["registryId"],
read: Effect.fn(function* () {
const observed = yield* readPolicy;
if (observed?.policyText === undefined) return undefined;
return {
registryId: observed.registryId ?? (yield* AWSEnvironment.current).accountId,
policy: observed.policyText,
};
}),
list: () =>
Effect.gen(function* () {
const observed = yield* readPolicy;
if (observed?.policyText === undefined) return [];
return [
{
registryId: observed.registryId ?? (yield* AWSEnvironment.current).accountId,
policy: observed.policyText,
},
];
}),
reconcile: Effect.fn(function* ({ news, session }) {
const desired = toPolicyText(news.policy);
const observed = yield* readPolicy;
let registryId = observed?.registryId;
if (
observed?.policyText === undefined ||
normalizePolicyDocument(observed.policyText) !== normalizePolicyDocument(desired)
) {
const put = yield* ecr.putRegistryPolicy({ policyText: desired });
registryId = put.registryId ?? registryId;
}
registryId ??= (yield* AWSEnvironment.current).accountId;
yield* session.note(registryId);
return { registryId, policy: desired };
}),
delete: Effect.fn(function* () {
yield* ecr
.deleteRegistryPolicy({})
.pipe(Effect.catchTag("RegistryPolicyNotFoundException", () => Effect.void));
}),
};
}),
);VibeLang
From packages/aws/src/ECR/RegistryPolicy.vibe.
// fragment
export const RegistryPolicyProvider = () =>
Provider.effect(
RegistryPolicy,
effect {
const readPolicy = effect {
return ecr.getRegistryPolicy({}) catch effect (e) => match (e) {
when { _tag: "RegistryPolicyNotFoundException" }: undefined;
default: throw e;
}
}
const toPolicyText = (policy: PolicyDocument | string) =>
typeof policy === "string" ? policy : stringifyPolicyDocument(policy)
return {
stables: ["registryId"],
read: effect () => {
const observed = try readPolicy
if (observed?.policyText === undefined) return undefined
return {
registryId: observed.registryId ?? (try AWSEnvironment.current).accountId,
policy: observed.policyText,
}
},
list: effect () => {
const observed = try readPolicy
if (observed?.policyText === undefined) return []
return [
{
registryId: observed.registryId ?? (try AWSEnvironment.current).accountId,
policy: observed.policyText,
},
]
},
reconcile: effect ({ news, session }) => {
const desired = toPolicyText(news.policy)
const observed = try readPolicy
let registryId = observed?.registryId
if (
observed?.policyText === undefined
|| normalizePolicyDocument(observed.policyText) !== normalizePolicyDocument(desired)
) {
const put = try ecr.putRegistryPolicy({ policyText: desired })
registryId = put.registryId ?? registryId
}
registryId ??= (try AWSEnvironment.current).accountId
try session.note(registryId)
return { registryId, policy: desired }
},
delete: effect () => {
ecr.deleteRegistryPolicy({}) catch effect (e) => match (e) {
when { _tag: "RegistryPolicyNotFoundException" }: undefined;
default: throw e;
}
},
}
},
)What changed
| Effect TypeScript | VibeLang |
|---|---|
Effect.gen(function* () { ... }) | effect { ... } |
Effect.fn(function* ({ news }) { ... }) | effect ({ news }) => { ... } |
yield* e | try e |
e.pipe(Effect.catchTag("X", () => Effect.void)) | e catch effect (err) => match (err) { when { _tag: "X" }: undefined; default: throw err; } |
Over the provider, the 9 yield*, 2 Effect.gen, 3 Effect.fn and 2 .pipe calls are gone. The listing takes 60 lines against 57, and 1372 characters against 1436, counting no whitespace.
The catch is the one place VibeLang is longer. Effect.catchTag rethrows every other error for you, and match writes that rethrow out as default: throw err.
Hand edits
The converter produced everything above except these, all made by hand:
when { _tag: "..." }: try Effect.voidandtry Effect.undefinedbecameundefined.ecr.getRegistryPolicy({})andecr.deleteRegistryPolicy({})moved onto one line with theircatch.- Two
: infer _return annotations and everymutablemember modifier, which an earlier converter wrote, were removed. The fork keeps the two annotations until it moves to the compiler this page documents.
How it was checked
- Compiler. The current compiler reports no diagnostics for this module. The rest of the fork was still on its pinned compiler for these runs.
- Declarations. The
.d.tsthis listing emits matches the one Alchemy's TypeScript emits, apart from the extensions of relative imports. - Tests. Alchemy's own test,
test/AWS/ECR/RegistryPolicy.test.ts, passes unchanged on the JavaScript this listing compiles to. It deploys the policy, reads it back, deploys again and destroys it. It is a live AWS test and these runs had no AWS account, so its HTTP calls went to a local stand-in for ECR and STS. The test failed when a planted fault replaced the delete call. - Formatting.
vibelang-format --checkpasses on the file.
Coming from Effect
Nothing in Provider.effect's signature changed, so TypeScript callers see the same module. See the cheat sheet for every form beside its Effect code, and Alchemy for how the two fit together.