Easy-to-use monads implementation with static types definition and separated packages.
- No dependencies, one small file
- Easily auditable TypeScript/JS code
MIT (c) Artem Kobzar see LICENSE file.
The library which provides useful monads, interfaces, and lazy iterators.
License: MIT License
Easy-to-use monads implementation with static types definition and separated packages.
MIT (c) Artem Kobzar see LICENSE file.
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;
}
}
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;
};
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
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.
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.
In this quarantine period im looking to work on something since ive got a lot of free time. Been studying haskell and this repo seems pretty cool.
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;
}
}
valueOf()
for auto unwrapping primitives in old JStoString()
(overrides Symbol.toStringTag
)[Symbol.toPrimitive]
(modern auto unwrapping for primitives)[Symbol.asyncIterator]
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.)
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?
I think it will be useful to have none
from maybe singleton, to allow direct comparison two values - and because obviously different none
s should be equal.
It might be a breaking change, of course .
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!
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.
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? :)
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.
Idea to add 2 functions to create Either
from function that might throw something and another function to create Either
from promise.
https://github.com/monet/monet.js/blob/master/docs/EITHER.md#creating-an-either-from-an-exception
https://github.com/monet/monet.js/blob/master/docs/EITHER.md#creating-an-either-from-a-promise
I'm happy to add it
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 🙂
It would be great if the implementation would be compatible with Fantasy Land: https://github.com/fantasyland/fantasy-land
In NPM, the latest version of the library is 1.2.1, and here it is 1.1.1. Can you update the repository?
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
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
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
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
This functions should have an ability to pass correct type to the calling code, eg
If( right<Either<L, R>>(someRightValue).isRight()) {
// Here I should have access to only R props, not the Either container.
}
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
.
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:
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 { ... }
}
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
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
None
a singletonContinuing 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";
}
}
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();
}
}
}
await
method for async monadsexport interface AsyncMonad<A> extends Monad<A> {
await<A>(this: AsyncMonad<MaybePromiseLike<A>>): Promise<AsyncMonad<A>>;
}
zip
methodCan 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]));
}
}
For providing more helpful messages.
Other implementations:
Either
:unwrap(errorFactory?: (left: L) => unknown) {
// ...
const error = typeof errorFactory === "function"
? errorFactory(this.value)
: new Error("Either state is Left");
// ...
}
Maybe
:unwrap(errorFactory?: () => unknown) {
// ...
const error = typeof errorFactory === "function"
? errorFactory(this.value)
: new Error("Maybe state is None");
// ...
}
I would really like to see suport for catamorphism
Similar to: https://github.com/monet/monet.js/blob/master/docs/MAYBE.md#cata
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.
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)
?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.