IMax153 / effect-docs-astro-showcase

https://effect-docs-astro-showcase.vercel.app
0 stars 0 forks source link

Landing page examples code #55

Open gcanti opened 1 day ago

gcanti commented 1 day ago

Here are the snippets under "Let's see some example code." I will add further considerations in the comments that follow.

Original Code # Basics ## Sync code ```ts const main = () => { console.log("Hello, World!") } main() ``` Effect ```ts import { Console, Effect } from "effect" const main = Console.log("Hello, World!") Effect.runSync(main) ``` ## Async code ```ts const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) const main = async () => { await sleep(1000) console.log("Hello, World!") } main() ``` Effect ```ts import { Console, Effect } from "effect" const main = Effect.sleep(1000).pipe( Effect.andThen(Console.log("Hello, World!")) ) Effect.runPromise(main) ``` ## Error Handling ```ts class CustomError extends Error { constructor(readonly value: number) {} } // Return type of `number` doesn't reflect the // fact that the function can throw const maybeFail = (): number => { const value = Math.random() if (value > 0.5) { throw new CustomError(value) } return value } const main = () => { try { const value = maybeFail() console.log(`Got value ${value}`) } catch (error) { if (error instanceof CustomError) { console.error(`Oops! Got value ${error.value}`) } else { console.error("No idea what happened!") } } } main() ``` Effect ```ts import { Console, Effect } from "effect" class CustomError { readonly _tag = "CustomError" constructor(readonly value: number) {} } const maybeFail: Effect.Effect< number, CustomError // type safety > = Effect.sync(() => Math.random()).pipe( Effect.andThen((value) => value > 0.5 ? Effect.fail(new CustomError(value)) : Effect.succeed(value) ) ) const main = maybeFail.pipe( Effect.andThen((value) => Console.log(`Got value ${value}`)), Effect.catchTag("CustomError", (error) => Console.error(`Oops! Got value ${error.value}`) ) ) Effect.runPromise(main) ``` ## Interruption ```ts const sleep = (ms: number, signal: AbortSignal): Promise => new Promise((resolve, reject) => { const timeout = setTimeout(resolve, ms) signal.addEventListener("abort", () => { clearTimeout(timeout) reject("Aborted!") }) }) async function main() { await sleep(1000, AbortSignal.timeout(500)) console.log("Hello") } main() ``` Effect ```ts import { Console, Effect } from "effect" const main = Effect.sleep(1000).pipe( Effect.andThen(Console.log("Hello")), Effect.timeoutFail({ duration: 500, onTimeout: () => "Aborted!" }) ) Effect.runPromise(main) ``` ## Retry ```ts async function getUser(id: number, retries = 3) { try { const response = await fetch(`/users/${id}`) if (!response.ok) throw new Error() return await response.json() } catch (error) { if (retries === 0) { throw error } return getUser(id, retries - 1) } } async function main() { const user = await getUser(1) console.log("Got user", user) } main() ``` Effect ```ts import { FetchHttpClient, HttpClient } from "@effect/platform" import { Console, Effect, Layer } from "effect" const makeUsers = Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk ) const findById = (id: number) => client.get(`/users/${id}`).pipe( Effect.andThen((response) => response.json), Effect.scoped, Effect.retry({ times: 3 }) ) return { findById } as const }) class Users extends Effect.Tag("Users")< Users, Effect.Effect.Success >() { static Live = Layer.effect(Users, makeUsers).pipe( Layer.provide(FetchHttpClient.layer) ) } const main = Users.findById(1).pipe( Effect.andThen((user) => Console.log("Got user", user)) ) ``` ## Concurrency ```ts declare const getUser: (id: number) => Promise function forEach( items: Array, concurrency: number, f: (a: A) => Promise ): Promise> { let index = 0 const results: Array = new Array(items.length) async function process(index: number) { const next = items[index] results[index] = await f(next) } async function worker() { while (index < items.length) { await process(index++) } } return Promise.all(Array.from({ length: concurrency }, worker)).then( () => results ) } const ids = Array.from({ length: 10 }, (_, i) => i) async function main() { const users = await forEach(ids, 3, (id) => getUser(id)) console.log("Got users", users) } main() ``` Effect ```ts import { Console, Effect } from "effect" declare const getUser: (id: number) => Effect.Effect const ids = Array.from({ length: 10 }, (_, i) => i) const main = Effect.forEach(ids, (id) => getUser(id), { concurrency: 3 }).pipe(Effect.andThen((users) => Console.log("Got users", users))) Effect.runPromise(main) ``` ## Composition ```ts // configuration has to be added to the type signature const getTodos = ( ids: Iterable, concurrency: number, signal?: AbortSignal ): Promise => forEach(ids, concurrency, (id) => withRetries(3, () => getTodo(id, signal)) ) const getTodo = (id: number, signal?: AbortSignal): Promise => abortable( (signal) => jsonOk(() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, { signal }) ), mergeAbortSignal(AbortSignal.timeout(1000), signal) ) async function main() { const ids = Array.from({ length: 10 }, (_, i) => i + 1) const todos = await getTodos(ids, 3, AbortSignal.timeout(10000)) console.log("Got todos", todos) } main() // helpers async function jsonOk(request: () => Promise): Promise { const res = await request() if (!res.ok) throw new Error("Response not ok") return res.json() } function forEach( items: Iterable, concurrency: number, f: (a: A) => Promise ): Promise> { let index = 0 const itemsArray = Array.from(items) const results: Array = new Array(itemsArray.length) async function process(index: number) { const next = itemsArray[index] results[index] = await f(next) } async function worker() { while (index < itemsArray.length) { await process(index++) } } return Promise.all(Array.from({ length: concurrency }, worker)).then( () => results ) } async function withRetries( retries: number, f: () => Promise ): Promise { try { return f() } catch (error) { if (retries === 0) { throw error } return withRetries(retries - 1, f) } } async function abortable( f: (signal: AbortSignal) => Promise, parentSignal?: AbortSignal ): Promise { if (parentSignal?.aborted) { throw new DOMException("Aborted", "AbortError") } const controller = new AbortController() parentSignal?.addEventListener("abort", () => controller.abort()) return await f(controller.signal) } function mergeAbortSignal( childSignal: AbortSignal, parentSignal?: AbortSignal ): AbortSignal { if (parentSignal?.aborted) { return parentSignal } else if (childSignal.aborted) { return childSignal } const controller = new AbortController() parentSignal?.addEventListener("abort", () => controller.abort()) childSignal.addEventListener("abort", () => controller.abort()) return controller.signal } ``` Effect ```ts const makeTodos = Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, HttpClient.mapRequest( HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com") ) ) // interuption and concurrency can be configured with // composition const list = ( ids: Iterable ): Effect.Effect< Array, HttpClientError.HttpClientError | Cause.TimeoutException > => Effect.forEach( ids, (id) => findById(id).pipe(Effect.retry({ times: 3 })), { concurrency: "inherit" } ) const findById = ( id: number ): Effect.Effect< unknown, HttpClientError.HttpClientError | Cause.TimeoutException > => client.get(`/todos/${id}`).pipe( Effect.andThen((response) => response.json), Effect.scoped, Effect.timeout("1 second") ) return { list, findById } as const }) class Todos extends Effect.Tag("Todos")< Todos, Effect.Effect.Success >() { static Live = Layer.effect(Todos, makeTodos).pipe( Layer.provide(FetchHttpClient.layer) ) } const main = Todos.list(Array.range(1, 10)).pipe( Effect.withConcurrency(3), Effect.timeout("10 seconds"), Effect.andThen((todos) => Console.log("Got todos", todos)) ) ``` # Ecosystem ## zod ```ts import { z } from "zod" const User = z.object({ username: z.string() }) User.parse({ username: "john_doe" }) // extract the inferred type type User = z.infer ``` Effect ```ts import { Schema } from "@effect/schema" const User = Schema.Struct({ username: Schema.String }) Schema.decodeUnknownSync(User)({ username: "john_doe" }) // extract the inferred type type User = Schema.Schema.Type ``` ## yup ```ts import * as yup from "yup" const User = yup.object({ username: yup.string().required() }) User.validate({ username: "john_doe" }) // extract the inferred type type User = yup.InferType ``` Effect ```ts import { Schema } from "@effect/schema" const User = Schema.Struct({ username: Schema.String }) Schema.decodeUnknownSync(User)({ username: "john_doe" }) // extract the inferred type type User = Schema.Schema.Type ``` ## superjson ```ts import superjson from "superjson" // encoding const json = superjson.stringify({ date: new Date(0) }) // '{"json":{"date":"1970-01-01T00:00:00.000Z"},"meta":{"values":{date:"Date"}}}' // decoding const obj = superjson.parse<{ date: Date }>(json) ``` Effect ```ts mport { Schema } from "@effect/schema" const schema = Schema.parseJson( Schema.Struct({ date: Schema.Date }) ) // encoding const json = Schema.encodeSync(schema)({ date: new Date(0) }) // '{"date":"1970-01-01T00:00:00.000Z"}' // decoding const obj = Schema.decodeUnknownSync(schema)(json) ``` ## rxjs ```ts import { interval, firstValueFrom } from "rxjs" import { take, map, toArray } from "rxjs/operators" const counts$ = interval(1000).pipe( take(5), map((x) => x * 2), toArray() ) firstValueFrom(counts$).then((x) => console.log(x)) ``` Effect ```ts import { Effect, Schedule, Stream } from "effect" const counts = Stream.fromSchedule(Schedule.spaced(1000)).pipe( Stream.take(5), Stream.map((x) => x * 2), Stream.runCollect ) Effect.runPromise(counts).then((x) => console.log(x)) ``` ## p-\* ```ts import pMap from "p-map" import pQueue from "p-queue" import pRetry from "p-retry" async function main() { const queue = new pQueue({ concurrency: 10 }) const signal = AbortSignal.timeout(10000) const todos = await pMap( Array.from({ length: 100 }, (_, i) => i + 1), (id) => queue.add(({ signal }) => fetchTodo(id, signal), { signal }) ) console.log(todos) } main().catch(console.error) // const fetchTodo = (id: number, signal?: AbortSignal): Promise => pRetry( async () => { const res = await fetch( `https://jsonplaceholder.typicode.com/todos/${id}`, { signal } ) if (!res.ok) throw new Error(res.statusText) return res.json() }, { retries: 3 } ) ``` Effect ```ts import { FetchHttpClient, HttpClient, HttpClientRequest } from "@effect/platform" import { Context, Effect, Layer, Schedule } from "effect" const main = Effect.gen(function* () { const todosService = yield* Todos const semaphore = yield* Effect.makeSemaphore(10) const todos = yield* Effect.forEach( Array.from({ length: 100 }, (_, i) => i + 1), (id) => semaphore.withPermits(1)(todosService.findById(id)), { concurrency: "unbounded" } ) console.log(todos) }).pipe(Effect.timeout("10 seconds")) // const makeTodos = Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, HttpClient.mapRequest( HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com") ) ) const findById = (id: number) => client.get(`/todos/${id}`).pipe( Effect.andThen((res) => res.json), Effect.scoped, Effect.retry({ schedule: Schedule.exponential(1000), times: 3 }) ) return { findById } as const }) class Todos extends Context.Tag("Todos")< Todos, Effect.Effect.Success >() { static Live = Layer.effect(Todos, makeTodos).pipe( Layer.provide(FetchHttpClient.layer) ) } main.pipe(Effect.provide(Todos.Live), Effect.runPromise) ``` ## ts-results ```ts import { existsSync, readFileSync } from "fs" import { Ok, Err, Result } from "ts-results" function readFile(path: string): Result { if (existsSync(path)) { return new Ok(readFileSync(path, "utf8")) } else { return new Err("invalid path") } } ``` Effect ```ts import { existsSync, readFileSync } from "fs" import { Effect } from "effect" function readFile(path: string): Effect.Effect { if (existsSync(path)) { return Effect.succeed(readFileSync(path, "utf8")) } else { return Effect.fail("invalid path") } } ``` ## fetch ```ts const fetchTodo = async ( id: number, signal?: AbortSignal ): Promise => { const res = await fetch( `https://jsonplaceholder.typicode.com/todos/${id}`, { signal } ) if (!res.ok) throw new Error(res.statusText) return res.json() } ``` Effect ```ts import { HttpClient, HttpClientError, HttpClientRequest } from "@effect/platform" import { Effect } from "effect" const fetchTodo = ( id: number ): Effect.Effect< unknown, HttpClientError.HttpClientError, HttpClient.HttpClient.Service > => Effect.gen(function* () { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.filterStatusOk, HttpClient.mapRequest( HttpClientRequest.prependUrl( "https://jsonplaceholder.typicode.com" ) ) ) const res = yield* client.get(`/todos/${id}`) return yield* res.json }).pipe(Effect.scoped) ``` ## express ```ts import Express from "express" const app = Express() app.get("/", (_req, res) => { res.json({ message: "Hello World" }) }) app.listen(3000) ``` Effect ```ts import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" import { Layer } from "effect" import { createServer } from "node:http" const app = HttpRouter.empty.pipe( HttpRouter.get("/", HttpServerResponse.json({ message: "Hello World" })) ) const HttpLive = HttpServer.serve(app).pipe( Layer.provide( NodeHttpServer.layer(() => createServer(), { port: 3000 }) ) ) NodeRuntime.runMain(Layer.launch(HttpLive)) ```
gcanti commented 1 day ago

Sync code

Would using Effect.gen and Random be a better example? I'm not sure what the goal is here, but at best, this is just as cumbersome as using the native code.

Proposal:

const main = () => {
  const value = Math.random()
  console.log(`Got value ${value}`)
}

main()
import { Effect, Random } from "effect"

const main = Effect.gen(function* (_) {
  const value = yield* Random.next
  console.log(`Got value ${value}`)
})

Effect.runSync(main)

Async code

Why not use Effect.gen? I'm not sure what the goal is here, but at best, this is just as cumbersome as using the native code.

Proposal:

import { Effect } from "effect"

const main = Effect.gen(function* (_) {
  yield* Effect.sleep("1 second")
  console.log("Hello, World!")
})

Effect.runPromise(main)

Error Handling

Why not use Effect.gen and Random?

Proposal:

class CustomError extends Error {
-  constructor(readonly value: number) {}
+  constructor(readonly value: number) {
+    super()
+  }
}

+// The return type `number` doesn't account
+// for the possibility of throwing an error
-// Return type of `number` doesn't reflect the
-// fact that the function can throw
const maybeFail = (): number => {
  const value = Math.random()
  if (value > 0.5) {
    throw new CustomError(value)
  }
  return value
}

const main = () => {
  try {
    const value = maybeFail()
    console.log(`Got value ${value}`)
  } catch (error) {
    if (error instanceof CustomError) {
      console.error(`Oops! Got value ${error.value}`)
    } else {
      console.error("No idea what happened!")
    }
  }
}

main()
import { Effect, Random, Console } from "effect"

class CustomError extends Error {
  constructor(readonly value: number) {
    super()
  }
}

const maybeFail: Effect.Effect<
  number,
  CustomError // Ensures type safety
> = Effect.gen(function* (_) {
  const value = yield* Random.next
  if (value > 0.5) {
    throw new CustomError(value)
  }
  return value
})

const main = Effect.gen(function* (_) {
  const value = yield* maybeFail
  console.log(`Got value ${value}`)
}).pipe(
  Effect.catchAll((error) =>
    Console.error(`Oops! Got value ${error.value}`)
  )
)

Effect.runPromise(main)

Interruption

Why not use Effect.gen?

Proposal:

import { Effect } from "effect"

const main = Effect.gen(function* (_) {
  yield* Effect.sleep("1 second")
  console.log("Hello")
}).pipe(
  Effect.timeoutFail({
    duration: "500 millis",
    onTimeout: () => "Aborted!"
  })
)

Effect.runPromise(main)

Retry

  • Should native code use p-retry?
  • Why not use Effect.gen?
  • A Tag is unnecessary.

Proposal:

import { FetchHttpClient, HttpClient } from "@effect/platform"
import { Effect } from "effect"

const getUser = (id: number, retries = 3) =>
  HttpClient.get(`/users/${id}`).pipe(
    Effect.andThen((response) => response.json),
    Effect.scoped,
    Effect.retry({ times: retries }),
    Effect.provide(FetchHttpClient.layer)
  )

const main = Effect.gen(function* () {
  const user = yield* getUser(1)
  console.log("Got user", user)
})

Effect.runPromise(main)

Concurrency

  • The native code is difficult to follow, switch to using p-map?

Proposal:

const getUser = (id: number) =>
  Promise.resolve({ id, name: `user ${id}` })

async function forEach<A, B>(
  items: Array<A>,
  concurrency: number,
  f: (a: A) => Promise<B>
): Promise<Array<B>> {
  let index = 0
  const results: Array<B> = new Array(items.length)
  async function process(index: number) {
    const next = items[index]!
    results[index] = await f(next)
  }
  async function worker() {
    while (index < items.length) {
      await process(index++)
    }
  }
  await Promise.all(Array.from({ length: concurrency }, worker))
  return results
}

const ids = Array.from({ length: 10 }, (_, i) => i)

async function main() {
  const users = await forEach(ids, 3, (id) => getUser(id))
  console.log("Got users", users)
}

main()

or (using p-map)

import pMap from "p-map"

const getUser = (id: number) =>
  Promise.resolve({ id, name: `user ${id}` })

const ids = [1, 2, 3, 4, 5]

async function main() {
  const users = await pMap(ids, getUser, { concurrency: 3 })
  console.log("Got users", users)
}

main()
import { Effect } from "effect"

const getUser = (id: number) => Effect.succeed({ id, name: `user ${id}` })

const ids = [1, 2, 3, 4, 5]

const main = Effect.gen(function* () {
  const users = yield* Effect.forEach(ids, getUser, { concurrency: 3 })
  console.log("Got users", users)
})

Effect.runPromise(main)

Composition

The example is too long and difficult to follow, remove it?

zod

Just a small improvement.

Proposal:

import { Schema } from "@effect/schema"

const User = Schema.Struct({
  username: Schema.String
})

Schema.decodeUnknownSync(User)({
  username: "john_doe"
})

// extract the inferred type
-type User = Schema.Schema.Type<typeof User>
+type User = typeof User.Type

yup

It's redundant and has fewer downloads than Zod, remove it? https://npmtrends.com/superjson-vs-yup-vs-zod

superjson

It's redundant and has fewer downloads than Zod, remove it? https://npmtrends.com/superjson-vs-yup-vs-zod

rxjs

Just a small improvement.

Proposal:

import { Effect, Schedule, Stream } from "effect"

-const counts = Stream.fromSchedule(Schedule.spaced(1000)).pipe(
+const counts = Stream.fromSchedule(Schedule.spaced("1 second")).pipe(
  Stream.take(5),
  Stream.map((x) => x * 2),
  Stream.runCollect
)

Effect.runPromise(counts).then((x) => console.log(x))

p-*

This example is too long and complicated, remove it?

ts-results

Switch to neverthrow, which has more downloads: https://npmtrends.com/@mobily/ts-belt-vs-neverthrow-vs-ts-results

Proposal:

import { existsSync, readFileSync } from "fs"
import { Ok, Err, Result } from "neverthrow"

function readFile(path: string): Result<string, "invalid path"> {
  if (existsSync(path)) {
    return new Ok(readFileSync(path, "utf8"))
  } else {
    return new Err("invalid path")
  }
}

fetch

It's more complex than native alternatives, remove it?

express

It's more complex than native alternatives, remove it?