-
-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
β¨ Introducing ts-belt
v4 (release candidate)
#51
Comments
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 |
Looks good, but I am starting to get lost in all that shortcuts |
@alexn-s the constructor of the const result = await pipe(
AR.make(promiseFn()),
AR.map(value => β¦),
AR.getWithDefault(β¦),
)
@lulldev that sounds like a great idea, I will add it to
@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 #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 |
@ivan-kleshnin added in 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 β @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
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 |
@kirillrogovoy it totally makes sense, I will update both,
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 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) |
Such ultra generic functions fail shortly with type inference, at least in my experience. I've personally started to use When you replace a seed literal |
@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 TypeScript does not have typeclasses (yet?) so categorical applications of such generic functions are out of question. |
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. π
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.
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 One thing I'm missing, maybe also related to the above, is getting a proper flow with 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 |
@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 That said, there's currently no good solution in ts-belt either. If you use |
Is this still accurate? They seem similar on the surface, but looking at the types there's differences, no?
So
Is the difference here intentional? |
By the way, why is there a |
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; |
#87 Please also consider this for |
I see that the last commit was on January. I want to use |
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. |
@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 /**
* 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 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 // 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 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: #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.mapWithDefault. |
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! πͺ
The text was updated successfully, but these errors were encountered: