Skip to content

Commit

Permalink
Merge pull request #158 from seasonedcc/documentation-on-plain-fns
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavoguichard authored Jun 27, 2024
2 parents a40510d + 4982b51 commit c63a18d
Show file tree
Hide file tree
Showing 17 changed files with 8,107 additions and 19,255 deletions.
83 changes: 40 additions & 43 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const result = await runtimeSafeAdd('1', null)

# Combinators

These combinators are useful for composing composables. They all return another `Composable`, thus allowing further application in more compositions.
These combinators are useful for composing functions. They operate on either plain functions or composables. They all return a `Composable`, thus allowing further application in more compositions.

## all

Expand All @@ -187,9 +187,9 @@ It will pass the same arguments to each provided function.
If __all constituent functions__ are successful, The `data` field (on the composite function's result) will be a tuple containing each function's output.

```ts
const a = composable(({ id }: { id: number }) => String(id))
const b = composable(({ id }: { id: number }) => id + 1)
const c = composable(({ id }: { id: number }) => Boolean(id))
const a = ({ id }: { id: number }) => String(id)
const b = ({ id }: { id: number }) => id + 1
const c = ({ id }: { id: number }) => Boolean(id)

const result = await all(a, b, c)({ id: 1 })
// ^? Result<[string, number, boolean]>
Expand All @@ -211,9 +211,9 @@ If any of the constituent functions fail, the `errors` field (on the composite f
const a = withSchema(z.object({ id: z.number() }))(({ id }) => {
return String(id)
})
const b = composable(() => {
const b = () => {
throw new Error('Error')
})
}

const result = await all(a, b)({ id: '1' })
// ^? Result<[string, never]>
Expand All @@ -233,9 +233,9 @@ Use `branch` to add conditional logic to your compositions.
It receives a composable and a predicate function that should return the next composable to be executed based on the previous function's output, like `pipe`.

```ts
const getIdOrEmail = composable((data: { id?: number, email?: string }) => {
const getIdOrEmail = (data: { id?: number, email?: string }) => {
return data.id ?? data.email
})
}
const findUserById = composable((id: number) => {
return db.users.find({ id })
})
Expand All @@ -259,7 +259,7 @@ For the example above, the result will be:
```
If you don't want to pipe when a certain condition is matched, you can return `null` like so:
```ts
const a = composable(() => 'a')
const a = () => 'a'
const b = composable(() => 'b')
const fn = branch(a, (data) => data === 'a' ? null : b)
// ^? Composable<() => 'a' | 'b'>
Expand Down Expand Up @@ -287,7 +287,7 @@ You can catch an error in a `Composable`, using `catchFailure` which is similar
```typescript
import { composable, catchFailure } from 'composable-functions'

const getUser = composable((id: string) => fetchUser(id))
const getUser = (id: string) => fetchUser(id)
// ^? Composable<(id: string) => User>
const getOptionalUser = catchFailure(getUser, (errors, id) => {
console.log(`Failed to fetch user with id ${id}`, errors)
Expand All @@ -303,9 +303,9 @@ const getOptionalUser = catchFailure(getUser, (errors, id) => {
The motivation for this is that an object with named fields is often preferable to long tuples, when composing many composables.

```ts
const a = composable(() => '1')
const b = composable(() => 2)
const c = composable(() => true)
const a = () => '1'
const b = () => 2
const c = () => true

const results = await collect({ a, b, c })({})
// ^? Result<{ a: string, b: number, c: boolean }>
Expand All @@ -330,19 +330,17 @@ When the given composable fails, its error is returned wihout changes.
If successful, mapper will receive the output of the composable as input.

```ts
const add = composable((a: number, b: number) => a + b)
const add = (a: number, b: number) => a + b
const addAndMultiplyBy2 = map(add, sum => sum * 2)
```

This can be useful when composing functions. For example, you might need to align input/output types in a pipeline:

```ts
const fetchAsText = composable(
({ userId }: { userId: number }) =>
fetch(`https://reqres.in/api/users/${String(userId)}`).then((r) =>
r.json(),
),
)
const fetchAsText = ({ userId }: { userId: number }) => {
return fetch(`https://reqres.in/api/users/${String(userId)}`)
.then((r) => r.json())
}
const fullName = withSchema(
z.object({ first_name: z.string(), last_name: z.string() }),
)(({ first_name, last_name }) => `${first_name} ${last_name}`)
Expand All @@ -369,7 +367,7 @@ For the example above, the result will be something like this:
`map` will also receive the input parameters of the composable as arguments:

```ts
const add = composable((a: number, b: number) => a + b)
const add = (a: number, b: number) => a + b
const aggregateInputAndOutput = map(add, (result, a, b) => ({ result, a, b }))
// ^? Composable<(a: number, b: number) => { result: number, a: number, b: number }>
```
Expand All @@ -383,12 +381,12 @@ This could be useful when adding any layer of error handling.
In the example below, we are counting the errors but disregarding the contents:

```ts
const increment = composable((n: number) => {
const increment = (n: number) => {
if (Number.isNaN(n)) {
throw new Error('Invalid input')
}
return n + 1
})
}
const summarizeErrors = (errors: Error[]) =>
[new Error('Number of errors: ' + errors.length)]

Expand All @@ -410,12 +408,11 @@ For the example above, the `result` will be:
It takes a Composable and a function that will map the input parameters to the expected input of the given Composable. Good to adequate the output of a composable into the input of the next composable in a composition. The function must return an array of parameters that will be passed to the Composable.

```ts
const getUser = composable(({ id }: { id: number }) => db.users.find({ id }))
// ^? Composable<(input: { id: number }) => User>
const getUser = ({ id }: { id: number }) => db.users.find({ id })

const getCurrentUser = mapParameters(
getUser,
(_input, user: { id: number }) => [{ id: user.id }]
(_input: unknown, user: { id: number }) => [{ id: user.id }]
)
// ^? Composable<(input: unknown, ctx: { id: number }) => User>
```
Expand All @@ -427,9 +424,9 @@ It will pass the output of a function as the next function's input in left-to-ri
The resulting data will be the output of the rightmost function.

```ts
const a = composable((aNumber: number) => String(aNumber))
const b = composable((aString: string) => aString == '1')
const c = composable((aBoolean: boolean) => !aBoolean)
const a = (aNumber: number) => String(aNumber)
const b = (aString: string) => aString == '1'
const c = (aBoolean: boolean) => !aBoolean

const d = pipe(a, b, c)

Expand All @@ -455,9 +452,9 @@ If one functions fails, execution halts and the error is returned.
Instead of the `data` field being the output of the last composable, it will be a tuple containing each intermediate output (similar to the `all` function).

```ts
const a = composable((aNumber: number) => String(aNumber))
const b = composable((aString: string) => aString == '1')
const c = composable((aBoolean: boolean) => !aBoolean)
const a = (aNumber: number) => String(aNumber)
const b = (aString: string) => aString == '1'
const c = (aBoolean: boolean) => !aBoolean

const d = sequence(a, b, c)

Expand All @@ -478,8 +475,8 @@ For the example above, the result will be:
If you'd rather have a sequential combinator that returns an object - similar to collect - instead of a tuple, you can use the `map` function like so:

```ts
const a = composable((aNumber: number) => String(aNumber))
const b = composable((aString: string) => aString === '1')
const a = (aNumber: number) => String(aNumber)
const b = (aString: string) => aString === '1'

const c = map(sequence(a, b), ([a, b]) => ({ aString: a, aBoolean: b }))

Expand Down Expand Up @@ -520,7 +517,7 @@ const trackErrors = trace(async (result, ...args) => {
# Input Resolvers
We export some functions to help you extract values out of your requests before sending them as user input.

These functions are better suited for use with `withSchema` rather than `composable` since they deal with external data and `withSchema` will ensure type-safety in runtime.
These functions are better suited for composables with runtime validation, such as those built with `withSchema` (or `applySchema`) since they deal with external data and `withSchema` will ensure type-safety in runtime.

For more details on how to structure your data, refer to this [test file](./src/tests/input-resolvers.test.ts).

Expand Down Expand Up @@ -792,9 +789,9 @@ It is the same as `branch` but it will forward the context to the next composabl
```ts
import { context } from 'composable-functions'

const getIdOrEmail = composable((data: { id?: number, email?: string }) => {
const getIdOrEmail = (data: { id?: number, email?: string }) => {
return data.id ?? data.email
})
}
const findUserById = composable((id: number, ctx: { user: User }) => {
if (!ctx.user.admin) {
throw new Error('Unauthorized')
Expand All @@ -819,9 +816,9 @@ Similar to `pipe` but it will forward the context to the next composable.
```ts
import { context } from 'composable-functions'

const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString == '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)
const a = (aNumber: number, ctx: { user: User }) => String(aNumber)
const b = (aString: string, ctx: { user: User }) => aString == '1'
const c = (aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin

const d = context.pipe(a, b, c)

Expand All @@ -834,9 +831,9 @@ Similar to `sequence` but it will forward the context to the next composable.
```ts
import { context } from 'composable-functions'

const a = composable((aNumber: number, ctx: { user: User }) => String(aNumber))
const b = composable((aString: string, ctx: { user: User }) => aString === '1')
const c = composable((aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin)
const a = (aNumber: number, ctx: { user: User }) => String(aNumber)
const b = (aString: string, ctx: { user: User }) => aString === '1'
const c = (aBoolean: boolean, ctx: { user: User }) => aBoolean && ctx.user.admin

const d = context.sequence(a, b, c)

Expand Down
50 changes: 36 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ npm i composable-functions
```tsx
import { composable, pipe } from 'composable-functions'

const faultyAdd = composable((a: number, b: number) => {
const faultyAdd = (a: number, b: number) => {
if (a === 1) throw new Error('a is 1')
return a + b
})
const show = composable(String)
}
const show = (a: number) => String(a)
const addAndShow = pipe(faultyAdd, show)

const result = await addAndShow(2, 2)
Expand Down Expand Up @@ -116,20 +116,30 @@ For more information and examples, check the [Handling external input](./with-sc

A `Composable` is a function that returns a `Promise<Result<T>>` where `T` is any type you want to return. Values of the type `Result` will represent either a failure (which carries a list of errors) or a success, where the computation has returned a value within the type `T`.

So we can define the `add` and the `toString` functions as a `Composable`:
We can create a `Composable` by wrapping a function with the `composable` method:

```typescript
import { composable } from 'composable-functions'

const add = composable((a: number, b: number) => a + b)
// ^? Composable<(a: number, b: number) => number>
```

Or we can use combinators that evaluate to both plain functions and `Composable` into another `Composable`:

```typescript
import { composable, pipe } from 'composable-functions'

const add = composable((a: number, b: number) => a + b)
// ^? Composable<(a: number, b: number) => number>
const toString = (a: unknown) => `${a}`

const toString = composable((a: unknown) => `${a}`)
// ^? Composable<(a: unknown) => string>
const addAndReturnString = pipe(add, toString)
// ^? Composable<(a: number, b: number) => string>
```

## Sequential composition
Now we can compose them using pipe to create `addAndReturnString`:
We can compose the functions above using pipe to create `addAndReturnString`:

```typescript
import { pipe } from 'composable-functions'
Expand All @@ -150,15 +160,16 @@ const addAndReturnString = pipe(toString, add)
Since pipe will compose from left to right, the only `string` output from `toString` will not fit into the first argument of `add` which is a `number`.
The error message comes in the form of an inferred `FailToCompose` type. This failure type is not callable, therefore it will break any attempts to call `addAndReturnString`.

### Using non-composables (mapping)
### Transforming the output (mapping)

Sometimes we want to use a simple function in this sort of sequential composition. Imagine that `toString` is not a composable, and you just want to apply a plain old function to the result of `add` when it succeeds.
The function `map` can be used for this, since we are mapping over the result of a `Composable`:
Sometimes we want to use a simple function to transform the output of another function. Imagine you want to apply a plain old function to the result of `add` when it succeeds.
The function `map` can be used for this:

```typescript
import { map } from 'composable-functions'

const addAndReturnString = map(add, result => `${result}`)
// ^? Composable<(a: number, b: number) => string>
```

## Parallel composition
Expand All @@ -167,16 +178,27 @@ There are also compositions where all functions are excuted in parallel, like `P
The `all` function is one way of composing in this fashion. Assuming we want to apply our `add` and multiply the two numbers returning a success only once both operations succeed:

```typescript
import { composable, all } from 'composable-functions'
import { all } from 'composable-functions'

const add = composable((a: number, b: number) => a + b)
const mul = composable((a: number, b: number) => a * b)
const add = (a: number, b: number) => a + b
const mul = (a: number, b: number) => a * b
const addAndMul = all(add, mul)
// ^? Composable<(a: number, b: number) => [number, number]>
```
The result of the composition comes in a tuple in the same order as the functions were passed to `all`.
Note that the input functions will also have to type-check and all the functions have to work from the same input.

If you want to work with records instead of tuples, you can use the `collect` function:

```typescript
import { collect } from 'composable-functions'

const add = (a: number, b: number) => a + b
const mul = (a: number, b: number) => a * b
const addAndMul = collect({ add, mul })
// ^? Composable<(a: number, b: number) => { add: number, mul: number }>
```

## Handling errors
Since a `Composable` always return a type `Result<T>` that might be either a failure or a success, there are never exceptions to catch. Any exception inside a `Composable` will return as an object with the shape: `{ success: false, errors: Error[] }`.

Expand Down Expand Up @@ -211,7 +233,7 @@ See [the errors module](./src/errors.ts) for more details.
You can catch an error in a `Composable` using `catchFailure`, which is similar to `map` but will run whenever the first composable fails:

```typescript
import { composable, catchFailure } from 'composable-functions'
import { catchFailure } from 'composable-functions'

// assuming we have the definitions from the previous example
const getOptionalUser = catchFailure(getUser, (errors, id) => {
Expand Down
12 changes: 6 additions & 6 deletions context.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ To avoid such awkwardness we use context:

```tsx
import { context } from 'composable-functions'
const dangerousFunction = composable(async (input: string, { user } : { user: { name: string, admin: boolean } }) => {
const dangerousFunction = async (input: string, { user } : { user: { name: string, admin: boolean } }) => {
// do something that only the admin can do
})
}

const carryUser = context.pipe(gatherInput, dangerousFunction)
```
Expand All @@ -32,8 +32,8 @@ The context.pipe function allows you to compose multiple functions in a sequence
```ts
import { context } from 'composable-functions'

const a = composable((str: string, ctx: { user: User }) => str === '1')
const b = composable((bool: boolean, ctx: { user: User }) => bool && ctx.user.admin)
const a = (str: string, ctx: { user: User }) => str === '1'
const b = (bool: boolean, ctx: { user: User }) => bool && ctx.user.admin

const pipeline = context.pipe(a, b)

Expand All @@ -53,8 +53,8 @@ The context.sequence function works similarly to pipe, but it returns a tuple co
```ts
import { context } from 'composable-functions'

const a = composable((str: string, ctx: { user: User }) => str === '1')
const b = composable((bool: boolean, ctx: { user: User }) => bool && ctx.user.admin)
const a = (str: string, ctx: { user: User }) => str === '1'
const b = (bool: boolean, ctx: { user: User }) => bool && ctx.user.admin

const sequence = context.sequence(a, b)

Expand Down
6 changes: 3 additions & 3 deletions examples/arktype/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"dev": "tsx src/usage.ts"
},
"devDependencies": {
"tsx": "^4.7.2"
"tsx": "^4.15.7"
},
"dependencies": {
"arktype": "2.0.0-dev.22",
"arktype": "2.0.0-dev.26",
"composable-functions": "file:../../npm",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
}
}
Loading

0 comments on commit c63a18d

Please sign in to comment.