newsletter

4 Principles for all programming languages

Different programming languages shares similar challenges. There are some common solutions to these problems: error handling, dependency injection, logging and observability.


Sandro Maglione

Sandro Maglione

Software development

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 and Option (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 🤩

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

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 🤓


I wrote a complete overview of how to rewrite plain Typescript code into Effect and what are the differences and advantages.

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 👋

Start here.

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.