Open mobily opened 2 years ago
looks really good, i was just getting back to ts-belt and have a lot of async code. 💖
@mobily what is the equivalent of fp-ts eitherAsync tryCatch? or the belt/Result/fromExecution in AsyncResult
is it AR.handleError?
How about alternative match/with
from ts-pattern out of box? something like fold
but for handling all or each states?
Looks good, but I am starting to get lost in all that shortcuts AD
, ADR
, AO
... Did you thinked about shipping also "non-shortcut" version of these?
what is the equivalent of fp-ts eitherAsync tryCatch? or the belt/Result/fromExecution in AsyncResult
@alexn-s the constructor of the AsyncResult
variant is named: AR.make
, you use it like the following:
const result = await pipe(
AR.make(promiseFn()),
AR.map(value => …),
AR.getWithDefault(…),
)
something like fold but for handling all or each states?
@lulldev that sounds like a great idea, I will add it to AsyncData
and AsyncDataResult
for sure!
Looks good, but I am starting to get lost in all that shortcuts AD, ADR, AO... Did you thinked about shipping also "non-shortcut" version of these?
@Nodonisko yes, I hear you, and totally agree, I was against adding full namespace names due to the conflict with native namespaces, however, I feel like this might be a good alternative for namespace abbreviations (by the way I was about to merge this PR https://github.com/mobily/ts-belt/pull/35 however it's been closed recently, sorry @cevr!)
@mobily can i help with release candidate and contribute?
@lulldev sure thing, that would be awesome! do you need anything from me to get started?
@mobily yes. i want to know more about flow for contributors. What exactly can I help to do and where is the list of what is left for the release candidate?
@mobily I've just started to use ts-belt
. Everything looks good but why there's no A.flatMap
? I've checked all other popular names for such functions like chain
, concatMap
, etc... but it seems to be really missing.
Interestingly enough, O.flatMap
and R.flatMap
do exist 🤔 Should I raise an issue for it?
@ivan-kleshnin added in v4.0.0-rc.5
🚀 https://github.com/mobily/ts-belt/commit/14a5c153b4d554cd304b0c6e44b671fc1d7d1d81
benchmarks:
flatMap (single function call)
✔ @mobily/ts-belt 27,383,074.99 ops/sec ±0.33% (99 runs) fastest
██████████████████████████████████████████████████████████████████████
✔ remeda 1,759,670.66 ops/sec ±1.36% (97 runs) -93.57%
████
✔ ramda 1,392,700.93 ops/sec ±0.52% (91 runs) -94.91%
███
✔ rambda 4,870,498.47 ops/sec ±0.92% (98 runs) -82.21%
████████████
✔ lodash/fp 5,749,906.26 ops/sec ±0.78% (87 runs) -79.00%
██████████████
→ Fastest is @mobily/ts-belt
flatMap (function call inside pipe
)
✔ @mobily/ts-belt 21,116,789.82 ops/sec ±2.48% (94 runs) fastest
██████████████████████████████████████████████████████████████████████
✔ remeda 2,500,686.11 ops/sec ±1.44% (98 runs) -88.16%
████████
✔ ramda 872,490.77 ops/sec ±0.77% (92 runs) -95.87%
██
✔ rambda 4,248,478.35 ops/sec ±0.54% (93 runs) -79.88%
██████████████
✔ lodash/fp 896,410.20 ops/sec ±1.63% (93 runs) -95.75%
██
→ Fastest is @mobily/ts-belt
other new additions or bug fixes in v4
:
A.takeWhile
type definitionADR.all
A.flatMap
work on the new documentation website is going pretty slowly since I do not have much spare time at this particular moment, but I will keep you posted on the progress!
One pattern I find myself using
pipe(
// ... some calculation resulting in an AsyncResult,
AR.flatMap((...) => AR.make(promiseFn1())),
AR.flatMap((...) => AR.make(promiseFn2())),
AR.flatMap((...) => AR.make(promiseFn3())),
)
In other words, I have a bunch of functions that simply return a Promise and know nothing about ts-belt, and I want to use them as is. But I constantly have to covert them into AsyncResults which has become on ergonomics issue.
I wish flatMap() callback function could just return either AsyncResult or just a Promise and it would get normalized to AsyncResult under the hood.
Or am I missing a simpler way of doing it?
Also, the line between R.fromPromise()
and AR.make()
seems kind of blurry. I'm utterly confused which one to use as they both seem to do the same thing (?).
I wish flatMap() callback function could just return either AsyncResult or just a Promise and it would get normalized to AsyncResult under the hood.
@kirillrogovoy it totally makes sense, I will update both, AR.flatMap
and AO.flatMap
R.fromPromise() and AR.make()
you can actually use both alternately to achieve the same thing
Thanks!
Actually, I've been using ts-belt this whole time since I saw this release candidate. Thanks for making it!
I made a thing I thought you'd be curious about: I've created a single map
function that, depending on the input, pretends to be one of: R.map, R.flatMap, AR.map, AR.flatMap, AR.fold, and it takes care of Promise -> AR transformation.
The best part – it doesn't lose the TS typing. So code like this is possible:
pipe(
123,
(v) => R.Ok(v), // now it's a Result
map(v => R.Ok(v)), // works like R.flatMap
map(v => fetch(...)), // now it's an AR
map(r => x ? R.Ok(x) : R.Error('OH_OH' as const)), // works like AR.fold
map(r => functionThatReturnsAR(r)) // works like AR.flatMap
) // AR.AsyncResult<Foo, Error | 'OH_OH'>
It completely replaced all the functions above in my code with zero downsides except for a few tiny typing nuances I'll fix.
It doesn't meet the bar for a contribution (yet), and I don't know if it should belong to this repo, but let me know if you want a quick demo and/or to chat about it.
@kirillrogovoy i think this would make sense for all utilities that are shared (map, flatmap, fold)
@kirillrogovoy i think this would make sense for all utilities that are shared (map, flatmap, fold)
Such ultra generic functions fail shortly with type inference, at least in my experience. I've personally started to use ts-belt
because its functions are split per module and inference works 100% of the time. Unlike Ramda, Rambda and other libraries which have a single map, flatMap, etc.
instances, just as proposed above. Given how many library authors, very competent guys, failed to achieve the same goal I doubt it's possible.
When you replace a seed literal 123
here: pipe(123, map(...), filter(...))
with a variable, let alone generically typed, you'll very soon have to explicitly type function arguments. To resolve ambiguity which arises from over-generic code. In the best case it will be more code to write. In the worst case it won't type at all. TypeScript can't magically guess that you meant "that map
overload for arrays, not that for promises".
@ivan-kleshnin my experience had been roughly the same and I can relate to every frustration; up until I did manage to cobble something together for myself.
I've just literally copy-pasted it from my repo to here so you can try it for yourself.
At the time of writing this, I have 42 calls of mmap in my code in a lot of different contexts, and there's no place where the typings get screwed up.
All that said, I still consider it an intellectual experiment of "how far I can push this idea." By no means am I making a case that this is a superior way of doing anything. Treat it more as research rather than an end-product.
Having taken a look at various map functions in Rambda and others, it feels like whatever generic API they are doing is related to working with collections (arrays, objects, Maps, etc.) and not containers (Promise, Result, etc.) I wasn't trying to solve any problem that they were trying to solve, so maybe I was unaffected by the challenges of that domain.
@kirillrogovoy thank you for the information! It's great that you have worked through forementioned challenges.
Another concern I have is that to extend map, filter, reduce, etc
to support even a single new type I would have to wrap and reexport each function. It's an expression problem in its purest form. With native approach of Ts-Belt I would simply make a new module (to support e.g. Set
helpers). The latter sounds much better: easier to do and to support (not counting cross-type functions which are still an issue).
TypeScript does not have typeclasses (yet?) so categorical applications of such generic functions are out of question.
So, aside of being a nice proof of infer
power in TypeScript, what benefit this new API adds, from your point of view?
I would have to wrap and reexport each function
I guess it's true in theory, but in practice, I don't actually pattern-match and choose the right ts-belt function in my code. Instead, I wrote an implementation from scratch (60 LOC) that accounts for all the different types. At their base, mapping functions are extremely simple. I spent maybe 5% of the time writing the implementation, and 95% dealing with the types. 😅
TypeScript does not have typeclasses (yet?) so categorical applications of such generic functions are out of question.
Unfortunately, I understand next to no FP theory — certainly not enough to understand what typeclasses and categorical applications are. I've just tried to read some Haskell examples but didn't really grasp much.
what benefit this new API adds, from your point of view?
For me, it's simply ergonomics. If I can stop thinking about which of the 6 functions to use with virtually no downside, it's a win already. At least, in this specific case where only one of six functions is actually valid and applicable depending on the input and what my mapping function returns.
One specific example was that I'd always forget which of AR.fold and AR.flatMap expect me to return a Result vs AsyncResult. Again, given that there's only one function that's even valid for my case, that's the kind of decision I just don't want to think about.
Another side of that example is being annoyed every time I need to change AR.map to AR.flatMap / AR.fold because I'm introducing some IO and/or Result in the mapping function. I always think "hey, Typescript knows what I'm trying to do, why can't it just select the right overload for me?!"
I also understand that this problem may be just a matter of preference. I have nothing against someone else wanting to write all those functions explicitly for any reason. But in my personal experience, it doesn't add value either to the writing experience or to the readability of the code. I prefer to be lazy and have a magic function haha.
@kirillrogovoy thank you for the explanation, your points are clear. My experience differs but I don't mind about an extra code layout option to choose from. Keep up 👍
I've being playing a bit with ts-belt
and is nice to see the async utilities to pop-up in the next version.
Thanks for make this library.
One thing I'm missing, maybe also related to the above, is getting a proper flow with Promises
and Array
or Dictionary
to unfold.
A small example:
pipe(
fetch(someData),
A.filterMap(async (response) => {
const { body } = await response;
return !!body ? ADR.makeCompleteOk(response.body) : ADR.makeCompleteError("No body found");
})
)
Maybe this should be possible with Async
types, but it's unclear to me; sorry for my ignorance.
I will believe that the underline Array
and Dictionary
might need to adapt in accepting Promises.
@kirillrogovoy I think I found a bug in your implementation of mmap
const result: Result<{id: string}, 'failed'> = R.Error('failed') // some result containing an error
const x = pipe(
result,
mmap(({ id }) => fetch('/user/' + id)), // should convert result to async result because fetch returns a promise
); // AR.AsyncResult<User, FetchError | 'failed'>
// but here x is not an async result because mmap skipped executing the async callback, so typescript and reality are not in sync
I think that the same function can not handle sync AND async at the same time when working with Results
Hey Antoine,
Yeah, you're right. This is one of the limitations that I couldn't fix.
Essentially, Typescript knows the function signature and so it kind of knows that the function would return a promise, but Javascript doesn't know that without actually running the function.
The right solution would probably be restricting it in TS land and forcing you to explicitly convert result
to a promise (e.g. AR.make(result)
first.
That said, there's currently no good solution in ts-belt either. If you use R.map
, you end up with Result<Promise<User>, FetchError | 'failed'>
R.fromPromise() and AR.make()
you can actually use both alternately to achieve the same thing
Is this still accurate? They seem similar on the surface, but looking at the types there's differences, no?
AR.make<A, B = globalThis.Error>(promise: Promise<A>): AsyncResult<A, B>;
R.fromPromise<A>(promise: Promise<A>): Promise<Result<ExtractValue<A>, globalThis.Error>>;
So AR.make
seems more flexible than R.fromPromise
in that it:
Error
typeOk
typesIs the difference here intentional?
By the way, why is there a NonNullable
constraint on R.fromPromise
anyway?
Hello @mobily
When do you expect to publish the official version?
Thank you for your incredible work 👍
Pardon me if this has been covered already, but is there any way to essentially have a sequential async pipe with this new system? Ideally we could do async operations in the pipe, and have the promise resolve before moving on to the next step? -- I'm mainly thinking about db and API calls, that pass data into the next call and so forth.
ie something like:
const pipeline = asyncPipe(
createValueAsync,
D.updateAsync,
D.updateAsync,
);
const result = await pipeline;
v4
:)I see that the last commit was on January. I want to use ts-belt
in my project, but I'm a bit worried about its future. @mobily Just to have some idea of what to expect: do you plan to keep maintaining ts-belt
or did you maybe stop using it yourself in your own projects or something else? By the way, thanks for your work until here, ts-belt
looks awesome.
While it seems that Marcin is busy with another project, I wanted to mention that I've been using ts-belt@4.0.0 in production for almost a year and it's been going pretty well. I only wish we could merge it into main one day.
Pardon me if this has been covered already, but is there any way to essentially have a sequential async pipe with this new system? Ideally we could do async operations in the pipe, and have the promise resolve before moving on to the next step? -- I'm mainly thinking about db and API calls, that pass data into the next call and so forth.
ie something like:
const pipeline = asyncPipe( createValueAsync, D.updateAsync, D.updateAsync, ); const result = await pipeline;
@IAmNatch I added this in my own code base, but would be nice if ts-belt has it!
/**
* Performs left-to-right async composition (the first argument must be a value).
*/
export function pipeAsync<A, B>(value: A, fn1: Task<A, B>): Promise<B>;
export function pipeAsync<A, B, C>(value: A, fn1: Task<A, B>, fn2: Task<B, C>): Promise<C>;
// ... add more function overloads as you require here...
export async function pipeAsync<A, B>(value: A, ...fns: Task<unknown, unknown>[]): Promise<B> {
return fns.reduce<unknown>(async (acc, fn) => await fn(await acc), value) as B;
}
type Task<A, B> = (arg: A) => Promise<B> | B;
Or flowAsync
/**
* Performs left-to-right promise composition and returns a new function, the first argument may have any arity, the remaining arguments must be unary.
*/
export function flowAsync<A extends Args, B>(fn1: LeadingTask<A, B>): (...args: A) => Promise<B>;
export function flowAsync<A extends Args, B, C>(fn1: LeadingTask<A, B>, fn2: TrailingTask<B, C>): (...args: A) => Promise<C>;
// ... add more function overloads as you require here...
export function flowAsync<A extends Args, B>(
fn1: LeadingTask<A, unknown>,
...fns: TrailingTask<unknown, unknown>[]
): (...args: A) => Promise<B> {
return (...args: A) =>
fns.reduce<unknown>(async (acc, fn) => await fn(acc), fn1(...args)) as Promise<B>;
}
type Args = ReadonlyArray<unknown>;
type LeadingTask<A extends Args, B> = (...args: A) => Promise<B> | B;
type TrailingTask<A, B> = (arg: A) => Promise<B> | B;
Examples:
const notificationSettingsByUserId = await pipeAsync(
"0a0ea077-22e7-4735-af13-e2ec0279c7f1",
getUserById,
getNotificationSettingsOfUser
)
const getNotificationSettingsByUserId = flowAsync(
getUserById,
getNotificationSettingsOfUser
)
await getNotificationSettingsByUserId("0a0ea077-22e7-4735-af13-e2ec0279c7f1")
Another feature that I think would be useful as a Function scope utility (or new Thunk scope? ), is a way to apply unary thunks in point free notation. A particular use case where I find this useful, is when isolating side effects in (function) composition patterns. It works similar to F.identity
but for unary functions or thunks, instead of values.
Simple example thunk for isolating reading from local storage:
const readFromStorage = (key) => () => { ... logic }
const readUserFromStorage = readFromStorage("some-user-id");
const user = readUserFromStorage();
When applying this pattern in composition, it is awkward (or impossible?) to write point free:
const result = pipe(
"some-user-id",
(id) => readFromStorage(id)()
)
At the moment I am using my own utility, which I simply call apply
. It looks like this (taken from some production code base but edited to fit here):
// isolated local storage read op side effect
const readFromStorage = <T = unknown>(key: string) => () => pipe(
R.fromExecution(() => localStorage.getItem(key)),
R.tapError(console.error),
R.flatMap(R.fromNullable("data is null")),
R.flatMap(parseJson<T>),
);
const parseJson = <T = unknown>(value: string) => pipe(
R.fromExecution(() => JSON.parse(value)),
R.tapError(console.error),
R.map(F.coerce<T>),
)
const getUserStorageId = (userId: string) => `user:${userId}`;
const getProgressStorageId = (progressId: string) => `progress:${progressId}`;
/**
* Find user progress by user ID in a normalised JSON storage
*/
const getProgressByUserId = flow(
getUserStorageId,
apply(readFromStorage<User>), // apply functor with user store id in composition
R.map(D.get('progressId')),
R.map(getProgressStorageId),
R.flatMap(apply(readFromStorage<Progress>)), // apply functor with progress storage id in composition
R.toNullable
)
const progress = getProgressByUserId("73d6aa07")
TypeScript implementation of my apply
utility:
export function apply<A, B>(arg: A, fn: (arg: A) => B): ReturnType<typeof fn>;
export function apply<A, B extends (...args: readonly unknown[]) => unknown>(
fn: (arg: A) => B,
): (arg: A) => ReturnType<ReturnType<typeof fn>>;
export function apply<A, B>(
argOrFn: A | ((arg: A) => (...args: readonly unknown[]) => B),
fn?: (arg: A) => B,
) {
return argOrFn instanceof Function ? (arg: A) => argOrFn(arg)() : fn!(argOrFn);
}
hello everyone! 👋 please accept my apologies for the inactivity, you can read more here: https://github.com/mobily/ts-belt/issues/93
found Option.fold was missing from the release note above, and the implementation should be fixed. current implementation is same as O.match. It should be behave as same as O.getWithDefault.
hello there! 👋
I have been working on the new version of ts-belt with support for
Async*
modules for a quite long time, and now I feel it's a great time to publish (at least) arelease candidate
version. It's also an excellent opportunity to gather feedback from you :) The bad news is, the docs for these modules are missing at the moment (I'm working on it!), but let me describe the essentials of each module:Installation
AsyncData
AsyncData
contains a variant type for representing the different states in which a value can be during an asynchronous data load.There are four possible states:
Init
,Loading
,Reloading
, andComplete
.Variant constructors:
AD.Init
AD.Loading
AD.Reloading(value)
AD.Complete(value)
AD.makeInit()
AD.makeLoading()
AD.makeReloading(value)
AD.makeComplete(value)
Functions:
AD.isInit
AD.isLoading
AD.isReloading
AD.isComplete
AD.isBusy
AD.isIdle
AD.isEmpty
AD.isNotEmpty
AD.toBusy
AD.toIdle
AD.getValue
AD.getWithDefault
AD.getReloading
AD.getComplete
AD.map
AD.mapWithDefault
AD.flatMap
AD.tapInit
AD.tapLoading
AD.tapReloading
AD.tapComplete
AD.tapEmpty
AD.tapNotEmpty
AD.all
AD.fold
Example: https://codesandbox.io/s/cool-star-6m87kk?file=/src/App.tsx
AsyncDataResult
AsyncDataResult
is basically an alias ofAsyncData<Result<Ok, Error>>
. This variant type can be used to represent the different states in which a data value can exist while being loaded asynchronously, with the possibility of either success or failure.Variant constructors:
ADR.Init
ADR.Loading
ADR.ReloadingOk(value)
ADR.ReloadingError(error)
ADR.CompleteOk(value)
ADR.CompleteError(error)
ADR.makeInit()
ADR.makeLoading()
ADR.makeReloadingOk(value)
ADR.makeReloadinError(error)
ADR.makeCompleteOk(value)
ADR.makeCompleteError(error)
Functions:
ADR.isOk
ADR.isError
ADR.isReloadingOk
ADR.isReloadingError
ADR.isCompleteOk
ADR.isCompleteError
ADR.getOk
ADR.getReloadingOk
ADR.getCompleteOk
ADR.getError
ADR.getReloadingError
ADR.getCompleteError
ADR.map
ADR.mapError
ADR.flatMap
ADR.tap
ADR.fold
ADR.foldOk
ADR.toAsyncData
Example: https://codesandbox.io/s/brave-cloud-ov30h7?file=/src/App.tsx
AsyncOption
Same as
Option
but for handling asynchronous operations.Variant constructors:
AO.make(promise)
AO.resolve(value)
AO.reject()
Functions:
AO.filter
AO.map
AO.flatMap
AO.fold
AO.mapWithDefault
AO.match
AO.toNullable
AO.toUndefined
AO.toResult
AO.getWithDefault
AO.isNone
AO.isSome
AO.tap
AO.contains
AO.flatten
AsyncResult
Same as
Result
but for handling asynchronous operations.Variant constructors:
AR.make(promise)
AR.resolve(value)
AR.reject(error)
Functions:
AR.flatMap
AR.fold
AR.map
AR.mapWithDefault
AR.getWithDefault
AR.filter
AR.match
AR.toNullable
AR.toOption
AR.toUndefined
AR.isOk
AR.isError
AR.tap
AR.tapError
AR.handleError
AR.mapError
AR.catchError
AR.recover
AR.flip
AR.flatten
Minor changes
A.sample
(gets a random element from provided array)O.all
(transforms an array ofOption(s)
into a singleOption
data type)R.all
(transforms an array ofResult(s)
into a singleResult
data type)R.filter
(returnsOk(value)
ifresult
isOk(value)
and the result ofpredicateFn
is truthy, otherwise, returnsError
)groupBy
signatureBreaking changes
ts-belt@v4 does not support
Flow
, due to a lack of proper features inflowgen
, sorry about that!Feel free to post your thoughts, any kind of feedback would be greatly appreciated! 💪