Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement effect #1

Merged
merged 5 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Release

on: [push]

jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
steps:
- uses: actions/checkout@v3
- name: Prepare repository
run: git fetch --unshallow --tags
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Test
run: pnpm vitest run
- name: Build
run: pnpm run build
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
pnpm run auto shipit
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist

.env
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# simply-effect

#### better than all the rest 🎶

This is very simple wrapper around `Effect.gen` that makes sure that generator functions can be written in a cleaner way.

With plain `Effect`:

```tsx
import { Effect } from "effect";

export const getTodoById = (id: string) =>
Effect.gen(function* () {
const todoService = yield* TodoService;
const todo = yield* todoService.getTodoById("some-id");
if (todo.description.length < 2) {
yield* Effect.fail(new ValidationError("Too small description"));
}
return todo;
});
```

Using `simply-effect`:

```tsx
import { effect } from "simply-effect";

export const getTodoById = effect(function* (id: string) {
const todoService = yield* TodoService;
const todo = yield* todoService.getTodoById("some-id");
if (todo.description.length < 2) {
yield* Effect.fail(new ValidationError("Too small description"));
}
return todo;
});
```

If the generator function has no argument than `effect` will work exactly the same as `Effect.gen`.

```tsx
const value: Effect.Effect<number> = effect(function* () {
return yield* Effect.succeed(1);
});
```

It can work together with classes as well, but an extra type annotations for `this` is needed:

```tsx
class MyService {
readonly local = 1;
compute = effect(this, function* (this: MyService, add: number) {
return yield* Effect.succeed(this.local + add);
});
}
```
67 changes: 67 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from "vitest";
import { Console, Context, Effect } from "effect";
import { effect } from "./index.js";

function* getTodo(id: string) {
const todoService = yield* TodoService;
const todo = yield* todoService.getTodoById(id);
if (todo.description.length < 2) {
return yield* Effect.fail(new ValidationError("Too small description"));
}
return todo;
}

const wrappedInEffect = effect(function* (first: string, second: number) {
yield* Console.log(first);
yield* Console.log(second);
return first + second.toString();
});

test("effect", async () => {
const program = effect(function* () {
const id = yield* wrappedInEffect("id-", 1);
return yield* getTodo(id);
});
const todo = await program.pipe(
Effect.provideService(TodoService, {
getTodoById: effect(function* (id: string) {
return { description: "Learn effect", id };
}),
}),
Effect.scoped,
Effect.runPromise,
);
expect(todo).toEqual({
description: "Learn effect",
id: "id-1",
});
});

test("can pass this to generator", async () => {
class MyService {
readonly local = 1;
compute = effect(this, function* (this: MyService, add: number) {
return yield* Effect.succeed(this.local + add);
});
}
const instance = new MyService();

expect(Effect.runSync(instance.compute(2))).toBe(3);
});

export class NotFoundError extends Error {
readonly name = "NotFoundError";
}

export class ValidationError extends Error {
readonly name = "ValidationError";
}

class TodoService extends Context.Tag("TodoService")<
TodoService,
{
getTodoById(
id: string,
): Effect.Effect<{ description: string }, NotFoundError>;
}
>() {}
54 changes: 54 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Effect } from "effect";
import type { YieldWrap } from "effect/Utils";

type InferE<Eff extends YieldWrap<Effect.Effect<any, any, any>>> = [
Eff,
] extends [never]
? never
: [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>]
? E
: never;
type InferR<Eff extends YieldWrap<Effect.Effect<any, any, any>>> = [
Eff,
] extends [never]
? never
: [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>]
? R
: never;

export function effect<
Eff extends YieldWrap<Effect.Effect<any, any, any>>,
AEff,
>(
f: () => Generator<Eff, AEff, never>,
): Effect.Effect<AEff, InferE<Eff>, InferR<Eff>>;
export function effect<
Eff extends YieldWrap<Effect.Effect<any, any, any>>,
AEff,
Args extends any[],
>(
f: (...args: Args) => Generator<Eff, AEff, never>,
): (...args: Args) => Effect.Effect<AEff, InferE<Eff>, InferR<Eff>>;
export function effect<
Self,
Eff extends YieldWrap<Effect.Effect<any, any, any>>,
AEff,
>(
self: Self,
f: (this: Self) => Generator<Eff, AEff, never>,
): Effect.Effect<AEff, InferE<Eff>, InferR<Eff>>;
export function effect<
Eff extends YieldWrap<Effect.Effect<any, any, any>>,
AEff,
Args extends any[],
Self,
>(
self: Self,
f: (this: Self, ...args: Args) => Generator<Eff, AEff, never>,
): (...args: Args) => Effect.Effect<AEff, InferE<Eff>, InferR<Eff>>;
export function effect() {
const f =
arguments.length === 1 ? arguments[0] : arguments[1].bind(arguments[0]);
if (f.length === 0) return Effect.gen(f);
return (...args: any) => Effect.gen(() => f(...args));
}
53 changes: 53 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "simply-effect",
"version": "0.0.0",
"type": "module",
"description": "simply-effect: better than all the rest",
"author": "Kasper Peulen",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/kasperpeulen/simply-effect.git"
},
"homepage": "https://github.com/kasperpeulen/simply-effect",
"bugs": {
"url": "https://github.com/kasperpeulen/simply-effect/issues"
},
"exports": {
".": {
"types": "./dist/dts/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"test": "vitest",
"build": "tsc -b tsconfig.json",
"auto": "auto",
"release": "auto shipit"
},
"peerDepenVdencies": {
"effect": "^3.0.6"
},
"devDependencies": {
"auto": "^11.1.6",
"effect": "^3.0.6",
"prettier": "^3.2.5",
"tsx": "^4.7.3",
"typescript": "^5.4.5",
"vitest": "^1.5.2"
},
"files": [
"dist/**/*",
"README.md"
],
"publishConfig": {
"access": "public"
},
"auto": {
"plugins": [
"npm",
"released"
]
}
}
Loading
Loading