Skip to content

Commit

Permalink
feat(operations): collect and transform (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
serras committed Oct 1, 2020
1 parent 48e1b48 commit b4b6b69
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 3 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const agePlus1Traversal = o.over(x => x + 1, people)

Optics provide a _language_ for data _access_ and _manipulation_ in a concise and compositional way. It excels when you want to code in an _immutable_ way.

[![intro](https://img.youtube.com/vi/tbNi-ykYev8/0.jpg)](https://www.youtube.com/watch?v=tbNi-ykYev8)
[![intro](https://img.youtube.com/vi/pZ-ELcxYwVc/0.jpg)](https://www.youtube.com/watch?v=pZ-ELcxYwVc)

There are very few moving parts in `optics.js`, the power comes from the ability to create long combinations or _paths_ by composing small primitive optics. Let's look at an example:

Expand Down Expand Up @@ -181,6 +181,31 @@ maybe('age').preview(person) || defaultAge

Creates a combined optic by applying each one on the result of the previous one. This is the most common way to combine optics.

#### `collect : { k: Getter s a | PartialGetter s a | Fold s a } -> Getter s { k: v }`

Generates a new object whose keys are based on the given optics. Depending on the type of optic, a single value, optional value, or array is collected.

```js
collect({ edad: optic('age') }).view({ name: 'Alex', age: 32 }) // { edad: 32 }
```

#### `transform : (s -> a) -> Getter s a`

Applies a transformation to the values targetted up to then. Since the transformation may not be reversible, after composing with `transform` you lose the ability to set or modify the value.

```js
transform(x => x + 1).view(2) // 3
```

This can be very useful in combination with `collect`.

```js
optic(
collect({ first: optic('firstName'), last: optic('lastName')}),
transform(x => x.first + ' ' + x.last)
).view({ first: 'Alex', last: 'Smith' }) // 'Alex Smith'
```

#### `sequence : [Fold s a] -> Fold s a`

Joins the result of several optics into a single one. In other words, targets all values from each of the given optics.
Expand Down
22 changes: 21 additions & 1 deletion __tests__/Getter.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { OpticComposeError } from '../src/errors'
import { get } from '../src/functions'
import { always, getter } from '../src/Getter'
import { preview, toArray, view } from '../src/operations'
import { collect, optic, preview, toArray, view } from '../src/operations'
import { reviewer } from '../src/Reviewer'

const obj = {
foo: [1, 2, 3],
Expand All @@ -27,4 +29,22 @@ describe('Getter', () => {
test('always works as expected', () => {
expect(view(always('foo'), obj)).toBe('foo')
})

test('collect works fine with lenses', () => {
const o = collect({ arraycito: optic('foo'), stringcita: optic('bar') })
expect(view(o, obj)).toStrictEqual({ arraycito: [1, 2, 3], stringcita: 'baz' })
})

test('collect works fine over optionals and traversals', () => {
const o = collect({
arraycito: optic('foo').asPartialGetter,
stringcita: optic('bar').asTraversal,
})
expect(view(o, obj)).toStrictEqual({ arraycito: [1, 2, 3], stringcita: ['baz'] })
})

test('collect does not work with reviewers', () => {
const o = collect({ coso: reviewer(x => x + 1) })
expect(() => view(o, obj)).toThrow(OpticComposeError)
})
})
8 changes: 7 additions & 1 deletion __tests__/operations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { OpticComposeError, UnavailableOpticOperationError } from '../src/errors
import { toUpper } from '../src/functions'
import { getter } from '../src/Getter'
import { alter } from '../src/Lens'
import { optic, over, path, set, view } from '../src/operations'
import { collect, optic, over, path, set, view } from '../src/operations'
import { setter } from '../src/Setter'

const theme = {
Expand Down Expand Up @@ -108,4 +108,10 @@ describe('Operations over Optics', () => {
),
).toThrow(UnavailableOpticOperationError)
})

test('collect + transform', () => {
const o = optic(collect({ a: optic('one'), b: optic('two') }), x => x.a + x.b)
const obj = { one: 1, two: 2 }
expect(view(o, obj)).toBe(3)
})
})
22 changes: 22 additions & 0 deletions src/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const toOptic = optic => {
if (typeof optic == 'number' && !isNaN(optic)) {
return ix(optic)
}
if (typeof optic == 'function') {
return transform(optic)
}
// any other case means it was already an optic
return optic
}
Expand Down Expand Up @@ -182,6 +185,25 @@ export const firstOf = (...optics) => {
}
}

export const collect = template =>
getter(obj =>
Object.fromEntries(
Object.entries(template).map(([k, o]) => {
if (o.asGetter) {
return [k, view(o, obj)]
} else if (o.asPartialGetter) {
return [k, preview(o, obj)]
} else if (o.asFold) {
return [k, toArray(o, obj)]
} else {
throw new OpticComposeError('collect', o.constructor.name, 'non-getter optic')
}
}),
),
)

export const transform = getter

// OPERATIONS
// ==========

Expand Down

0 comments on commit b4b6b69

Please sign in to comment.