Closed seangwright closed 2 years ago
I have an initial implementation of .pipe()
that accepts up to 8 functions as parameters (which is based on the RxJs Observable.pipe()
code linked above):
test('executes all operator functions', () => {
const sut = Maybe.some(1);
const maybe = sut.pipe(
map((n) => n + 1),
map((n) => n * 2),
map((n) => `Calculation: ${n}`)
);
expect(sut).toHaveValue(1);
expect(maybe).toHaveValue('Calculation: 4');
});
I've noticed 2 issues:
Maybe
) to async (eg MaybeAsync
) isn't very ergonomic and requires many extra .pipe()
overloads.Maybe.map
and MaybeAsync.map
as pipeable functions in the same file).In regards to 1, we want to enable it, but we can consider it a dead-end for the given .pipe()
call and then require developers to make another .pipe()
call, which would be the MaybeAsync.pipe()
:
Maybe.some(1)
.pipe(map(n => n + 1), mapAsync(n => Promise.resolve(n * 2)) // this a call to Maybe.pipe()
.pipe(or(5), tap(n => console.log(n)); // this is a call to MaybeAsync.pipe()
In regards to 2, it's the fundamental nature of this library to separate the async/sync cases for the monads so that developers don't have to pay the 'unwrap' cost with Promises when working with purely synchronous values.
This means that while .pipe()
could be a useful feature for custom operator functions, it's not ever going to be as nice of composing operations as the class methods, which inherently namespace themselves (by making the namespacing transparent).
This library has some existing goals:
Maybe
, Result
) can be converted to async ones (MaybeAsync
, ResultAsync
) but don't require them.While it would be nice to have a purely composable solution for the benefit of package size with tree shaking, it's not compatible with some of the above goals.
However, adding .pipe()
is probably worthwhile if only to allow developers to integrate their own custom operator functions in a natural way, which improves the flexibility of the library while maintaining its ease-of-use.
Is your feature request related to a problem? Please describe. Currently the monads in this library are monolithic since they are defined as classes with methods.
The library size isn't huge when minified, but with tree shaking in modern bundlers (and no-bundle ES Module browser scenarios), it would be nice to only pay for the operators used.
It would also be nice for library consumers to be able to enhance the monads with new operators without needing to use inheritance or update the library. Example: Domain/app-specific custom operators are currently not easy to add.
Describe the solution you'd like To use the monad operations we create an instance and then use the instance methods:
Having each operator as a separate function, which can be imported, and composed with other operators using a
pipe()
function is an option.Libraries like RxJs use this approach since there is no language native 'pipe' operator.
pipe()
would be an instance method on the monad:Then each operator would be its own function:
These operator functions could be used by successive calls to
.pipe()
:A domain-specific operator could be added by just creating a new function:
Describe alternatives you've considered The repeated calls to
.pipe()
could be avoided by adopting the complex overloads and type definitions that RxJs uses for its Observable.pipe() method, but that would add a lot of complexity to the library.Additional context I'm not sure whether it would make sense to support both the class method approach and the operator function approach in the same library - though the class methods could use the operator functions, which would reduce logic duplication.
There would probably be a need for separate monad types, like
Maybe
andMaybeFn
(but with a better name!)It's worth noting that having the class instance methods makes the monad operations a lot more discoverable. A dev can
.
their way to success. By making everything a separate function, one would have to consult the docs or know what to import.It wouldn't be nearly as easy to quickly compare available operations either.