Giter Site home page Giter Site logo

sweet-monads's Introduction

@sweet-monads

Mmmm, sweet

Easy-to-use monads implementation with static types definition and separated packages.

  • No dependencies, one small file
  • Easily auditable TypeScript/JS code

Libraries

License

MIT (c) Artem Kobzar see LICENSE file.

sweet-monads's People

Contributors

alexxandergrib avatar dependabot[bot] avatar jsmonk avatar kapelianovych avatar lonli-lokli avatar macarie avatar outbreak avatar uaeio avatar whiteand avatar winexy avatar

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

sweet-monads's Issues

Proposal: Identity monad

All current monads in package are representing some extra state (Left/Right, Just/None), but using monads just for chaining is good on itself

Example:

// Before
app.use(
  express.static(
    path.resolve(getDirname(import.meta.url), "../public")
  )
)


// After
new Chain(import.meta.url)
    .map(getDirname)
    .map(path.resolve, "../public")
    .map(express.static)
    .map(app.use);

Example implementation

export class Chain<T> {
  constructor(public readonly value: T) {}

  map<X, A extends unknown[]>(
    mapper: (...parameters: [value: T, ...rest: A]) => X,
    ...rest: A
  ): Chain<X> {
    return new Chain(mapper(this.value, ...rest));
  }
}

export class AsyncChain<T> {
  constructor(public readonly value: T | PromiseLike<T>) {}

  map<X, A extends unknown[]>(
    mapper: (
      ...parameters: [value: T, ...rest: A]
    ) => X | PromiseLike<X>,
    ...rest: A
  ): AsyncChain<X> {
    return new AsyncChain(
      Promise.resolve(this.value).then((x) => mapper(x, ...rest))
    );
  }

  async get() {
    return await this.value;
  }
}

Do you consider implementing `Maybe#orElse()` method?

Hi, thanks for the lib. Love it! ❤️

Do you find Maybe#orElse() method useful and consider implementing it? I find it useful in order to gracefully avoid such constructions:

let maybeGTINs = getGTINsFromString(string);
if (maybeGTINs.isNone()) {
  let maybeGTINs = just(['default']);
}

with something like this:

const maybeGTINs = getGTINsFromString(string).orElse(['default']);

Monet's seem to have one, but it accepts a Maybe instance for some reason. I guess, the best implementation would be the one mentioned in this article: https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/

Maybe.prototype.orElse = function(default) {
    if (this.isNothing()) {
        return Maybe.of(default);
    }

    return this;
};

Proposal: `assert*`

Assets functions that use assertions conditions in typescript to ensure that value is given type.

This is helpful for cases where you do not necessarily want to follow fp style everywhre.


none().assertsJust() // throws an error
none().assertsJust('Optional error message') // throws an error
just(1).assertsJust() // do nothing

Implement join method

Implementation in haskell:

join :: Maybe (Maybe a) -> Maybe a
join (Just x) = x

main = print $ join (Just (Just "hi"))
> Just "hi"

I'm not sure if that can be expressed in typescript without runtime exceptions.

What is the difference between promises and async/await when returning left and right results?

Hello!

I cannot figure out what is wrong with the second function and how it is different from the third one.

function fn1(): Promise<Either<Error, any>> {
  return Promise.resolve().then(() => right("value"));
}

function fn2(): Promise<Either<Error, any>> {
  return Promise.resolve()
    .then(() => right("value"))
    .catch((e) => left(new Error()));
}

async function fn3(): Promise<Either<Error, any>> {
  try {
    return right("value");
  } catch (e) {
    return left(new Error("caught error"));
  }
}

For fn2, I am getting an error:

Type 'EitherConstructor<unknown, string, EitherType.Right>' is not assignable to type 'Either<Error, any>'.
      Type 'EitherConstructor<unknown, string, EitherType.Right>' is not assignable to type 'EitherConstructor<Error, any, EitherType.Right>'.
        Type '{}' is missing the following properties from type 'Error': name, messagets(2322)

Fn1 and fn3 are considered correct.

Proposal: Convinience methods for containers

In js you can override object properties with symbols. TS can add very good type safety for this as well.

Example for Maybe

class Maybe<T, State> {
  // If current value is Just and is iterable allow to iterate without unwrapping
  *[Symbol.iterator]<T>(this: MaybeConstructor<Iterable<T>, MaybeState.Just>) {
     if(this.isLeft() || !(Symbol.iterator in this.value)) return; // or throw
  
    yield* this.value;
  }

  // [object Object] => [object Maybe]
  get [Symbol.toStringTag]() {
    return this.constructor.name;
  }
}

What else is possible & easy to add

  • valueOf() for auto unwrapping primitives in old JS
  • toString() (overrides Symbol.toStringTag)
  • [Symbol.toPrimitive] (modern auto unwrapping for primitives)
  • [Symbol.asyncIterator]

`chain` operator name

I guess that the chain operator name is pretty well-established, both here and outside the project. Iʼm thinking of possibility to alias it to flatMap, so it would align slightly better with JS API for arrays etc.

See also similar issue with discussion elsewhere.

(Iʼm more used to of instead from for bind operator but thatʼs not a case with JS API.)

[Question] How to use async generators within asyncMap function of Maybe monad

Assuming I have this code

 private async *mapDeleteOwners(): AsyncIterableIterator<EngagementOwnersTabState> {
    yield this.createState({ loading: true });

    await mergeM([this.state.selectedOwners, this.state.engagement]).asyncMap(async ([owners, engagement]) => {
      const result = await this.svc.deleteOwners(engagement.id, owners.map(owner => owner.id)).toPromise();
      if (result.isRight()) {
        const owners = await this.svc.getEngagementOwners(engagement.id).toPromise();
        yield this.createState({ owners: just<Either<ServerError, VendorOwnerDm[]>>(owners), canDelete: false }); // <-!!!!! HERE
      }  
    })
    this.state.selectedOwners.map(eSelection => eSelection)
    if (this.state.selectedOwners.isJust() && this.state.engagement.isJust()) {
     

    yield this.createState({ loading: false });
  }
}

Typescript complains that ts1163: yield expressions is only allowed in a generator body.

Is there any chance to use it this way?

Make `Maybe.none` singleton

I think it will be useful to have none from maybe singleton, to allow direct comparison two values - and because obviously different nones should be equal.
It might be a breaking change, of course .

Breaking issue on the latest release (3.3.0)

Hello!

I'm a maintainer of quint and we use both the either and maybe packages of sweet-monads - they are very handy, thank you so much!

We found an issue today trying to run our tool, and turns out it was something related to your latest release. I even tried reproducing the problem on a very small project with only your package in the dependencies, and I got this error while trying to compile:

node_modules/@sweet-monads/either/index.ts:205:7 - error TS2322: Type 'Either<unknown, R>' is not assignable to type 'Either<L, R>'.
  Type 'EitherConstructor<unknown, R, EitherType.Right>' is not assignable to type 'Either<L, R>'.
    Type 'EitherConstructor<unknown, R, EitherType.Right>' is not assignable to type 'EitherConstructor<L, R, EitherType.Right>'.
      Type 'unknown' is not assignable to type 'L'.
        'L' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.

205       return EitherConstructor.left(e);
          ~~~~~~


Found 1 error in node_modules/@sweet-monads/either/index.ts:205

I believe the version you released also has this problem, and therefore couldn't compile. The error we were getting in our tool was:

node:internal/modules/cjs/loader:573
      throw e;
      ^

Error: Cannot find module '/home/gabriela/.npm/lib/node_modules/@informalsystems/quint/node_modules/@sweet-monads/either/cjs/index.js'
    at createEsmNotFoundErr (node:internal/modules/cjs/loader:1098:15)
    at finalizeEsmResolution (node:internal/modules/cjs/loader:1091:15)
    at resolveExports (node:internal/modules/cjs/loader:567:14)
    at Module._findPath (node:internal/modules/cjs/loader:636:31)
    at Module._resolveFilename (node:internal/modules/cjs/loader:1063:27)
    at Module._load (node:internal/modules/cjs/loader:922:27)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:110:18)
    at Object.<anonymous> (/home/gabriela/.npm/lib/node_modules/@informalsystems/quint/dist/src/parsing/quintParserFrontend.js:37:18)
    at Module._compile (node:internal/modules/cjs/loader:1256:14) {
  code: 'MODULE_NOT_FOUND',
  path: '/home/gabriela/.npm/lib/node_modules/@informalsystems/quint/node_modules/@sweet-monads/either/package.json'
}

Node.js v18.16.1

We did fix it on our side by pinning both packages to ~3.2.0, but this report might be useful for you guys either way!

Usage examples in docs are wrong

You give examples of "minimal complete definitions" for each category, but your actual interfaces require the async versions of each method. E.g. implementing a functor also requires implementing asyncMap. I ended up not being able to use your library because I didn't want to clutter up my class with all of the extra async variations.

Latest versions are not CommonJS compatible

Hi again 😄 My applications are failing now as the latest released versions of @sweet-monads are not CommonJS-compatible for some reason. You might have changed something in the compilation settings? The 2.0.0 version is compatible and the later ones are already not:

// 2.0.0 exports
exports["default"] = MaybeConstructor;
exports.merge = MaybeConstructor.merge, exports.just = MaybeConstructor.just, exports.none = MaybeConstructor.none, exports.from = MaybeConstructor.from;
exports.isMaybe = function (value) {
    return value instanceof MaybeConstructor;
};
// 2.1.2 export
export default class MaybeConstructor {
    constructor(type, value) {

So, Node 12 (which is LTS still) fails to run such apps. Should we consider it as a bug or it's something that meant to happen? :)

Implement ap method

Shouldn't monads be implementing Apply as well?)

ap<A, B>(this: Maybe<A>, foo: Maybe<(a: A) => B>): Maybe<B>;

Or smth like that.

Can't use this module properly while using ESM

I think fixing #17 broke imports while using ESM. I don't mean broke as "it's impossible to make it work", but fixing the import will break TS.

import LazyIterator from '@sweet-monads/iterator'

console.log(LazyIterator)
// => { default: [class LazyIterator] }

This is because in the required file, iterator/index.js, the default export is defined as exports.default = LazyIterator.

There is a workaround, but it will cause the TS type-checker to fail/not resolve anything:

import LI from '@sweet-monads/iterator'

// Here we get `const LazyIterator: any`
const { default: LazyIterator } = LI
// => Property 'default' does not exist on type 'typeof LazyIterator'.

console.log(LazyIterator)
// => [class LazyIterator]

Node has conditional exports, I think those would solve the problem.

A possible fix would be to define the exports as follows:

{
  "type": "module",
  // Used by older versions of Node and bundlers that don't support exports
  "main": "./cjs/index.js",
  // Used by older versions of bundlers that don't support exports but do support ESM
  "module": "./esm/index.js",
  // Used by newer versions of bundlers and Node
  "exports": {
	// Used when `import`ed
    "import": "./esm/index.js",
	// Used when `require`d
    "require": "./cjs/index.js"
  }
}

I don't think that tsc can run multiple builds with just one command tho, maybe it would be easier to have two scripts in package.json, build:esm and build:cjs, like this:

{
  "scripts": {
    "build:esm": "tsc ./tsconfig.json --module 'ESNext' --outDir './esm'",
    "build:cjs": "tsc ./tsconfig.json --module 'CommonJS' --outDir './cjs'"
  }
}

If you'd like, I can open a PR. Let me know 🙂

Either version

In NPM, the latest version of the library is 1.2.1, and here it is 1.1.1. Can you update the repository?

Changing either type inside mapRight

Hi, i have a proposal for Either monad functional. But first, some word about my case. It is follow code. I think it so simple for understanding, sorry for image, i dont know how can i insert formatted code
image

Problem

I want to return my error SessionServiceErrorType.sessionDoesntActiveYet as Left branch of this either.
It needed me for handling this either in executing place and i want to handle all business error in ONE left branch.
I think it is good idea, splitting business logic by partial in some mapRight calls.
And if we detect something business logic error in a rightMap we need to change either direction from RIGHT to LEFT

Proposal

I think it maybe made as passing to mapRight second argument such as toLeft which takes new value and transforms this either from right to left. If you approve the idea i can do pull request with implementation for this
It will allow us to detect something error(left branch in either case) in each mapRight step and handle these errors in one place, as Promises do

Maybe. Import and Build problems

Thanks for it, simple helpfull library and speech about monads, but i have a problem. Example of usage Maybe in this docs doesnt work. In Maybe we have undefined , because index.js exports Maybe class as module.exports = its defaul export, not desctructurisation. And if we swap destructutisation import to default, we would have a errors like ' Cannot use namespace 'Maybe' as a type.', 'Property 'none' does not exist on type 'typeof import("path_to_project/node_modules/@sweet-monads/maybe/index")'', Thanks once again

Either.apply may produce RangeError: Maximum call stack size exceeded

it('Either.apply crash', () => {
  const either1 = Either.right(() => {});
  either1.apply(Either.right(() => {}));
  const either2 = Either.left(() => {});
  either2.apply(Either.right(() => {}));
})

As result of test run will be crash js application and RangeError.

May I in that case (when recursive call of apply function crashed call stack) return Either.left?

if ((isWrappedFunction(this)) {
  if (this.isRight() && !isWrappedFunction(argOrFn)) {
    return argOrFn.map(this.value as (a: A) => B);
  }
  return Either.left<L, B>(this.value as L);
}
return (argOrFn as Either<L, (a: A) => B>).apply(this as Either<L, A>);

PS: same thing in asyncApply.

Possible library improuvements

Hello, while @JSMonk was away this spring i created my fork of this library. Here it is https://github.com/AlexXanderGrib/monads-io

And here some good ideas you can grab from it:

1. Use inheritance

class EitherConstructor<L, R>
  implements AsyncMonad<R>, Alternative<R>, Container<R> { ... }
  
class Right<L, R> extends EitherConstructor<L, R> { 
  get [Symbol.toStringTag](): "Right" { ... }
  get name(): "Either" { ... }
  get type(): EitherType.Right { ... }
  getRight(): R { ... }
  getLeft(): undefined { ... } 
  
  private constructor(public readonly right: R) {
    super();
    Object.freeze(this);
  }
}

class Left<L, R> extends EitherConstructor<L, R> {
   ...
   get type(): EitherType.Left { ... }
}
  1. It's simpler. Types can now be defined as:

    type Either<L, R> = Left<L, R> | Right<L, R>
    type Maybe<T> = Just<T> | None<T>; // <T> may be obsolete, cause of T = never by default
  2. It's more efficient. Now left and right are different on class level, so difference is expressed through prototype without specific property, so overhead is minimal

2. Make None a singleton

Continuing with classes, it is possible to create one and only one None for all program

class None<T = unknown> extends MaybeConstructor<T> implements SerializedNone {
  static readonly instance = new None<never>();
  static create<T>(): None<T> {
    return None.instance;
  }

  get [Symbol.toStringTag]() {
    return "None";
  }  
}

3. Create rust-like iterator helpers

export function* iterator<T>(
  callback: () => Maybe<T>
): Generator<T, void, void> {
  let result: Maybe<T>;

  while ((result = callback()).isJust()) {
    yield result.unwrap();
  }
}

export async function* asyncIterator<T>(
  callback: () => MaybePromiseLike<Maybe<MaybePromiseLike<T>>>
): AsyncGenerator<T, void, void> {
  let result: Maybe<MaybePromiseLike<T>>;

  while ((result = await callback()).isJust()) {
    yield await result.unwrap();
  }
}

export function* filterMap<T, X>(
  iterable: Iterable<T>,
  filterMap: (value: T, index: number) => Maybe<X>
): Generator<X, void, void> {
  let index = 0;
  for (const value of iterable) {
    const processed = filterMap(value, index++);

    if (processed.isJust()) {
      yield processed.unwrap();
    }
  }
}

4. Add await method for async monads

export interface AsyncMonad<A> extends Monad<A> {
  await<A>(this: AsyncMonad<MaybePromiseLike<A>>): Promise<AsyncMonad<A>>;
}

5. Add zip method

Can be used to refactor .apply()

class EitherConstructor {
  zip<A, B>(either: Either<A, B>): Either<L | A, [R, B]> {
    return this.chain((value) => either.map((right) => [value, right]));
  }
}

Proposal: `unwrap()` custom error

Why?

For providing more helpful messages.

Other implementations:

For Either:

unwrap(errorFactory?: (left: L) => unknown) {
  // ...
  const error = typeof errorFactory === "function" 
    ? errorFactory(this.value) 
    : new Error("Either state is Left");
  // ...
}

For Maybe:

unwrap(errorFactory?: () => unknown) {
  // ...
  const error = typeof errorFactory === "function" 
    ? errorFactory(this.value) 
    : new Error("Maybe state is None");
  // ...
}

Add a mapNullable

It would be awesome to have a map that caters for nullable values and would return none if it's so.

At the moment I use

just(4).chain(x => fromNullable(add2(x)))

but it's cumbersome and not point-free. https://github.com/AlexGalays/space-monad implemented it in the map itself which imo is great.

Chaining of async functions

Hi again!

I am trying to find a readable way of chaining when using promises to write it as .then(doSomething)
Suppose, we have a sequence of async functions fn1 and fn2.

const fn1 = (a: number) => Promise.resolve(right(a + 1));
const fn2 = (b: number) => Promise.resolve(right(b + 2));

My first idea was to write something like:

const fn = () =>
  Promise.resolve(right(1))
    .then((mb1) => mb1.asyncChain(fn1))
    .then((mb2) => mb2.asyncChain(fn2));

To hide the callbacks, I wrapped the functions with Either.asyncChain and obtained something like:

const fn1 = (mb: Either<Error, any>) => mb.asyncChain((a: number) => Promise.resolve(right(a + 1)));
const fn2 = (mb: Either<Error, any>) => mb.asyncChain((b: number) => Promise.resolve(right(b + 2)));

const fn = () =>
  Promise.resolve(right(1))
    .then(fn1)
    .then(fn2);

Is there a simpler way to chain the functions? Is it possible to implement some new method in Either to chain like right(1).thenChain(fn1).thenChain(fn2)?

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.