GithubHelp home page GithubHelp logo

thames-technology / monads Goto Github PK

View Code? Open in Web Editor NEW
705.0 10.0 30.0 1.22 MB

Option, Result, and Either types for TypeScript - Inspired by Rust ๐Ÿฆ€

Home Page: https://thames-technology.github.io/monads/

License: MIT License

TypeScript 98.39% JavaScript 1.61%
monads javascript typescript types option result rust nodejs either node

monads's Introduction

Monads Logo

If you use this repo, star it โœจ


Option, Result, and Either types for JavaScript

๐Ÿฆ€ Inspired by Rust

Zero dependencies โ€ข Lightweight โ€ข Functional


Install

npm install @thames/monads

Getting started

The Option<T> type

Option represents an optional value: every Option is either Some and contains a value, or None, and does not.

Note

Full documentation here: Option

import { Option, Some, None } from '@thames/monads';

const divide = (numerator: number, denominator: number): Option<number> => {
  if (denominator === 0) {
    return None;
  } else {
    return Some(numerator / denominator);
  }
};

// The return value of the function is an option
const result = divide(2.0, 3.0);

// Pattern match to retrieve the value
const message = result.match({
  some: (res) => `Result: ${res}`,
  none: 'Cannot divide by 0',
});

console.log(message); // "Result: 0.6666666666666666"

The Result<T, E> type

Result represents a value that is either a success (Ok) or a failure (Err).

Note

Full documentation here: Result

import { Result, Ok, Err } from '@thames/monads';

const getIndex = (values: string[], value: string): Result<number, string> => {
  const index = values.indexOf(value);

  switch (index) {
    case -1:
      return Err('Value not found');
    default:
      return Ok(index);
  }
};

const values = ['a', 'b', 'c'];

getIndex(values, 'b'); // Ok(1)
getIndex(values, 'z'); // Err("Value not found")

The Either<L, R> type

Either represents a value that is either Left or Right. It is a powerful way to handle operations that can result in two distinctly different types of outcomes.

Note

Full documentation here: Either

import { Either, Left, Right } from '@thames/monads';

const divide = (numerator: number, denominator: number): Either<string, number> => {
  if (denominator === 0) {
    return Left('Cannot divide by 0');
  } else {
    return Right(numerator / denominator);
  }
};

const result = divide(2.0, 3.0);

const message = result.match({
  left: (err) => `Error: ${err}`,
  right: (res) => `Result: ${res}`,
});

console.log(message); // "Result: 0.6666666666666666"

monads's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

monads's Issues

Source maps linking to non-existing source files

When using @sniptt/monads in React, I am getting a warning indicating that source maps are pointing to non-existing files.

Expected behavior

I expected the library to compile without any compiler warnings.

Actual behavior

When compiling, the React compiler (to be more precise: source-maps-loader) prints multiple warnings, all looking similar to the following:

WARNING in ./node_modules/@sniptt/monads/build/result/result.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '[Path to my project]\node_modules\@sniptt\monads\lib\result\result.ts' file: Error: ENOENT: no such file or directory, open '[Path to my project]\node_modules\@sniptt\monads\lib\result\result.ts'

Potential reason

It seems like the source maps are pointing to a "lib" folder that does not exist. For instance, build/result/result.js.map has the following content:

{"version":3,"file":"result.js","sourceRoot":"","sources":["../../lib/result/result.ts"], [...]

Thus, this source map is trying to reference (root)/lib/result/result.ts. However, the lib/ folder does not exist since it is not part of the package; only the build/ folder is currently included in package.json:

https://github.com/sniptt-official/monads/blob/fe0b45709cf3efa0a82b2602c3ef619553fbe21e/package.json#L11-L13

Environment

  • @sniptt/monads: Version 0.5.10
  • Project based on Create-React-App with React 18.2.0.

if let construction?

I'm not sure if this would be possible in typescript.

In addition to match, rust also has if let Some(X) = an_opt_var { ....

This is really useful, since often the None case isn't used / ignored.

It's possible to do if (opt.isSome()) { let x = opt.unwrap()... but that's less compiler-checked, and it requires an unsafe unwrap.

I'd like this a lot since my lemmy-ui codebase has tons of these verbose .match whilst ignoring the none case, all over the code.

Can't find module

Adding import { Option, Some, None } from '@threestup/monads' give me an error like Can't find module ...

I need to use import { Option, None, Some } from "@threestup/monads/src/main"

Should main in package.json point there instead?

I installed this using yarn. There is no dist folder in my installed dir.

Add a Either type

Hi,

It would be nice to have a Either type like the one in this crate.

The enum Either with variants Left and Right is a general purpose sum type with two cases.
The Either type is symmetric and treats its variants the same way, without preference.

What do you think ? I can open a PR if you want

node version constraint too aggressive

In the package.json of this project, the version of node is set to ">=13.5.0" which is unreasonable.

The LTS version of nodejs is currently node12. And node13 is not officially supported by some platforms like aws lambda so far.

Is that possible to loosen this constriant in the near future?

Proposal: Make the code less opaque and more explicit

Right now, the code is opaque (function returns a plain object, uses symbols, ResLeft/ResRight types), and requires assigning ResLeft to Either return values which known to be Left, as the IDE would otherwise not know.

It would be great if this library used classes instead of Left and Right being functions. This would let us use instanceof on Either values and overall be cleaner. The code below is the proposal and is what I am using in the interim.

export abstract class Either<L, R> {
	protected constructor(protected readonly Value: L | R) {
	}

	unwrapLeft(): L {
		throw new ReferenceError(`Cannot unwrap Left value of abstract class Either`);
	}

	unwrapRight(): R {
		throw new ReferenceError(`Cannot unwrap Right value of abstract class Either`);
	}

	unwrap(): R | L {
		return this.Value;
	}

	static Left<T>(Value: T) {
		return new Left(Value);
	}

	static Right<T>(Value: T) {
		return new Right(Value);
	}
}

export class Left<T> extends Either<T, never> {
	constructor(protected readonly Value: T) {
		super(Value);
	}

	unwrapLeft(): T {
		return this.Value;
	}
}

export class Right<T> extends Either<never, T> {
	constructor(protected readonly Value: T) {
		super(Value);
	}

	unwrapRight(): T {
		return this.Value;
	}
}

export abstract class Option<S> {
	protected constructor(protected readonly Value?: S) {
	}

	unwrap(): S {
		throw new ReferenceError(`Cannot unwrap Some value of abstract class Option`);
	}

	static Some<T>(Value: T) {
		return new Some(Value);
	}

	static None() {
		return new None();
	}
}

export class Some<T> extends Option<T> {
	constructor(protected readonly Value: T) {
		super(Value);
	}

	unwrap(): T {
		return this.Value;
	}
}

export class None extends Option<never> {
}

Performance suggestions

Greetings. I saw someone mention this library in a Reddit post, and upon checking it out I noticed that the factories for your types could be implemented in a more performant way.

By creating plain-old-objects in your factory functions, you're actually creating new functions for those object methods for every single instance created. The Result type, for example, defines 12 methods, which means that every Result that is created during a program's execution also creates 12 brand new function objects in memory, most of which probably aren't ever even going to be called, and most of which are implementing the same exact logic. For example, the isOk() method on an Ok object returns true. If I run my program and create 100 Oks, we'll be creating 100 functions that always return true instead of sharing one function that always returns true.

A trivial change that will improve this situation is to simply define some private classes for these factory functions. For example:

export function Ok<T, E = never>(val: T): ResOk<T, E> {
  return new OkImpl(val);
}

class OkImpl<T> implements ResOk<T> {
  #val: T;
  constructor(val: T) {
    this.#val = val;
  }
  get type(): typeof ResultType.Ok {
    return ResultType.Ok;
  }
  isOk(): boolean {
    return true;
  }
  isErr(): boolean {
    return false;
  }
  ok(): Option<T> {
    return Some(this.#val);
  }
  err(): OptNone<T> {
    return None;
  }
  unwrap(): T {
    return this.#val;
  }
  unwrapOr(_optb: never): T {
    return this.#val;
  }
  unwrapOrElse(_fn: never): T {
    return this.#val;
  }
  unwrapErr(): never {
    throw new ReferenceError('Cannot unwrap Err value of Result.Ok');
  }
  match<U>(matchObject: Match<T, never, U>): U {
    return matchObject.ok(this.#val);
  }
  map<U>(fn: (val: T) => U): ResOk<U> {
    return new OkImpl(fn(this.#val));
  }
  mapErr<U>(_fn: never): ResOk<T> {
    return this; // Why create a new instance when this is (shallow) immutable, anyway?
  }
  andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> { // I think the type on your ResOk interface is incorrect, but I'll do a different issue for that.
    return fn(this.#val);
  }
  orElse<U>(_fn: never): ResOk<T> {
    return this; // Why create a new instance when this is (shallow) immutable, anyway?
  }
}

This approach will be much more efficient, since every instance of an Ok object will share the same methods through the prototyope, instead of each carrying around 12 unique function instances and a Symbol.

I'll post some other suggestions as separate issues.

Cheers!

First monadic law is violated ๐Ÿ˜•

version 2.1:

Some(null).is_some();

I expect the above code to return true, however it returns false, which means the first law is violated (left identity): I put a value into a monad, but it is changed in default context.

Edit: To be precise, the first law states, that I can do this:

const value = null;
const monad = Some(value);
const fn = (v: any) => String(v);

fn(monad.unwrap()) === fn(value);

which should gracefully execute with a true result, but it fails with ReferenceError: Trying to unwrap None..

Erasing T for isErr

The function isErr is not erasing the T of Result<T, E> even though it could do so, and its partner function isOk is already erasing E.

Changing the signature to export function isErr<T, E>(val: Result<T, E>): val is ResErr<never, E> would allow writing if (isErr(value)) { return value; } analogous to if (isOk(value)) { return value; } which already is possible.

Implement or_else

I need the counterpart to and_then. It would also be nice with some methods to conditionally get the option value, the result value and result error that returns T|null and E|null for simpler integration in existing projects which expect nullable types already.

Proposal: Option.filter

A function that turn an Some in to a None when a given predicate returns false.

The Interface might look like this:

interface Option<T> {
  filter<T>(predicate: T => boolean): Option<T>
}
const value = Some(4);
const noValue = None;

value.filter(v => v < 4) // None
value.filter(v => v == 4) // Some(4)
noValue.filter(v => v) // None

Unexpected behavior with `Ok(None)`

Version: 0.5.10

I have code like this in Vue3 project:

Ok(None).unwrap().isSome()

I expect the above code to return false, however it returns true.

I would like to know if there is an explanation for this behaviour ?

handling async results

Hi,

A lot of my methods return a Promise<Result<T,K>> because they handle async behaviour (writing to the database for instance).

I would love to be able to chain these methods with and_then but the latter only accepts methods that return a Result<T,K>.

Example:

const user = makeUser({ name, email }).and_then(userDB.insert);

// where userDB.insert = (user) => Promise<Result<User, Error>>

Is this possible currently or does this require additional work ?
I'm willing to help if I can.

Cheers

Importing in Node breaks

This doesn't work in 0.3.3 and 0.3.2 and latest typescript 3.9.5:

import { Result, Ok, Err } from '@hqoss/monads'

Error:

Cannot find module '@hqoss/monads' or its corresponding type declarations.ts(2307)

while this works:

import { Result, Ok, Err } from '@hqoss/monads/src'

I'm guessing it's because of this commit:
14fd30b

Since mod.ts is at the root level and you're not using "include" in tsconfig.json, then I think tsc sees that you're importing from './src/...' so it creates the extra 'src' directory under 'dist', which is strange, since you already have mod.ts in excludes.

Adding this in tsconfig.json seems to fix it:

  "include": [
    "./src/**/*.ts",
  ],

although I have no idea if that breaks deno or not, since I don't use deno.

Wrap async/sync result in the library

Its super common for async/sync operations to throw an error, but i don't see a constructor to conveniently wrap the operation in the Result. I propose a new constructor:

async function AsyncResultOf<T>(val: Promise<T>): Promise<Result<T, Error>> {
  try {
    return Ok(await val);
  } catch(err) {
    return Err(err);
  }
}

This allows us to go from the throw/catch error handling to the monadic error handling patterns const result = await AsyncResultOf(fetch(...))

I will go further to suggest an even more general constructor that translates both async and non-async fallible operations:

async function ResultOf<T>(val: () => Promise<T>): Promise<Result<T, Error>> {
  try {
    return Ok(await val());
  } catch(err) {
    return Err(err);
  }
}

This can of course be overloaded with the signature of AsyncResultOf.

(PS im trying to whip up a PR but the test keeps saying "Timed out while running tests" on my machine)

How to deserialize into these monads? IE use monads over an API.

I'm trying to convert all of my API fields like:

interface MyObj {
  test?: string; // OLD
  test: Option<string>; // NEW
}

I've tried using the Option type in my API client, but when I try to use them in code, I get errors like:

Cannot read property 'match' of null

or

...unwrapOr is not a function

When I console.log that data, it seems that despite my using an Option in the API code, its completely ignored, and will either return a string, or null.

Is there any way to use monads over an API, or deserialize nullables into options.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.