Skip to content

Commit

Permalink
Option: add reduceAll
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Feb 7, 2023
1 parent ae3338c commit 14f87fb
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-fishes-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fp-ts/core": patch
---

Option: add `reduceAll`
46 changes: 43 additions & 3 deletions docs/modules/Option.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ Added in v1.0.0
- [multiply](#multiply)
- [multiplyAll](#multiplyall)
- [of](#of)
- [reduce](#reduce)
- [reduceAll](#reduceall)
- [struct](#struct)
- [subtract](#subtract)
- [sum](#sum)
Expand Down Expand Up @@ -874,7 +876,7 @@ Returns the contained value if the option is `Some`, otherwise throws an error.
**Signature**

```ts
export declare const getOrThrow: (onError?: any) => <A>(self: Option<A>) => A
export declare const getOrThrow: (onNone?: any) => <A>(self: Option<A>) => A
```

Added in v1.0.0
Expand Down Expand Up @@ -1467,7 +1469,7 @@ Added in v1.0.0
**Signature**

```ts
export declare const multiplyAll: any
export declare const multiplyAll: (self: Iterable<Option<unknown>>) => number
```

Added in v1.0.0
Expand All @@ -1482,6 +1484,44 @@ export declare const of: <A>(a: A) => Option<A>

Added in v1.0.0

## reduce

**Signature**

```ts
export declare const reduce: <A, B>(b: B, f: (b: B, a: A) => B) => (self: Option<A>) => B
```

Added in v1.0.0

## reduceAll

Reduces an `Iterable` of `Option<A>` to a single value of type `B`.

**Signature**

```ts
export declare const reduceAll: <B, A>(b: B, f: (b: B, a: A) => B) => (self: Iterable<Option<A>>) => B
```

**Example**

```ts
import { some, none, reduceAll } from '@fp-ts/core/Option'
import { pipe } from '@fp-ts/core/Function'

const iterable = [some(1), none(), some(2), none()]
assert.strictEqual(
pipe(
iterable,
reduceAll(0, (b, a) => b + a)
),
3
)
```

Added in v1.0.0

## struct

**Signature**
Expand Down Expand Up @@ -1519,7 +1559,7 @@ Added in v1.0.0
**Signature**

```ts
export declare const sumAll: any
export declare const sumAll: (self: Iterable<Option<unknown>>) => number
```

Added in v1.0.0
Expand Down
66 changes: 48 additions & 18 deletions guides/Option.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,22 +308,18 @@ console.log(pipe(some(10), getOrThrow()); // 10
console.log(pipe(none(), getOrThrow()); // throws new Error("getOrThrow called on a None")
```
An alternative more safe is that of doing [pattern matching](https://github.com/gvergnaud/ts-pattern#what-is-pattern-matching) on the `Option`.
A more safe alternative is [pattern matching](https://github.com/gvergnaud/ts-pattern#what-is-pattern-matching) on the `Option`.
The `match` function allows us to match on the `None` and `Some` cases of an `Option` value and provide different actions for each.
The `match` function allows you to match on the `None` and `Some` cases of an `Option` value and provide different actions for each.
For example we can use the `match` function to handle the `Option` value returned by `parseNumber` and decide what to do based on whether it's a `None` or a `Some`.
```ts
import { pipe } from "@fp-ts/core/Function";
import { match } from "@fp-ts/core/Option";

// parseNumber returns an Option<number>
const result = parseNumber("Not a number");

// Using pipe and match to pattern match on the result
const output = pipe(
result,
parseNumber("Not a number"),
match(
// If the result is a None, return an error string
() => `Error: ${error}`,
Expand All @@ -335,16 +331,50 @@ const output = pipe(
console.log(output); // Output: Error: Cannot parse 'Not a number' as a number
```
There are specializations of `match` to make working with code that does not use `Option` more convenient and faster, particularly `getOrNull` and `getOrUndefined`.
```ts
import { getOrNull, getOrUndefined } from "fp-ts/core/Option";

pipe(some(5), getOrNull); // 5
pipe(none(), getOrNull); // null

pipe(some(5), getOrUndefined); // 5
pipe(none(), getOrUndefined); // undefined
```
For greater flexibility, there is also the `getOrElse` function which allows you to set what value corresponds to the `None` case:
```ts
import { getOrElse } from "fp-ts/core/Option";

pipe(
some(5),
getOrElse(() => 0)
); // 5
pipe(
none(),
getOrElse(() => 0)
); // 0
```
**Cheat sheet** (error handling)
| Name | Given | To |
| --------------------- | --------------------------------------------------- | ---------------------- |
| `match` | `Option<A>`, `onNone: LazyArg<B>`, `onSome: A => C` | `B \| C` |
| `getOrElse` | `Option<A>`, `onNone: LazyArg<B>` | `A \| B` |
| `getOrNull` | `Option<A>` | `A \| null` |
| `getOrUndefined` | `Option<A>` | `A \| undefined` |
| `orElse` | `Option<A>`, `LazyArg<Option<B>>` | `Option<A \| B>` |
| `orElseEither` | `Option<A>`, `LazyArg<Option<B>>` | `Option<Either<A, B>>` |
| `firstSomeOf` | `Iterable<Option<A>>` | `Option<A>` |
| `getFailureSemigroup` | `Semigroup<A>` | `Semigroup<Option<A>>` |
| `getFailureMonoid` | `Monoid<A>` | `Monoid<Option<A>>` |
| Name | Given | To |
| ---------------- | --------------------------------------------------- | ---------------- |
| `match` | `Option<A>`, `onNone: LazyArg<B>`, `onSome: A => C` | `B \| C` |
| `getOrThrow` | `Option<A>`, `onNone?: LazyArg<Error>` | `A` |
| `getOrNull` | `Option<A>` | `A \| null` |
| `getOrUndefined` | `Option<A>` | `A \| undefined` |
| `getOrElse` | `Option<A>`, `onNone: LazyArg<B>` | `A \| B` |
# Combining two or more `Option`s
**Cheat sheet** (combining)
| Name | Given | To |
| -------------- | ----------------------------------------- | ---------------------- |
| `orElse` | `Option<A>`, `LazyArg<Option<B>>` | `Option<A \| B>` |
| `orElseEither` | `Option<A>`, `LazyArg<Option<B>>` | `Option<Either<A, B>>` |
| `firstSomeOf` | `Iterable<Option<A>>` | `Option<A>` |
| `reduceAll` | `Iterable<Option<A>>`, `B`, `(B, A) => B` | `B` |
45 changes: 39 additions & 6 deletions src/Option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,21 @@ export const liftThrowable = <A extends ReadonlyArray<unknown>, B>(
/**
* Returns the contained value if the option is `Some`, otherwise throws an error.
*
* @param onError - An optional function that returns the error to be thrown when the option is `None`
* @param onNone - An optional function that returns the error to be thrown when the option is `None`
* @param self - The option to extract the value from
* @throws The error returned by `onError` if the option is `None`
*
* @category interop
* @since 1.0.0
*/
export const getOrThrow = (
onError: LazyArg<Error> = () => new Error("getOrThrow called on a None")
onNone: LazyArg<Error> = () => new Error("getOrThrow called on a None")
) =>
<A>(self: Option<A>): A => {
if (isSome(self)) {
return self.value
}
throw onError()
throw onNone()
}

/**
Expand Down Expand Up @@ -707,12 +707,18 @@ export const Alternative: alternative.Alternative<OptionTypeLambda> = {
...Coproduct
}

/**
* @since 1.0.0
*/
export const reduce = <A, B>(b: B, f: (b: B, a: A) => B) =>
(self: Option<A>): B => isNone(self) ? b : f(b, self.value)

/**
* @category instances
* @since 1.0.0
*/
export const Foldable: foldable.Foldable<OptionTypeLambda> = {
reduce: (b, f) => (self) => isNone(self) ? b : f(b, self.value)
reduce
}

/**
Expand Down Expand Up @@ -1207,12 +1213,39 @@ export const subtract = lift2Curried((a: number, b: number) => a - b)
*/
export const divide = lift2Curried((a: number, b: number) => a / b)

/**
* Reduces an `Iterable` of `Option<A>` to a single value of type `B`.
*
* @since 1.0.0
*
* @param b - The initial value of the accumulator
* @param f - The reducing function that takes the current accumulator value and the unwrapped value of an `Option<A>`
* @param self - The Iterable of `Option<A>` to be reduced
*
* @example
* import { some, none, reduceAll } from '@fp-ts/core/Option'
* import { pipe } from '@fp-ts/core/Function'
*
* const iterable = [some(1), none(), some(2), none()]
* assert.strictEqual(pipe(iterable, reduceAll(0, (b, a) => b + a)), 3)
*/
export const reduceAll = <B, A>(b: B, f: (b: B, a: A) => B) =>
(self: Iterable<Option<A>>): B => {
let out: B = b
for (const oa of self) {
if (isSome(oa)) {
out = f(out, oa.value)
}
}
return out
}

/**
* @since 1.0.0
*/
export const sumAll = getOptionalMonoid(N.SemigroupSum).combineAll
export const sumAll = reduceAll(0, N.SemigroupSum.combine)

/**
* @since 1.0.0
*/
export const multiplyAll = getOptionalMonoid(N.SemigroupMultiply).combineAll
export const multiplyAll = reduceAll(1, N.SemigroupMultiply.combine)
17 changes: 13 additions & 4 deletions test/Option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ describe.concurrent("Option", () => {
expect(_.Alternative).exist

expect(_.Foldable).exist
expect(_.reduce).exist
expect(_.reduceAll).exist
expect(_.toArray).exist

expect(_.Traversable).exist
Expand Down Expand Up @@ -592,13 +594,20 @@ describe.concurrent("Option", () => {
expect(pipe(_.some(6), _.divide(_.some(3)))).toEqual(_.some(2))
})

it("reduce", () => {
expect(pipe(_.none(), _.reduce(0, (b, a) => b + a))).toEqual(0)
expect(pipe(_.some(1), _.reduce(0, (b, a) => b + a))).toEqual(1)
})

it("sumAll", () => {
expect(_.sumAll([_.some(2), _.some(3)])).toEqual(_.some(5))
expect(_.sumAll([_.some(2), _.none(), _.some(3)])).toEqual(_.some(5))
expect(_.sumAll([])).toEqual(0)
expect(_.sumAll([_.some(2), _.some(3)])).toEqual(5)
expect(_.sumAll([_.some(2), _.none(), _.some(3)])).toEqual(5)
})

it("multiplyAll", () => {
expect(_.multiplyAll([_.some(2), _.some(3)])).toEqual(_.some(6))
expect(_.multiplyAll([_.some(2), _.none(), _.some(3)])).toEqual(_.some(6))
expect(_.multiplyAll([])).toEqual(1)
expect(_.multiplyAll([_.some(2), _.some(3)])).toEqual(6)
expect(_.multiplyAll([_.some(2), _.none(), _.some(3)])).toEqual(6)
})
})

0 comments on commit 14f87fb

Please sign in to comment.