Effect TS

From bibbleWiki
Jump to navigation Jump to search

Introduction

Dipped my toe into this. It seems to be a combination of RxJs and Microsoft DI. Not sure why it is worthwhile using types rather than a typed language. If all you have is TS resource then it makes sense. Make change my mind. Useful examples can be found here

First Program

And its not hello world

import { Console, Effect, pipe, Schema } from 'effect';

const Pokemon = Schema.Struct({
  name: Schema.String,
  weight: Schema.Number,
});

type Pokemon = Schema.Schema.Type<typeof Pokemon>;

const getPokemon = (id: number) => 
    pipe(
       Effect.tryPromise({
        try: () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
            .then(response => response.json()),
        catch: (unknown) => new Error(`Failed to fetch Pokemon with id ${id}, Error %s ${unknown}`),
        }),
        Effect.flatMap((data) => Schema.decodeUnknown(Pokemon)((data))),
    )

const getRandomNumberArray = Effect.all(
    Array.from({ length: 10 }, () => 
        Effect.sync(
            () => Math.floor(Math.random() * 100) +1
        )
    )
)

 const calculateHeaviestPokemon = (pokemons: Pokemon[]) => 
    Effect.reduce(pokemons, 0, (highest, pokemon) =>
        pokemon.weight === highest 
            ? Effect.fail(new Error("Two pokemons have the same weight"))
            : Effect.succeed(pokemon.weight > highest ? pokemon.weight : highest)
    )

const program = pipe(
    getRandomNumberArray,
    Effect.flatMap((arr) =>
        Effect.all(
            arr.map((arr) => getPokemon(arr))
        )
    ),
    Effect.tap((pokemons) =>
    Effect.log("\n" + pokemons.map((pokemon) => `${pokemon.name} ${pokemon.weight}`).join("\n"))),
    Effect.flatMap((pokemon) => calculateHeaviestPokemon(pokemon)),
    Effect.flatMap((heaviestWeight) => 
        Effect.log(`The heaviest Pokemon weighs: ${heaviestWeight}`)    
    ),
)

Effect.runPromise(program).then(Console.log)

Second Program

Same bat time same bat channel but this time with generators

const getPokemonB = (id: number) => 
    Effect.gen(function* (_) {
        const res = yield* _(
            Effect.tryPromise({
                try: () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
                    .then(response => response.json()),
                catch: (unknown) => new Error(`Failed to fetch Pokemon with id ${id}, Error %s ${unknown}`),
            }),
        )
        return yield* _(Schema.decodeUnknown(Pokemon)((res)))
    }
)


const programB = Effect.gen(function* (_) {
    const arr = yield* _(getRandomNumberArray)
    const pokemons = yield* _(Effect.all(arr.map(getPokemonB)))
    yield* _(Effect.log("\n" + pokemons.map( (pokemon) => `${pokemon.name} ${pokemon.weight}`).join("\n")))

    const heaviestWeight = yield* _(calculateHeaviestPokemon(pokemons))
    yield* _(Effect.log(`The heaviest Pokemon weighs: ${heaviestWeight}`))
})

Effect.runPromise(programB).then(Console.log)

Handling errors

So this took a bit of time to figure out but you know me, one example I understand a good to go. Here I implement catchAll and catchTag. The later has changed since the video. I chose to stick with generators as they look better. So the video did not show me how either. I this example I wrapped the getPokemon in catchAll but with the individual gone error It after where the error could occur.

For errors

You just make a unique tag and good to go

class FetchError {
    readonly _tag = "FetchError"
}

class DecodeError {
    readonly _tag = "DecodeError"
}

class SameWeightError {
    readonly _tag = "SameWeightError"
    constructor(readonly weight: number) {}
}

For CatchTag

We added the catch inline with the call.

const calculateHeaviestPokemonC = (pokemons: Pokemon[]) => 
    Effect.reduce(pokemons, 0, (highest, pokemon) =>
        pokemon.weight === highest 
            ? Effect.fail(new SameWeightError(pokemon.weight))
            : Effect.succeed(pokemon.weight > highest ? pokemon.weight : highest)
    )

const programC = Effect.gen(function* (_) {
    const arr = yield* _(getRandomNumberArray)
    const pokemons = yield* _(Effect.all(arr.map(getPokemonC)))
    yield* _(Effect.log("\n" + pokemons.map( (pokemon) => `${pokemon.name} ${pokemon.weight}`).join("\n")))
    const heaviestWeight = yield* _(calculateHeaviestPokemonC(pokemons))

    Effect.catchTag(
        calculateHeaviestPokemonC(pokemons),
         "SameWeightError", (err: SameWeightError) =>
            Effect.log(`Two pokemons have the same weight: ${err.weight}`)
         ),
    
    yield* _(Effect.log(`The heaviest Pokemon weighs: ${heaviestWeight}`))
})

For CatchAll

We separated out the functions and assigned the right error

const getPokemonC = (id: number) => 
    Effect.catchAll(
        Effect.gen(function* (_) {
            const res = yield* _(

                Effect.tryPromise({
                    try: () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`),
                    catch: () => new FetchError(),
                }))

            return yield* _(            
                Effect.tryPromise({
                    try: () => (res.json()),
                    catch: () => new DecodeError(),
                })
            )
        }
    ),
    () => Effect.succeed({ name: "Unknown", weight: 0 }),
)

If I had waited I would have seen how they did it but I wanted to try myself. The second approach uses pipe instead of wrapping. I think they will behave the same.

const getPokemonC = (id: number) =>
    Effect.gen(function* (_) {
        const res = yield* _(

            Effect.tryPromise({
                try: () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`),
                catch: () => new FetchError(),
            }))

        return yield* _(
            Effect.tryPromise({
                try: () => (res.json()),
                catch: () => new DecodeError(),
            })
        )
    }
    ).pipe
        (
            Effect.catchAll(() => Effect.succeed({ name: "Unknown", weight: 0 }))
        )

Next up Requirements of DI

This is where you create an interface and provide an implementation for it. This took a while because it was new and there was a change between v2 and v3. They swapped parameters around.
In v2 it is

  • R - Requirement (Context/Dependencies)
  • E - Error
  • A - Success value

In v3 it is

  • A - Success value
  • E - Error
  • R - Requirement (Context/Dependencies)

Anyway, they have also changed how they do errors along with how to define a schema - oh and how to run with a service - argghhhhhhhhhhhhhhh.

import { Context, Data, Effect, Schema } from 'effect';
import { ParseError } from 'effect/ParseResult';

// Define the Pokemon schema
class Pokemon extends Schema.Class<Pokemon>("Pokemon")({
    name: Schema.String,
    weight: Schema.Number,
}) { }


class FetchError extends Data.TaggedError("FetchError")<{}> { }
class JsonError extends Data.TaggedError("JsonError")<{}> { }

class SameWeightError extends Data.TaggedError("SameWeightError")<{
    weight: number
}> { }

// Define a function to get a random number array
const getRandomNumberArray = Effect.all(
    Array.from({ length: 10 }, () =>
        Effect.sync(
            () => Math.floor(Math.random() * 100) + 1
        )
    )
)

// Define a function to find heaviest Pokemon
const calculateHeaviestPokemon = (pokemons: Pokemon[]) =>
    Effect.reduce(pokemons, 0, (highest, pokemon) =>
        pokemon.weight === highest
            ? Effect.fail(new SameWeightError({ weight: pokemon.weight }))
            : Effect.succeed(pokemon.weight > highest ? pokemon.weight : highest)
    )

const getPokemon = (id: number) =>
    Effect.gen(function* (_) {
        const client = yield* _(PokemonClient)
        return yield* _(client.getById(id))
    }).pipe(
        Effect.catchAll(
            () => Effect.succeed({ name: "Unknown", weight: 0 })
        )
    )

// Define the program using Effect.gen
const program = Effect.gen(function* (_) {
    const arr = yield* _(getRandomNumberArray)
    const pokemons = yield* _(Effect.all(arr.map(getPokemon)))
    yield* _(Effect.log("\n" + pokemons.map((pokemon) => `${pokemon.name} ${pokemon.weight}`).join("\n")))
    const heaviestWeight = yield* _(calculateHeaviestPokemon(pokemons))

    Effect.catchTag(
        calculateHeaviestPokemon(pokemons),
        "SameWeightError", (err: SameWeightError) =>
        Effect.log(`Two pokemons have the same weight: ${err.weight}`)
    ),

        yield* _(Effect.log(`The heaviest Pokemon weighs: ${heaviestWeight}`))
})

// Define the PokemonClient interface
interface PokemonClientImpl {
    getById: (id: number) =>
        Effect.Effect<Pokemon, FetchError | JsonError | ParseError, never>;
};


// Create a service tag for the PokemonClient
class PokemonClient extends Context.Tag("@app/PokemonClient")<PokemonClient, PokemonClientImpl>() { }

// Define a function to fetch Pokemon data
const fetchRequest = (id: number) => Effect.tryPromise({
    try: () => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`),
    catch: () => new FetchError(),
});

// Define a function to decode the JSON response
const jsonResponse = (response: Response) =>
    Effect.tryPromise({
        try: () => response.json(),
        catch: () => new JsonError(),
    });

const decodePokemon = Schema.decodeUnknown(Pokemon);

const getPokemonById = (id: number) =>
    Effect.gen(function* () {
        const response = yield* fetchRequest(id);
        if (!response.ok) {
            return yield* new FetchError();
        }

        const json = yield* jsonResponse(response);
        return yield* decodePokemon(json);
    }
    )

// Define the implementation of the PokemonClient getById
const pokemonClientLive: PokemonClientImpl = {
    getById: (id: number) => getPokemonById(id)
}

// Define the runnable program with the PokemonClient service
const runnable = program.pipe(
    Effect.provideService(PokemonClient, pokemonClientLive)
)

Effect.runPromise(runnable)