seangwright / typescript-functional-extensions

A TypeScript implementation of the C# library CSharpFunctionalExtensions, including synchronous and asynchronous Maybe and Result monads.
MIT License
34 stars 4 forks source link

Code Flow improvements #13

Closed GregOnNet closed 2 years ago

GregOnNet commented 2 years ago

Is your feature request related to a problem? Please describe.

I experience that I sometimes have a condition that decides whether successive operation are skipped.

Example:


if (isIrrelevant) {
  return job.discard
}

// continue

I wonder how this can be done using our Result-Monad.

Currently, I need to escape the monad, then doing some checks & re-enter the monad.

Maybe, I do not think the right monad-way.

Describe the solution you'd like

Based on a given condition, I want to execute different code-branches.

  result
    .do({
      if: value => value === value.isRelevant
      then: value => /* execute if === true */
      else: value  =>/* execute if === false /*
   })

Describe alternatives you've considered

I did some research but did not find an answer to my problem. There is the Either-Monad. This monad seems to be coupled to success/error-Handling.

Additional context Add any other context or screenshots about the feature request here.

seangwright commented 2 years ago

While there isn't something as structured for the if/then/else flow as your example, you can create this with tap()

Result.success(10)
  .tap(number => {
    if (number >= 5) {
      // 
    } else {
      //
    }
  });

There's also ensure which will convert Result to a failure state if the predicate is false

Result.success(10)
  .ensure(number => number >= 5, number => `The number ${number} was not greater than or equal to 5`)
  .map(...);

Finally, there's check which conditionally makes the Result dependent on another

Result.success(10)
  .check(number => validateNumber(number)) // validateNumber returns some Result<T> but check returns Result<number>
  .tap(number => {
      console.log(`This will be logged if ${number} was validated`);
  });

Maybe we could add a separate utility for Result called doIfElse

function doIfElse<T>(predicate: PredicateOfT<T>, thenFn: ActionOfT<T>, elseFn: ActionOfT<T>): ActionOfT<T> {
    return (value) => {
        if (predicate(value)) {
            thenFn(value);
        } else {
            elseFn(value);
       }
    };
}

Result.success(10)
  .tap(doIfElse(
      number => number >= 5, 
      number => console.log('greater than or equal'), 
      number => console.log('less than')));

I'd prefer not to add to Result a method that feels slightly unrelated to what Result does.

GregOnNet commented 2 years ago

Hey,

I came across check, too. However, I did not understand it, but thanks to your Sample, I see clear now, thanks.

I agree that if-else-Branching should not be part of Result, since it does not feel right.

So check is definitely one possibility.

In my case, I finally noticed that I can "re-enter" the monad after exiting it.

// ...
cosnt result = await this.service.do() // do returns Promise<Result<T>>

if (result.hasError()) {
  return
}

return result.mapAsync(value => this.otherService.continue())
GregOnNet commented 2 years ago

Closing this issue since I think the discussion already provided enough valuable information. :-)