Skip to content

Commit

Permalink
refactor(prism): rename "has" to "where"
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Rename "has" to "where"
  • Loading branch information
Flavio Corpa committed Sep 30, 2020
1 parent d703c4f commit 48e1b48
Show file tree
Hide file tree
Showing 24 changed files with 158 additions and 145 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100
"printWidth": 100,
"arrowParens": "avoid"
}
56 changes: 29 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,27 @@ If you want to know more about the implementation, you can check this talk by [m
## Meet it!

```js
import { has, maybe, optic, values } from 'optics.js'
import { where, maybe, optic, values } from 'optics.js'

const people = [
{
name: { first: 'Alejandro', last: 'Serrano' },
birthmonth: 'april', age: 32,
birthmonth: 'april',
age: 32,
},
{
name: { first: 'Flavio', last: 'Corpa' },
birthmonth: 'april', age: 29
birthmonth: 'april',
age: 29,
},
{ name: { first: 'Laura' }, birthmonth: 'august', age: 27 },
]

const firstNameTraversal = optic(values, 'name', 'first').toArray(people)
const lastNameOptional = optic(values, 'name', maybe('last')).toArray(people)
const o = optic(values, has({ birthmonth: 'april' }), 'age')
const o = optic(values, where({ birthmonth: 'april' }), 'age')
const ageTraversal = o.toArray(people)
const agePlus1Traversal = o.over((x) => x + 1, people)
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.
Expand All @@ -82,7 +84,7 @@ Intuitively, optics simply point to one (or more) positions within your data. Yo
const shoppingList = { pie: 3, milk: { whole: 6, skimmed: 3 } }

view(wholeMilk, shoppingList) // > 6
over(wholeMilk, (x) => x + 1, shoppingList)
over(wholeMilk, x => x + 1, shoppingList)
// > { pie: 3, milk: { whole: 7, skimmed: 3 } }
```

Expand Down Expand Up @@ -214,8 +216,8 @@ Given a `key`, its `view` operation returns:
The `set`/`over` operation can be used to modify, create, and remove keys in an object.

```js
set(optic('name'), 'Alex', { name: 'Flavio' }) // { name: 'Alex' }
set(optic('name'), 'Alex', { } ) // { name: 'Alex' }
set(optic('name'), 'Alex', { name: 'Flavio' }) // { name: 'Alex' }
set(optic('name'), 'Alex', {}) // { name: 'Alex' }
set(optic('name'), notFound, { name: 'Flavio' }) // { }
```

Expand All @@ -239,10 +241,10 @@ In a similar fashion to `alter`, the `view` operation returns:
However, `maybe` does _not_ create or remove keys from an object. The most common use is to modify only values which are already there.

```js
over(maybe('age'), (x) => x + 1, { name: 'Alex', age: 32 })
// { name: 'Alex', age: 33 }
over(maybe('age'), (x) => x + 1, { name: 'Flavio' })
// { name: 'Flavio' }
over(maybe('age'), x => x + 1, { name: 'Alex', age: 32 })
// { name: 'Alex', age: 33 }
over(maybe('age'), x => x + 1, { name: 'Flavio' })
// { name: 'Flavio' }
```

#### `never : Optional s a`
Expand All @@ -251,39 +253,39 @@ This optional _never_ matches: `view`ing through it always returns `notFound`, u

### Prisms (preview, set, review)

#### `has : { ...obj } -> Prism { ...obj, ...rest } { ...obj }`
#### `where : { ...obj } -> Prism { ...obj, ...rest } { ...obj }`

This prism targets only objects which contain a given "subobject". This might be seen more clearly with a few examples:

```js
preview(optic(has({ id: 1 }), 'name'), { id: 1, name: 'Alex' }) // 'Alex'
preview(optic(has({ id: 1 }), 'name'), { id: 2, name: 'Alex' }) // notFound
preview(optic(where({ id: 1 }), 'name'), { id: 1, name: 'Alex' }) // 'Alex'
preview(optic(where({ id: 1 }), 'name'), { id: 2, name: 'Alex' }) // notFound
```

This prism is quite useful when dealing with [discriminating unions](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions), like those usually found in [Redux actions](https://redux.js.org/basics/actions):

```js
optic(has({ type: 'ADD_ITEM' }), ...)
optic(where({ type: 'ADD_ITEM' }), ...)
```

Since it is a prism, `has` may also be used for constructing objects. In that case, it ensures that the subobject is part of the created:
Since it is a prism, `where` may also be used for constructing objects. In that case, it ensures that the subobject is part of the created:

```js
has({ type: 'ADD_ITEM' }).review({ item: 'Hello' })
// { type: 'ADD_ITEM', item: 'Hello' }
where({ type: 'ADD_ITEM' }).review({ item: 'Hello' })
// { type: 'ADD_ITEM', item: 'Hello' }
```

When combined with traversals like `values`, `has` can be used to filter out values.
When combined with traversals like `values`, `where` can be used to filter out values.

```js
// return only people who were born in April
toArray(optic(values, has({ birthmonth: 'april' })), people)
toArray(optic(values, where({ birthmonth: 'april' })), people)
```

Note that when matching the optic itself returns the _whole_ object again:

```js
preview(has({ id: 1 })), { id: 1, name: 'Alex' }) // { id: 1, name: 'Alex' }
preview(where({ id: 1 })), { id: 1, name: 'Alex' }) // { id: 1, name: 'Alex' }
```

### Traversals (toArray, set)
Expand All @@ -293,7 +295,7 @@ preview(has({ id: 1 })), { id: 1, name: 'Alex' }) // { id: 1, name: 'Alex' }
Targets every position in an array.

```js
over(values, (x) => x + 1, [1, 2, 3]) // [2, 3, 4]
over(values, x => x + 1, [1, 2, 3]) // [2, 3, 4]
```

#### `entries : Traversal Object [k, v]`
Expand All @@ -302,14 +304,14 @@ Targets every key-value pair in an object. As with the identically-named `entrie

```js
toArray(entries, { name: 'Alex', age: 32 })
// [ ['name', 'Alex'], ['age', 32] ]
// [ ['name', 'Alex'], ['age', 32] ]
```

When using `set`/`over` with `entries`, you _must_ always return the _same key_ that was given to you, or `BadThingWillHappen`.

```js
over(entries, ([k, v]) => [k, v + 1], numbers) // right
over(entries, ([k, v]) => v + 1, numbers) // throws TypeError
over(entries, ([k, v]) => [k, v + 1], numbers) // right
over(entries, ([k, v]) => v + 1, numbers) // throws TypeError
```

### Getters (view)
Expand All @@ -331,7 +333,7 @@ This optic matches may only focus on values with a _single_ key. Note that this
Since `Iso`s give you the `review`, you can use `single` to build objects too:

```js
review(single('name'), 'Flavio') // { name: 'Flavio' }
review(single('name'), 'Flavio') // { name: 'Flavio' }
```

## License
Expand Down
2 changes: 1 addition & 1 deletion __tests__/Fold.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const doubleArray = [
[3, 4],
]

const valuesFold = foldFromToArray((obj) => [...obj])
const valuesFold = foldFromToArray(obj => [...obj])
const valuesReduceFold = foldFromReduce((f, i, obj) => obj.reduce(f, i))

describe('Traversal', () => {
Expand Down
10 changes: 5 additions & 5 deletions __tests__/Iso.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
view,
} from '../src/operations'

const id = (x) => x
const id = x => x
const idIso = iso(id, id)
const optics = [
'Lens',
Expand All @@ -35,7 +35,7 @@ describe('Iso', () => {
expect(idIso.asIso).toBe(idIso)
})

optics.forEach((optic) => {
optics.forEach(optic => {
test('conversion to ' + optic, () => {
const nm = `as${optic}`
const o = idIso[nm]
Expand All @@ -62,7 +62,7 @@ describe('Iso', () => {
})
})

getterOptics.forEach((gopt) => {
getterOptics.forEach(gopt => {
test('getter as ' + gopt, () => {
const nm = `as${gopt}`
const o = optic(idIso[nm], single('name'))
Expand All @@ -86,7 +86,7 @@ describe('Iso', () => {
})
})

setterOptics.forEach((sopt) => {
setterOptics.forEach(sopt => {
test('setter as ' + sopt, () => {
const nm = `as${sopt}`
const o = optic(idIso[nm], single('name'))
Expand All @@ -96,7 +96,7 @@ describe('Iso', () => {
})
})

reviewOptics.forEach((ropt) => {
reviewOptics.forEach(ropt => {
test('review as ' + ropt, () => {
const nm = `as${ropt}`
const o = optic(idIso[nm], single('name'))
Expand Down
4 changes: 2 additions & 2 deletions __tests__/Optional.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('Optional', () => {
expect(over(firstOf('name', 'toli'), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf('toli', 'name'), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf('name', 'id'), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf('id', 'name'), (x) => x + 1, user)).toStrictEqual({ id: 2, name: 'Flavio' })
expect(over(firstOf('id', 'name'), x => x + 1, user)).toStrictEqual({ id: 2, name: 'Flavio' })
expect(over(firstOf('toli', 'moli'), toUpper, user)).toStrictEqual(user)
expect(firstOf('toli', 'moli').set('chorizo', user)).toStrictEqual(user)
})
Expand All @@ -138,7 +138,7 @@ describe('Optional', () => {
expect(over(firstOf(nameL, toliL), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf(toliL, nameL), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf(nameL, idL), toUpper, user)).toStrictEqual({ id: 1, name: 'FLAVIO' })
expect(over(firstOf(idL, nameL), (x) => x + 1, user)).toStrictEqual({ id: 2, name: 'Flavio' })
expect(over(firstOf(idL, nameL), x => x + 1, user)).toStrictEqual({ id: 2, name: 'Flavio' })
expect(over(firstOf(toliL, moliL), toUpper, user)).toStrictEqual(user)
})

Expand Down
76 changes: 39 additions & 37 deletions __tests__/Prism.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,68 @@ import { OpticComposeError, UnavailableOpticOperationError } from '../src/errors
import { toUpper } from '../src/functions'
import { notFound } from '../src/notFound'
import { matches, optic, over, preview, review, sequence, toArray } from '../src/operations'
import { has } from '../src/Prism'
import { where } from '../src/Prism'

const user = { id: 1, name: 'Flavio' }
const modifiedUser = { id: 1, name: 'Alejandro' }
const modifyUser = (u) => ({ ...u, name: 'Alejandro' })
const modifyUser = u => ({ ...u, name: 'Alejandro' })

describe('Prism', () => {
test('has returns itself if ok', () => {
expect(preview(has({ id: 1 }), user)).toEqual(user)
expect(matches(has({ id: 1 }), user)).toBe(true)
expect(() => matches(has({ id: 1 }).asReviewer, user)).toThrow(UnavailableOpticOperationError)
expect(() => sequence(has({ id: 1 }).asReviewer)).toThrow(OpticComposeError)
test('where returns itself if ok', () => {
expect(preview(where({ id: 1 }), user)).toEqual(user)
expect(matches(where({ id: 1 }), user)).toBe(true)
expect(() => matches(where({ id: 1 }).asReviewer, user)).toThrow(UnavailableOpticOperationError)
expect(() => sequence(where({ id: 1 }).asReviewer)).toThrow(OpticComposeError)
})

test('has returns nothing if not found', () => {
expect(preview(has({ id: 2 }), user)).toEqual(notFound)
expect(matches(has({ id: 2 }), user)).toBe(false)
test('where returns nothing if not found', () => {
expect(preview(where({ id: 2 }), user)).toEqual(notFound)
expect(matches(where({ id: 2 }), user)).toBe(false)
})

test('has works correctly when setting', () => {
expect(over(has({ id: 1 }), modifyUser, user)).toStrictEqual(modifiedUser)
expect(has({ id: 1 }).over(modifyUser, user)).toStrictEqual(modifiedUser)
test('where works correctly when setting', () => {
expect(over(where({ id: 1 }), modifyUser, user)).toStrictEqual(modifiedUser)
expect(where({ id: 1 }).over(modifyUser, user)).toStrictEqual(modifiedUser)
})

test('has sets nothing if not found', () => {
expect(over(has({ id: 2 }), modifyUser, user)).toStrictEqual(user)
expect(has({ id: 2 }).over(modifyUser, user)).toStrictEqual(user)
test('where sets nothing if not found', () => {
expect(over(where({ id: 2 }), modifyUser, user)).toStrictEqual(user)
expect(where({ id: 2 }).over(modifyUser, user)).toStrictEqual(user)
})

test('has works in review', () => {
expect(review(has({ id: 1 }), { name: 'Flavio' })).toStrictEqual(user)
expect(has({ id: 1 }).review({ name: 'Flavio' })).toStrictEqual(user)
test('where works in review', () => {
expect(review(where({ id: 1 }), { name: 'Flavio' })).toStrictEqual(user)
expect(where({ id: 1 }).review({ name: 'Flavio' })).toStrictEqual(user)
})

test('has works correctly in composition with itself', () => {
expect(optic(has({ id: 1 }), has({ name: 'Flavio' })).preview(user)).toStrictEqual(user)
expect(optic(has({ id: 1 }), has({ name: 'Flavio' })).preview({ id: 2, name: 'Flavio' })).toBe(
test('where works correctly in composition with itself', () => {
expect(optic(where({ id: 1 }), where({ name: 'Flavio' })).preview(user)).toStrictEqual(user)
expect(
optic(where({ id: 1 }), where({ name: 'Flavio' })).preview({ id: 2, name: 'Flavio' }),
).toBe(notFound)
expect(optic(where({ id: 1 }), where({ name: 'Flavio' })).preview({ name: 'Flavio' })).toBe(
notFound,
)
expect(optic(has({ id: 1 }), has({ name: 'Flavio' })).preview({ name: 'Flavio' })).toBe(
notFound,
)
expect(optic(has({ id: 1 }), has({ name: 'Flavio' })).set(0, { name: 'Alex' })).toStrictEqual({
expect(
optic(where({ id: 1 }), where({ name: 'Flavio' })).set(0, { name: 'Alex' }),
).toStrictEqual({
name: 'Alex',
})
})

test('has works correctly in composition with lens', () => {
expect(over(optic(has({ id: 1 }), 'name'), toUpper, user)).toStrictEqual({
test('where works correctly in composition with lens', () => {
expect(over(optic(where({ id: 1 }), 'name'), toUpper, user)).toStrictEqual({
id: 1,
name: 'FLAVIO',
})
expect(optic(has({ id: 1 }), 'name').over(toUpper, user)).toStrictEqual({
expect(optic(where({ id: 1 }), 'name').over(toUpper, user)).toStrictEqual({
id: 1,
name: 'FLAVIO',
})
})

test('has works correctly in composition', () => {
const o = optic(has({ id: 1 }), has({ name: 'Flavio' }))
test('where works correctly in composition', () => {
const o = optic(where({ id: 1 }), where({ name: 'Flavio' }))
expect(review(o, { age: 30 })).toEqual({
id: 1,
name: 'Flavio',
Expand All @@ -70,14 +72,14 @@ describe('Prism', () => {
})

test('Prism.asTraversal -> works when value is found', () => {
expect(toArray(has({ id: 1 }), user)).toEqual([user])
expect(toArray(has({ id: 1 }).asTraversal, user)).toEqual([user])
expect(toArray(optic(has({ id: 1 }), 'name'), user)).toEqual(['Flavio'])
expect(toArray(where({ id: 1 }), user)).toEqual([user])
expect(toArray(where({ id: 1 }).asTraversal, user)).toEqual([user])
expect(toArray(optic(where({ id: 1 }), 'name'), user)).toEqual(['Flavio'])
})

test('Prism.asTraversal -> works when value is not found', () => {
expect(toArray(has({ id: 2 }), user)).toEqual([])
expect(toArray(has({ id: 2 }).asTraversal, user)).toEqual([])
expect(toArray(optic(has({ id: 2 }), 'name'), user)).toEqual([])
expect(toArray(where({ id: 2 }), user)).toEqual([])
expect(toArray(where({ id: 2 }).asTraversal, user)).toEqual([])
expect(toArray(optic(where({ id: 2 }), 'name'), user)).toEqual([])
})
})
6 changes: 3 additions & 3 deletions __tests__/Traversal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Traversal', () => {
})

test('traversal from array updates', () => {
expect(over(values, (x) => x + 1, [1, 2])).toStrictEqual([2, 3])
expect(over(values, x => x + 1, [1, 2])).toStrictEqual([2, 3])
})

test('traversal from double array reduces', () => {
Expand All @@ -36,7 +36,7 @@ describe('Traversal', () => {

test('traversal from double array updates', () => {
const o = optic(values, values)
expect(over(o, (x) => x + 1, doubleArray)).toStrictEqual([
expect(over(o, x => x + 1, doubleArray)).toStrictEqual([
[2, 3],
[4, 5],
])
Expand All @@ -47,7 +47,7 @@ describe('Traversal', () => {
})

test('traversal from double reduce updates', () => {
expect(over(valuesReduceTraversal, (x) => x + 1, [1, 2])).toStrictEqual([2, 3])
expect(over(valuesReduceTraversal, x => x + 1, [1, 2])).toStrictEqual([2, 3])
})

test('traversal from entries reduces', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/functions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('Function Operators', () => {
})

test('compose -> should compose N functions correctly', () => {
const inc = (x) => x + 1
const inc = x => x + 1

expect(compose(inc, exp(5))(1)).toBe(inc(exp(5, 1)))
})
Expand Down

0 comments on commit 48e1b48

Please sign in to comment.