Effect TS: Difference between revisions
(2 intermediate revisions by the same user not shown) | |||
Line 85: | Line 85: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Handling errors= | =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.<br> | |||
==For errors== | |||
You just make a unique tag and good to go | |||
<syntaxhighlight lang="ts"> | |||
class FetchError { | |||
readonly _tag = "FetchError" | |||
} | |||
class DecodeError { | |||
readonly _tag = "DecodeError" | |||
} | |||
class SameWeightError { | |||
readonly _tag = "SameWeightError" | |||
constructor(readonly weight: number) {} | |||
} | |||
</syntaxhighlight> | |||
==For CatchTag== | |||
We added the catch inline with the call. | |||
<syntaxhighlight lang="ts"> | |||
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}`)) | |||
}) | |||
</syntaxhighlight> | |||
==For CatchAll== | |||
We separated out the functions and assigned the right error | |||
<syntaxhighlight lang="ts"> | |||
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 }), | |||
) | |||
</syntaxhighlight> | |||
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. | |||
<syntaxhighlight lang="ts"> | |||
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 })) | |||
) | |||
</syntaxhighlight> | |||
==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. <br> | |||
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. | |||
<syntaxhighlight lang="ts"> | <syntaxhighlight lang="ts"> | ||
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) | |||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 05:29, 24 June 2025
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)