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.

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)