Skip to content
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

Open
mobily opened this issue Nov 2, 2022 · 34 comments
Open

✨ Introducing ts-belt v4 (release candidate) #51

mobily opened this issue Nov 2, 2022 · 34 comments

Comments

@mobily
Copy link
Owner

mobily commented Nov 2, 2022

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) a release 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

yarn add @mobily/ts-belt@next

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, and Complete.

type AsyncData<T> = Init | Loading | Reloading<T> | Complete<T>
import { AD } from '@mobily/ts-belt'

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 of AsyncData<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.

type AsyncDataResult<A, B> = AsyncData<Result<A, B>>
import { ADR } from '@mobily/ts-belt'

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.

type AsyncOption<T> = Promise<Option<T>>
import { AO } from '@mobily/ts-belt'

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.

type AsyncResult<A, B> = Promise<Result<A, B>>
import { AR } from '@mobily/ts-belt'

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

  • ✨ added A.sample (gets a random element from provided array)
  • ✨ added O.all (transforms an array of Option(s) into a single Option data type)
  • ✨ added R.all (transforms an array of Result(s) into a single Result data type)
  • ✨ added R.filter (returns Ok(value) if result is Ok(value) and the result of predicateFn is truthy, otherwise, returns Error)
  • πŸ› fixed up the groupBy signature

Breaking changes

ts-belt@v4 does not support Flow, due to a lack of proper features in flowgen, sorry about that!

Feel free to post your thoughts, any kind of feedback would be greatly appreciated! πŸ’ͺ

@alexn-s
Copy link

alexn-s commented Nov 6, 2022

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?

@kalagin
Copy link

kalagin commented Nov 14, 2022

How about alternative match/with from ts-pattern out of box? something like fold but for handling all or each states?

@Nodonisko
Copy link
Contributor

Nodonisko commented Nov 14, 2022

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?

@mobily
Copy link
Owner Author

mobily commented Nov 14, 2022

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 #35 however it's been closed recently, sorry @cevr!)

@kalagin
Copy link

kalagin commented Nov 15, 2022

@mobily can i help with release candidate and contribute?

@mobily
Copy link
Owner Author

mobily commented Nov 15, 2022

@lulldev sure thing, that would be awesome! do you need anything from me to get started?

@kalagin
Copy link

kalagin commented Nov 16, 2022

@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?

@ivan-kleshnin
Copy link

ivan-kleshnin commented Dec 23, 2022

@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?

@mobily
Copy link
Owner Author

mobily commented Jan 10, 2023

@ivan-kleshnin added in v4.0.0-rc.5 πŸš€ 14a5c15

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

@mobily
Copy link
Owner Author

mobily commented Jan 10, 2023

other new additions or bug fixes in v4:

  • πŸ› fixed A.takeWhile type definition
  • πŸ› fixed type definitions for R.flatMap and AR.flatMap
  • ✨ added ADR.all
  • ✨ added 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!

@kirillrogovoy
Copy link

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?

@kirillrogovoy
Copy link

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 (?).

@mobily
Copy link
Owner Author

mobily commented Feb 2, 2023

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

@kirillrogovoy
Copy link

kirillrogovoy commented Feb 2, 2023

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.

@cevr
Copy link

cevr commented Feb 2, 2023

@kirillrogovoy i think this would make sense for all utilities that are shared (map, flatmap, fold)

@ivan-kleshnin
Copy link

ivan-kleshnin commented Feb 2, 2023

@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".

@kirillrogovoy
Copy link

@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.

@ivan-kleshnin
Copy link

ivan-kleshnin commented Feb 3, 2023

@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?

@kirillrogovoy
Copy link

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.

@ivan-kleshnin
Copy link

ivan-kleshnin commented Feb 4, 2023

@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 πŸ‘

@M3kH
Copy link

M3kH commented Feb 5, 2023

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.

@afoures
Copy link

afoures commented Mar 2, 2023

@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

@kirillrogovoy
Copy link

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'>

@denizdogan
Copy link

denizdogan commented Jun 13, 2023

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:

  • Allows selecting the Error type
  • Allows nullable Ok types

Is the difference here intentional?

@denizdogan
Copy link

By the way, why is there a NonNullable constraint on R.fromPromise anyway?

@mattaiod
Copy link

Hello @mobily

When do you expect to publish the official version?

Thank you for your incredible work πŸ‘

@IAmNatch
Copy link

IAmNatch commented Sep 14, 2023

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;

@Visual-Dawg
Copy link

#87 Please also consider this for v4 :)

@gustavopch
Copy link

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.

@kirillrogovoy
Copy link

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.

@stefvw93
Copy link

stefvw93 commented Nov 24, 2023

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")

@stefvw93
Copy link

stefvw93 commented Nov 24, 2023

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);
}

@mobily
Copy link
Owner Author

mobily commented Dec 2, 2023

hello everyone! πŸ‘‹ please accept my apologies for the inactivity, you can read more here: #93

@JUSTIVE
Copy link
Sponsor

JUSTIVE commented Feb 10, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests