Was ist Effect? Das ist eine schwierige Frage, die ich mit ein paar Beispielen beantworten möchte.
Man kann sich `Effect` vereinfacht als eine JS-Funktion mit Superpower vorstellen.
import { Effect } from 'effect'
// Dies entspricht vereinfacht etwa
// const program = () => 42
const program = Effect.succeed(42)
// Effects müssen ausgeführt werden, um den Wert zu erhalten
const result = Effect.runSync(program)
console.log('Hello World', result)
Das obige Beispiel hat aber noch keinen Vorteil gegenüber einer einfachen Funktion.
Die interessanten Fähigkeiten von `Effect` sind die Fehlerbehandlung und wie Services implementiert werden. Dies wird auch im Typ von `Effect` sichtbar.
import { Effect } from 'effect'
// Der Type von program ist dieser:
// Effect.Effect<number, NumberNotFound, NumberService>
const program = NumberService.getNumber
Dieses Programm kann den Fehler `NumberNotFound` werfen und benötigt den Service `NumberService`. Den Fehler muss ich nicht behandeln, wenn ich das nicht will, aber den Service muss ich dem Programm zur Verfügung stellen, sonst läuft es nicht (siehe Kapitel Services).
Fehlerbehandlung
Wenn ein Fehler auftritt, gibt es viele Möglichkeiten, wie ich damit umgehen kann. Entweder ich versuche den Effekt erneut auszuführen, ich stelle einen Default zur Verfügung, ich zeige den Fehler dem Benutzer an, etc.
// Effect.Effect<number, never, NumberService>
const program = NumberService.getNumber().pipe(
// Bei Fehler nochmals versuchen
Effect.retry({ times: 1 }),
// und sonst 7 als default nutzen
Effect.orElseSucceed(() => 7),
)
In diesem Beispiel wird einmal versucht, die Nummer erneut zu erhalten und ansonsten eine 7 zurückgegeben. Im `Effect` Typ ist der mittlere Typ nun auch `never`, da immer eine Nummer zurückgegeben wird.
Services
Im obigen Beispiel ist ein `NumberService` zu sehen, der den Effekt `getNumber` zur Verfügung stellt. Die Implementierung dieses Services wird erst vor der Ausführung des Programms zur Verfügung gestellt. Dadurch können je nach Situation (z.B. Produktions- oder Testumgebung) unterschiedliche Implementierungen zur Verfügung gestellt werden.
Der oben genannte Service könnte z.B. wie folgt implementiert werden:
// service.ts
import { Effect, Layer } from 'effect'
// Implementation
const make = {
getNumber: Effect.tryPromise(() => new Promise<number>((resolve) => setTimeout(() => resolve(42), 1000))),
plusOne: (a: number) => Effect.sync(a + 1),
}
// Eindeutigen Service mit dem Typ
export class NumberService extends Effect.Tag('@services/Number')<
NumberService,
typeof make
>() {
// Implementation "exportieren". Kann aber auch als Funktion
// geschehen. Hat nichts direkt mit der Klasse zu tun.
static Live = Layer.succeed(this, make)
}
Um diesen Service dem Programm zur Verfügung zu stellen, muss er noch "bereitgestellt" werden.
import { Console, Effect, Layer } from 'effect'
import { NumberService } from './service'
// Braucht den Service 👇
// Effect.Effect<number, never, NumberService>
const program = NumberService.getNumber
// Service zur Verfügung stellen (bereitstellen)
// Braucht keine weiteren Service 👇
// Effect.Effect<number, never, never>
const runnable = Effect.provide(program, NumberService.Live)
// Effect ausführen
Effect.runPromise(runnable)
Mit `Effect` ist es üblich, einem Service einen anderen Service zur Verfügung zu stellen und die Implementierung aller Services erst vor der Ausführung zur Verfügung zu stellen. So kann z.B. die DB einer Anwendung im Testfall ein Mock sein und die anderen Services, die die DB verwenden, werden normal genutzt.
Effects kombinieren
Ein Effect ist ein Legostein. Richtig cool wird es aber erst, wenn man mehrere dieser Legosteine zusammensetzen kann. Dafür gibt es verschiedene Kombinatoren und Techniken. Hier eine kleine Auswahl:
import { Console, Effect, pipe } from 'effect'
// Als Pipe kombinieren
const program1 = pipe(
Console.log('Starting program...'),
Effect.flatMap(() => Effect.succeed(42)),
Effect.map((n) => n + 1),
Effect.map((n) => `The answer is ${n}`),
)
// das gleich als Generator
const program2 = Effect.gen(function* () {
yield* Console.log('Starting program...')
const number = yield* Effect.succeed(42)
const addedUpNumber = number + 1
return `The answer is ${addedUpNumber}`
})
Infos zu Effect
Effect lernt man nicht an einem Nachmittag. Aber mit wenig Wissen kann man viel aus der Bibliothek herausholen. Mehr Infos gibt es hier: