Some coding patterns stay true regardless of programming language.
No, this is not about OOP, functional programming, design patterns, or anything like that 💁🏼♂️
This is all about shared problems with every codebase, and how to solve those problems:
- Error handling (
Either
/Option
)- Dependency management (Dependency injection)
- Debugging (Logging and observability)
These were the same when I started with PHP, with Java for Android, Dart, Typescript, and all.
Let me share this with you 👇
Tech stack
- Effect: I am exploring these principles and how to solve them with Effect lately. The same ideas can (and will) be applied to other programming languages
Setup
This week I explored more of the Effect API.
Principle 1: any usable language must provides tooling (documentation, IDE support, types, debugging)
This is why working with Typescript and Dart is great: the IDE support is next level 🚀
Get started
I rewrote some native Typescript code to Effect to learn the differences.
Principles 2: Error handling requires
Either
andOption
(try/catch is flawed)
Errors cannot be ignored: they must be explicit at the type level.
Effect.Effect<void, PlatformError | FileError, Fs.FileSystem> // 👈 With Effect
Promise<void> // 👈 Plain Typescript
Effect collects all possible errors, you know exactly what can go wrong. You can then handle all the errors in one place.
"If it complies it works" 🪄
There is more 🤩
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
Implementation
Solving error handling is as simple as this 👇
export type Either<E, A> = Left<E, A> | Right<E, A>
You only need to be explicit: sometimes things go wrong (Left
) and sometimes they work (Right
).
This removes any try/catch, throw, exceptions, or whatever.
Scale complexity: Dependency Injection
Second (major) problem: organize code dependencies to loose coupling and simplify testing.
Principle 3: interfaces and dependency injection
Effect uses services and layers: define interfaces, and only at the very end provide concrete implementations.
export interface JsonParseService {
readonly parse: (
source: string
) => Effect.Effect<JsonString, JsonParseError>;
}
export const JsonParseService = Context.GenericTag<JsonParseService>(
"@app/JsonParseService"
);
In the above code there is no mention of "how" this is implemented.
Nonetheless, we can still use the service, since the interface is enough to know what this does:
const program = (body: string) =>
Effect.gen(function* (_) {
const json = yield* _(JsonParse.JsonParseService);
const idOrUrlJson = yield* _(json.parse(body));
return yield* _(idOrUrlJson, parseZod(zodSchema));
});
We can then "inject" the implementation only at the end:
const JsonParseServiceLive = Layer.succeed(
JsonParseService,
JsonParseService.of({
parse: (source) =>
Effect.try({
try: () => JsonString(JSON.parse(source)),
catch: (error) => new JsonParseError({ source, error }),
}),
})
);
const program = (body: string) =>
Effect.gen(function* (_) {
const json = yield* _(JsonParse.JsonParseService);
const idOrUrlJson = yield* _(json.parse(body));
return yield* _(idOrUrlJson, parseZod(zodSchema));
}).pipe(
Effect.provide(JsonParse.JsonParseServiceLive)
);
This allows full control over each implementation, and more flexibility for testing ✅
Logging and observability
How do I make sure everything works as expected?
Principle 4: debugging/logging/observability are not afterthoughts, they are core to every codebase
Effect provides a full Logger
module for message reporting (fully customizable with dependency injection 🛠️).
Effect.gen(function* (_) {
const fs = yield* _(Fs.FileSystem);
const gzip = yield* _(Gzip.Gzip);
const existsExtractedFilename = yield* _(fs.exists(extractedFilename));
if (useCachedFile && existsExtractedFilename) {
return yield* _(
Effect.logDebug(`using cached file "${extractedFilename}"`)
);
}
yield* _(Effect.logDebug(`extracting ${gzipFilename}...`));
yield* _(gzip({ gzipFilename, extractedFilename }));
})
Often times logging is not enough. We need full observability.
Effect provides APIs for Metrics and Tracing:
- Tracking a Value Over Time
- Request Counts
- Error Counts
- Memory Usage
I have still a lot to explore on these APIs 🤓
This is your starting point to start converting your codebase to Effect 💡
Takeaways
- Every programming language shares similar challenges
- Some principles stay true regardless of your programming language or framework
- The main problem is managing complexity: explicit errors and dependencies using types helps scaling
- Effect is spearheading a new paradigm, but this applies to other languages as well 💡
I am flying to Vienna this week for the Effect Days ✈️
Next week is all about the conference and all the new innovations, learnings, and takeaways from it.
Trust me: you don't want to miss this 🤝
See you next 👋