From f900ebb96f7c5edb425f653e34fca4fc5dc38148 Mon Sep 17 00:00:00 2001 From: Alexander Gribochkin Date: Wed, 14 Sep 2022 00:04:10 +0300 Subject: [PATCH 1/2] feat(identity): Added monad (#45) --- identity/LICENSE | 21 ++ identity/README.md | 493 ++++++++++++++++++++++++++++++++++++++++++ identity/index.ts | 100 +++++++++ identity/package.json | 36 +++ 4 files changed, 650 insertions(+) create mode 100644 identity/LICENSE create mode 100644 identity/README.md create mode 100644 identity/index.ts create mode 100644 identity/package.json diff --git a/identity/LICENSE b/identity/LICENSE new file mode 100644 index 0000000..d7c59aa --- /dev/null +++ b/identity/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2022 Artem Kobzar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/identity/README.md b/identity/README.md new file mode 100644 index 0000000..9046771 --- /dev/null +++ b/identity/README.md @@ -0,0 +1,493 @@ +# @sweet-monads/identity + +[Either Monad](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html), The Either monad represents values with two possibilities: a value of type Either a b is either Left a or Right b. + +### This library belongs to _sweet-monads_ project + +> **sweet-monads** — easy-to-use monads implementation with static types definition and separated packages. + +- No dependencies, one small file +- Easily auditable TypeScript/JS code +- Check out all libraries: + [either](https://github.com/JSMonk/sweet-monads/tree/master/either), + [iterator](https://github.com/JSMonk/sweet-monads/tree/master/iterator), + [interfaces](https://github.com/JSMonk/sweet-monads/tree/master/interfaces), + [maybe](https://github.com/JSMonk/sweet-monads/tree/master/maybe) + +## Usage + +> npm install @sweet-monads/identity + +```typescript +import { Either, right } from "@sweet-monads/either"; + +class UserNotFoundError extends Error { + name: "UserNotFoundError"; +} +type User = { email: string; password: string }; + +function getUser(id: number): Either { + return right({ email: "test@gmail.com", password: "test" }); +} + +// Either +const user = getUser(1).map(({ email }) => email); +``` + +## API + +- [@sweet-monads/identity](#sweet-monadsidentity) + - [This library belongs to _sweet-monads_ project](#this-library-belongs-to-sweet-monads-project) + - [Usage](#usage) + - [API](#api) + - [`chain`](#chain) + - [`merge`](#merge) + - [`mergeInOne`](#mergeinone) + - [`mergeInMany`](#mergeinmany) + - [`left`](#left) + - [`right`](#right) + - [`from`](#from) + - [`isEither`](#iseither) + - [`Either#isLeft`](#eitherisleft) + - [`Either#isRight`](#eitherisright) + - [`Either#or`](#eitheror) + - [`Either#join`](#eitherjoin) + - [`Either#map`](#eithermap) + - [`Either#mapRight`](#eithermapright) + - [`Either#mapLeft`](#eithermapleft) + - [`Either#asyncMap`](#eitherasyncmap) + - [`Either#apply`](#eitherapply) + - [`Either#asyncApply`](#eitherasyncapply) + - [`Either#chain`](#eitherchain) + - [`Either#asyncChain`](#eitherasyncchain) + - [Helpers](#helpers) + - [License](#license) + +#### `chain` + +```typescript +function chain(fn: (v: R) => Promise>): (m: Either) => Promise>; +``` + +- `fn: (v: R) => Promise>` - function which should be applied asynchronously to `Either` value +- Returns function with `Either` argument and promisied `Either` with new error or maped by `fn` value (could be used inside `Promise#then` function). + +Example: + +```typescript +const getValue = async () => right(1); + +// Either +const result = await getValue() + .then(Either.chain(async v => right(v * 2))) + .then(Either.chain(async v => left(new TypeError("Unexpected")))); +``` + +#### `merge` + +Alias for [`mergeInOne`](#mergeinone) + +```typescript +function merge(values: [Either]): Either; +function merge(values: [Either, Either]): Either; +function merge( + values: [Either, Either, Either] +): Either; +// ... until 10 elements +``` + +- `values: Array>` - Array of Either values which will be merged into Either of Array +- Returns `Either>` which will contain `Right>` if all of array elements was `Right` otherwise `Left`. + +Example: + +```typescript +const v1 = right(2); // Either.Right +const v2 = right("test"); // Either.Right +const v3 = left(new Error()); // Either.Left + +merge([v1, v2]); // Either.Right +merge([v1, v2, v3]); // Either.Left +``` + +#### `mergeInOne` + +```typescript +function merge(values: [Either]): Either; +function merge(values: [Either, Either]): Either; +function merge( + values: [Either, Either, Either] +): Either; +// ... until 10 elements +``` + +- `values: Array>` - Array of Either values which will be merged into Either of Array +- Returns `Either>` which will contain `Right>` if all of array elements was `Right` otherwise `Left`. + +Example: + +```typescript +const v1 = right(2); // Either.Right +const v2 = right("test"); // Either.Right +const v3 = left(new Error()); // Either.Left + +merge([v1, v2]); // Either.Right +merge([v1, v2, v3]); // Either.Left +``` + +#### `mergeInMany` + +```typescript +function mergeInMany(values: [Either]): Either, [R1]>; +function mergeInMany(values: [Either, Either]): Either, [R1, R2]>; +function mergeInMany( + values: [Either, Either, Either] +): Either, [R1, R2, R3]>; +// ... until 10 elements +``` + +- `values: Array>` - Array of Either values which will be merged into Either of Array +- Returns `Either, Array>` which will contain `Right>` if all of array elements was `Right` otherwise array of all catched `Left` values. + +Example: + +```typescript +const v1 = right(2); // Either.Right +const v2 = right("test"); // Either.Right +const v3 = left(new Error()); // Either.Left + +merge([v1, v2]); // Either, [number, string]>.Right +merge([v1, v2, v3]); // Either, [number, string, boolean]>.Left +``` + +#### `left` + +```typescript +function left(value: L): Either; +``` + +- Returns `Either` with `Left` state which contain value with `L` type. + Example: + +```typescript +const v1 = left(new Error()); // Either.Left +const v2 = left(new Error()); // Either.Left +``` + +#### `right` + +```typescript +function right(value: R): Either; +``` + +- Returns `Either` with `Right` state which contain value with `R` type. + Example: + +```typescript +const v1 = right(2); // Either.Right +const v2 = right(2); // Either.Right +``` + +#### `from` + +The same as [`right`](#right) + +Return only `Right` typed value. + +```typescript +function from(value: R): Either; +``` + +- Returns `Either` with `Right` state which contain value with `R` type. + Example: + +```typescript +from(2); // Either.Right +``` + +#### `isEither` + +```typescript +function isEither(value: unknown | Either): value is Either; +``` + +- Returns `boolean` if given `value` is instance of Either constructor. + Example: + +```typescript +const value: unknown = 2; +if (isEither(value)) { + // ... value is Either at this block +} +``` + +#### `Either#isLeft` + +```typescript +function isLeft(): boolean; +``` + +- Returns `true` if state of `Either` is `Left` otherwise `false` + Example: + +```typescript +const v1 = right(2); +const v2 = left(2); + +v1.isLeft(); // false +v2.isLeft(); // true +``` + +#### `Either#isRight` + +```typescript +function isRight(): boolean; +``` + +- Returns `true` if state of `Either` is `Right` otherwise `false` + Example: + +```typescript +const v1 = right(2); +const v2 = left(2); + +v1.isRight(); // true +v2.isRight(); // false +``` + +#### `Either#or` + +```typescript +function or(x: Either): Either; +``` + +- Returns `Either`. If state of `this` is `Right` then `this` will be returned otherwise `x` argument will be returned + Example: + +```typescript +const v1 = right(2); +const v2 = left("Error 1"); +const v3 = left("Error 2"); +const v4 = right(3); + +v1.or(v2); // v1 will be returned +v2.or(v1); // v1 will be returned +v2.or(v3); // v3 will be returned +v1.or(v4); // v1 will be returned + +v2.or(v3).or(v1); // v1 will be returned +v2.or(v1).or(v3); // v1 will be returned +v1.or(v2).or(v3); // v1 will be returned +``` + +#### `Either#join` + +```typescript +function join(this: Either>): Either; +``` + +- `this: Either>` - `Either` instance which contains other `Either` instance as `Right` value. +- Returns unwrapped `Either` - if current `Either` has `Right` state and inner `Either` has `Right` state then returns inner `Either` `Right`, if inner `Either` has `Left` state then return inner `Either` `Left` otherwise outer `Either` `Left`. + Example: + +```typescript +const v1 = right(right(2)); +const v2 = right(left(new Error())); +const v3 = left>(new TypeError()); + +v1.join(); // Either.Right with value 2 +v2.join(); // Either.Left with value new Error +v3.join(); // Either.Left with value new TypeError +``` + +#### `Either#map` + +```typescript +function map(fn: (val: R) => NewR): Either; +``` + +- Returns mapped by `fn` function value wrapped by `Either` if `Either` is `Right` otherwise `Left` with `L` value + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +const newVal1 = v1.map(a => a.toString()); // Either.Right with value "2" +const newVal2 = v2.map(a => a.toString()); // Either.Left with value new Error() +``` + +#### `Either#mapRight` + +```typescript +function mapRight(fn: (val: R) => NewR): Either; +``` + +The same as [`Either#map`](#eithermap) + +- Returns mapped by `fn` function value wrapped by `Either` if `Either` is `Right` otherwise `Left` with `L` value + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +const newVal1 = v1.map(a => a.toString()); // Either.Right with value "2" +const newVal2 = v2.map(a => a.toString()); // Either.Left with value new Error() +``` + +#### `Either#mapLeft` + +```typescript +function mapLeft(fn: (val: L) => NewL): Either; +``` + +- Returns mapped by `fn` function value wrapped by `Either` if `Either` is `Left` otherwise `Right` with `R` value + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +const newVal1 = v1.mapLeft(a => a.toString()); // Either.Right with value 2 +const newVal2 = v2.mapLeft(a => a.toString()); // Either.Left with value "Error" +``` + +##### `Either#asyncMap` + +```typescript +function asyncMap(fn: (val: R) => Promise): Promise>; +``` + +- Returns `Promise` with mapped by `fn` function value wrapped by `Either` if `Either` is `Right` otherwise `Left` with value `L` + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +// Promise.Right> with value "2" +const newVal1 = v1.asyncMap(a => Promise.resolve(a.toString())); +// Promise.Left> with value new Error() +const newVal2 = v2.asyncMap(a => Promise.resolve(a.toString())); +``` + +##### `Either#apply` + +```typescript +function apply(this: Either B>, arg: Either): Either; +function apply(this: Either, fn: Either B>): Either; +``` + +- `this | fn` - function wrapped by Either, which should be applied to value `arg` +- `arg | this` - value which should be applied to `fn` +- Returns mapped by `fn` function value wrapped by `Either` if `Either` is `Right` otherwise `Left` with `L` value + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); +const fn1 = right number>((a: number) => a * 2); +const fn2 = left number>(new Error()); + +const newVal1 = fn1.apply(v1); // Either.Right with value 4 +const newVal2 = fn1.apply(v2); // Either.Left with value new Error() +const newVal3 = fn2.apply(v1); // Either.Left with value new Error() +const newVal4 = fn2.apply(v2); // Either.Left with value new Error() +``` + +##### `Either#asyncApply` + +Async variant of [`Either#apply`](#eitherapply) + +```typescript +function asyncApply(this: Either Promise>, arg: Either | A>): Promise>; +function asyncApply(this: Either | A>, fn: Either Promise>): Promise>; +function asyncApply( + this: Either | A> | Either Promise>, + argOrFn: Either | A> | Either Promise> +): Promise>; +``` + +- `this | fn` - function wrapped by Either, which should be applied to value `arg` +- `arg | this` - value which should be applied to `fn` +- Returns `Promise` with mapped by `fn` function value wrapped by `Either` if `Either` is `Right` otherwise `Left` with `L` value + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); +const fn1 = right Promise>((a: number) => Promise.resolve(a * 2)); +const fn2 = left Promise>(new Error()); + +const newVal1 = fn1.apply(v1); // Promise.Right> with value 4 +const newVal2 = fn1.apply(v2); // Promise.Left> with value new Error() +const newVal3 = fn2.apply(v1); // Promise.Left> with value new Error() +const newVal4 = fn2.apply(v2); // Promise.Left> with value new Error() +``` + +#### `Either#chain` + +```typescript +function chain(fn: (val: R) => Either): Either; +``` + +- Returns mapped by `fn` function value wrapped by `Either` if `Either` is `Right` and returned by `fn` value is `Right` too otherwise `Left` + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +// Either.Right with value "2" +const newVal1 = v1.chain(a => right(a.toString())); +// Either.Left with value new TypeError() +const newVal2 = v1.chain(a => left(new TypeError())); +// Either.Left with value new Error() +const newVal3 = v2.chain(a => right(a.toString())); +// Either.Left with value new Error() +const newVal4 = v2.chain(a => left(new TypeError())); +``` + +##### `Either#asyncChain` + +```typescript +function chain(fn: (val: R) => Promise>): Promise>; +``` + +- Returns `Promise` with mapped by `fn` function value wrapped by `Either` if `Either` is `Right` and returned by `fn` value is `Right` too otherwise `Left` + Example: + +```typescript +const v1 = right(2); +const v2 = left(new Error()); + +// Promise.Right> with value "2" +const newVal1 = v1.asyncChain(a => right(a.toString())); +// Promise.Left> with value new TypeError() +const newVal2 = v1.asyncChain(a => left(new TypeError())); +// Promise.Left> with value new Error() +const newVal3 = v2.asyncChain(a => right(a.toString())); +// Promise.Left> with value new Error() +const newVal4 = v2.chain(a => left(new TypeError())); +``` + +#### Helpers + +```typescript +// Value from Either instance +const { value } = right(2); // number | Error +const { value } = right(2); // number +const { value } = left(new Error()); // number | Error +const { value } = left(new Error()); // Error +``` + +```typescript +right(2).unwrap() // number +left(new TypeError()).unwrap() // throws value (TypeError) +left(2).unwrap() // throws 2 (don't do this) +``` + +## License + +MIT (c) Artem Kobzar see LICENSE file. diff --git a/identity/index.ts b/identity/index.ts new file mode 100644 index 0000000..7e7005d --- /dev/null +++ b/identity/index.ts @@ -0,0 +1,100 @@ +import type { + AsyncMonad, + AsyncChainable, + ClassImplements, + MonadConstructor, + ApplicativeConstructor, + Container +} from "@sweet-monads/interfaces"; + +function isWrappedFunction(m: Identity> | Identity<(a: A) => B>): m is Identity<(a: A) => B> { + return typeof m.value === "function"; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type StaticCheck = ClassImplements< + typeof IdentityConstructor, + [MonadConstructor, ApplicativeConstructor, AsyncChainable>] +>; + +class IdentityConstructor implements AsyncMonad, Container { + static chain(f: (v: A) => Promise>) { + return (m: Identity): Promise> => m.asyncChain(f); + } + + static from(v: T): Identity { + return new IdentityConstructor(v); + } + + private constructor(public readonly value: T) {} + + join(this: Identity>): Identity { + return this.chain(x => x); + } + + map(f: (r: T, ...parameters: A) => B, ...parameters: A): Identity { + return IdentityConstructor.from(f(this.value, ...parameters)); + } + + asyncMap( + f: (r: T, ...parameters: A) => Promise, + ...parameters: A + ): Promise> { + return f(this.value, ...parameters).then(v => IdentityConstructor.from(v)); + } + + apply(this: Identity<(a: A) => B>, arg: Identity): Identity; + apply(this: Identity, fn: Identity<(a: A) => B>): Identity; + apply( + this: Identity | Identity<(a: A) => B>, + argOrFn: Identity | Identity<(a: A) => B> + ): IdentityConstructor { + if (isWrappedFunction(this)) { + return (argOrFn as Identity).map(this.value as (a: A) => B); + } + if (isWrappedFunction(argOrFn)) { + return (argOrFn as Identity<(a: A) => B>).apply(this); + } + + throw new Error("Some of the arguments should be a function"); + } + + asyncApply(this: Identity<(a: A) => Promise>, arg: Identity | A>): Promise>; + asyncApply(this: Identity | A>, fn: Identity B>>): Promise>; + asyncApply( + this: Identity | A> | Identity<(a: A) => Promise>, + argOrFn: Identity | A> | Identity<(a: A) => Promise> + ): Promise> { + if (isWrappedFunction(this)) { + return (argOrFn as Identity | A>) + .map(a => Promise.resolve(a)) + .asyncMap(pa => pa.then(this.value as (a: A) => Promise)); + } + if (isWrappedFunction(argOrFn)) { + return (argOrFn as Identity<(a: Promise | A) => Promise>).asyncApply(this as Identity>); + } + throw new Error("Some of the arguments should be a function"); + } + + chain(f: (r: T, ...parameters: A) => Identity, ...parameters: A): Identity { + return f(this.value, ...parameters); + } + + asyncChain( + f: (r: T, ...parameters: A) => Promise>, + ...parameters: A + ): Promise> { + return f(this.value, ...parameters); + } + + unwrap(): T { + return this.value; + } +} + +export type Identity = IdentityConstructor; + +export const { from, chain } = IdentityConstructor; + +export const isIdentity = (value: unknown | Identity): value is Identity => + value instanceof IdentityConstructor; diff --git a/identity/package.json b/identity/package.json new file mode 100644 index 0000000..732430a --- /dev/null +++ b/identity/package.json @@ -0,0 +1,36 @@ +{ + "name": "@sweet-monads/identity", + "version": "3.1.0", + "description": "Identity monad", + "main": "./cjs/index.js", + "module": "./esm/index.js", + "exports": { + "import": "./esm/index.js", + "require": "./cjs/index.js" + }, + "types": "index.d.ts", + "scripts": { + "build": "run-s build:pre build:all build:after", + "build:clean": "rm -rf build && mkdir build", + "build:config": "cp ../tsconfig.json tsconfig.json", + "build:pre": "run-p build:clean build:config", + "build:esm": "tsc --project ./tsconfig.json --module 'ESNext' --outDir './build/esm'", + "build:cjs": "tsc --project ./tsconfig.json --module 'CommonJS' --outDir './build/cjs'", + "build:declaration": "tsc --project ./tsconfig.json --outDir './build' --emitDeclarationOnly", + "build:all": "run-p build:esm build:cjs build:declaration", + "build:copy": "cp ./package.json ./build/package.json && cp ./README.md ./build/README.md", + "build:fixcjs": "echo '{\"type\":\"commonjs\"}' > ./build/cjs/package.json", + "build:fixesm": "echo '{\"type\":\"module\"}' > ./build/esm/package.json", + "build:after": "run-p build:copy build:fixcjs build:fixesm" + }, + "homepage": "https://github.com/JSMonk/sweet-monads/tree/master/identity", + "repository": { + "type": "git", + "url": "https://github.com/JSMonk/sweet-monads/tree/master/identity" + }, + "dependencies": { + "@sweet-monads/interfaces": "^3.1.0" + }, + "author": "", + "license": "ISC" +} From fe28633ee241e776f2f02dbf5db86a380a44cb83 Mon Sep 17 00:00:00 2001 From: Alexander Gribochkin Date: Wed, 14 Sep 2022 00:04:24 +0300 Subject: [PATCH 2/2] tests(identity): Added tests --- tests/identity.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/identity.test.ts diff --git a/tests/identity.test.ts b/tests/identity.test.ts new file mode 100644 index 0000000..8bf895c --- /dev/null +++ b/tests/identity.test.ts @@ -0,0 +1,11 @@ +import { from } from "../identity"; +import { fileURLToPath } from "url"; +import { join } from "path"; + +describe("Identity", () => { + test("map", () => { + const file = from("file:///").map(fileURLToPath).map(join, "etc", "hosts").unwrap(); + + expect(file).toBe("/etc/hosts"); + }); +});