Motivation
(Adapted from monocle site)
Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.
Let's have a look at some examples:
interface Street {
num: number
name: string
}
interface Address {
city: string
street: Street
}
interface Company {
name: string
address: Address
}
interface Employee {
name: string
company: Company
}
Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we
could write it in vanilla JavaScript
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employee2 = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could
we do with monocle-ts
import { Lens, Optional } from 'monocle-ts'
const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')
company
.compose(address)
.compose(street)
.compose(name)
compose
takes two Lenses
, one from A
to B
and another one from B
to C
and creates a third Lens
from A
to
C
. Therefore, after composing company
, address
, street
and name
, we obtain a Lens
from Employee
to
string
(the street name). Now we can use this Lens
issued from the composition to modify the street name using the
function capitalize
company
.compose(address)
.compose(street)
.compose(name)
.modify(capitalize)(employee)
Here modify
lift a function string => string
to a function Employee => Employee
. It works but it would be clearer
if we could zoom into the first character of a string
with a Lens
. However, we cannot write such a Lens
because
Lenses
require the field they are directed at to be mandatory. In our case the first character of a string
is
optional as a string
can be empty. So we need another abstraction that would be a sort of partial Lens, in
monocle-ts
it is called an Optional
.
import { some, none } from 'fp-ts/lib/Option'
const firstLetter = new Optional<string, string>(s => (s.length > 0 ? some(s[0]) : none), a => s => a + s.substring(1))
company
.compose(address)
.compose(street)
.compose(name)
.asOptional()
.compose(firstLetter)
.modify(s => s.toUpperCase())(employee)
Similarly to compose
for lenses, compose
for optionals takes two Optionals
, one from A
to B
and another from
B
to C
and creates a third Optional
from A
to C
. All Lenses
can be seen as Optionals
where the optional
element to zoom into is always present, hence composing an Optional
and a Lens
always produces an Optional
.
TypeScript compatibility
The stable version is tested against TypeScript 3.1.6, but should run with TypeScript 2.8.0+ too
Note. If you are running < typescript@3.0.1
you have to polyfill unknown
.
You can use unknown-ts as a polyfill.
API
Iso
class Iso<S, A> {
constructor(readonly get: (s: S) => A, readonly reverseGet: (a: A) => S)
}
Methods
unwrap
;(s: S) => A
Alias of get
to
;(s: S) => A
Alias of get
wrap
;(a: A) => S
Alias of reverseGet
from
;(a: A) => S
Alias of reverseGet
reverse
(): Iso<A, S>
reverse the Iso
: the source becomes the target and the target becomes the source
modify
(f: (a: A) => A): (s: S) => S
asLens
(): Lens<S, A>
view an Iso as a Lens
asPrism
(): Prism<S, A>
view an Iso as a Prism
asOptional
(): Optional<S, A>
view an Iso as a Optional
asTraversal
(): Traversal<S, A>
view an Iso as a Traversal
asFold
(): Fold<S, A>
view an Iso as a Fold
asGetter
(): Getter<S, A>
view an Iso as a Getter
asSetter
(): Setter<S, A>
view an Iso as a Setter
compose
<B>(ab: Iso<A, B>): Iso<S, B>
compose an Iso with an Iso
composeIso
Alias of compose
composeLens
<B>(ab: Lens<A, B>): Lens<S, B>
compose an Iso with a Lens
composePrism
<B>(ab: Prism<A, B>): Prism<S, B>
compose an Iso with a Prism
composeOptional
<B>(ab: Optional<A, B>): Optional<S, B>
compose an Iso with an Optional
composeTraversal
<B>(ab: Traversal<A, B>): Traversal<S, B>
compose an Iso with a Traversal
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose an Iso with a Fold
composeGetter
<B>(ab: Getter<A, B>): Getter<S, B>
compose an Iso with a Getter
composeSetter
<B>(ab: Setter<A, B>): Setter<S, B>
compose an Iso with a Setter
Lens
class Lens<S, A> {
constructor(readonly get: (s: S) => A, readonly set: (a: A) => (s: S) => S)
}
fromPath
<S>(): <K1 extends keyof S>(path: [K1]) => Lens<S, S[K1]>
<S, K1 extends keyof S>(path: [K1]): Lens<S, S[K1]>
Example
type Person = {
name: string
age: number
address: {
city: string
}
}
const city = Lens.fromPath<Person>()(['address', 'city'])
const person: Person = { name: 'Giulio', age: 43, address: { city: 'Milan' } }
console.log(city.get(person))
console.log(city.set('London')(person))
fromProp
<S>(): <P extends keyof S>(prop: P) => Lens<S, S[P]>
<S, P extends keyof T>(prop: P): Lens<S, S[P]>
generate a lens from a type and a prop
Example
type Person = {
name: string
age: number
}
const age = Lens.fromProp<Person>()('age')
const person: Person = { name: 'Giulio', age: 43 }
console.log(age.get(person))
console.log(age.set(44)(person))
fromProps
<S>(): <P extends keyof S>(props: Array<P>) => Lens<S, { [K in P]: S[K] }>
generate a lens from a type and an array of props
Example
interface Person {
name: string
age: number
rememberMe: boolean
}
const lens = Lens.fromProps<Person>()(['name', 'age'])
const person: Person = { name: 'Giulio', age: 44, rememberMe: true }
console.log(lens.get(person))
console.log(lens.set({ name: 'Guido', age: 47 })(person))
fromNullableProp
<S>(): <A extends S[K], K extends keyof S>(k: K, defaultValue: A) => Lens<S, A>
<S, A extends S[K], K extends keyof S>(k: K, defaultValue: A): Lens<S, A>
generate a lens from a type and a prop whose type is nullable
Example
interface Outer {
inner?: Inner
}
interface Inner {
value: number
foo: string
}
const inner = Lens.fromNullableProp<Outer>()('inner', { value: 0, foo: 'foo' })
const value = Lens.fromProp<Inner>()('value')
const lens = inner.compose(value)
console.log(lens.set(1)({}))
console.log(lens.get({}))
console.log(lens.set(1)({ inner: { value: 1, foo: 'bar' } }))
console.log(lens.get({ inner: { value: 1, foo: 'bar' } }))
Methods
modify
(f: (a: A) => A): (s: S) => S
asOptional
(): Optional<S, A>
view a Lens as a Optional
asTraversal
(): Traversal<S, A>
view a Lens as a Traversal
asSetter
(): Setter<S, A>
view a Lens as a Setter
asGetter
(): Getter<S, A>
view a Lens as a Getter
asFold
(): Fold<S, A>
view a Lens as a Fold
compose
<B>(ab: Lens<A, B>): Lens<S, B>
compose a Lens with a Lens
composeLens
Alias of compose
composeGetter
<B>(ab: Getter<A, B>): Getter<S, B>
compose a Lens with a Getter
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose a Lens with a Fold
composeOptional
<B>(ab: Optional<A, B>): Optional<S, B>
compose a Lens with an Optional
composeTraversal
<B>(ab: Traversal<A, B>): Traversal<S, B>
compose a Lens with an Traversal
composeSetter
<B>(ab: Setter<A, B>): Setter<S, B>
compose a Lens with an Setter
composeIso
<B>(ab: Iso<A, B>): Lens<S, B>
compose a Lens with an Iso
composePrism
<B>(ab: Prism<A, B>): Optional<S, B>
compose a Lens with a Prism
Prism
class Prism<S, A> {
constructor(readonly getOption: (s: S) => Option<A>, readonly reverseGet: (a: A) => S)
}
fromPredicate
<A>(predicate: Predicate<A>): Prism<A, A>
some
<A>(): Prism<Option<A>, A>
Methods
modify
(f: (a: A) => A): (s: S) => S
modifyOption
(f: (a: A) => A): (s: S) => Option<S>
set
(a: A): (s: S) => S
set the target of a Prism with a value
asOptional
(): Optional<S, A>
view a Prism as a Optional
asTraversal
(): Traversal<S, A>
view a Prism as a Traversal
asSetter
(): Setter<S, A>
view a Prism as a Setter
asFold
(): Fold<S, A>
view a Prism as a Fold
compose
<B>(ab: Prism<A, B>): Prism<S, B>
compose a Prism with a Prism
composePrism
Alias of compose
composeOptional
<B>(ab: Optional<A, B>): Optional<S, B>
compose a Prism with a Optional
composeTraversal
<B>(ab: Traversal<A, B>): Traversal<S, B>
compose a Prism with a Traversal
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose a Prism with a Fold
composeSetter
<B>(ab: Setter<A, B>): Setter<S, B>
compose a Prism with a Setter
composeIso
<B>(ab: Iso<A, B>): Prism<S, B>
compose a Prism with a Iso
composeLens
<B>(ab: Lens<A, B>): Optional<S, B>
compose a Prism with a Lens
composeGetter
<B>(ab: Getter<A, B>): Fold<S, B>
compose a Prism with a Getter
Optional
class Optional<S, A> {
constructor(readonly getOption: (s: S) => Option<A>, readonly set: (a: A) => (s: S) => S) {}
}
fromNullableProp
<S>() <K extends keyof S>(k: K): Optional<S, NonNullable<S[K]>>
<S, A extends S[K], K extends keyof S>(k: K): Optional<S, A>
Example
interface Phone {
number: string
}
interface Employment {
phone?: Phone
}
interface Info {
employment?: Employment
}
interface Response {
info?: Info
}
const info = Optional.fromNullableProp<Response>()('info')
const employment = Optional.fromNullableProp<Info>()('employment')
const phone = Optional.fromNullableProp<Employment>()('phone')
const number = Lens.fromProp<Phone>()('number')
const numberFromResponse = info
.compose(employment)
.compose(phone)
.composeLens(number)
const response1: Response = {
info: {
employment: {
phone: {
number: '555-1234'
}
}
}
}
const response2: Response = {
info: {
employment: {}
}
}
numberFromResponse.getOption(response1)
numberFromResponse.getOption(response2)
fromOptionProp
<S>(): <P extends OptionPropertyNames<S>>(prop: P) => Optional<S, OptionPropertyType<S, P>>
<S>(prop: OptionPropertyNames<S>) => Optional<S, OptionPropertyType<S, typeof prop>>
Example
interface Phone {
number: string
}
interface Employment {
phone: Option<Phone>
}
interface Info {
employment: Option<Employment>
}
interface Response {
info: Option<Info>
}
const info = Optional.fromOptionProp<Response>('info')
const employment = Optional.fromOptionProp<Info>('employment')
const phone = Optional.fromOptionProp<Employment>('phone')
const number = Lens.fromProp<Phone>()('number')
const numberFromResponse = info
.compose(employment)
.compose(phone)
.composeLens(number)
Methods
modify
(f: (a: A) => A): (s: S) => S
modifyOption
(f: (a: A) => A): (s: S) => Option<S>
asTraversal
(): Traversal<S, A>
view a Optional as a Traversal
asFold
(): Fold<S, A>
view an Optional as a Fold
asSetter
(): Setter<S, A>
view an Optional as a Setter
compose
<B>(ab: Optional<A, B>): Optional<S, B>
compose a Optional with a Optional
composeOptional
Alias of compose
composeTraversal
<B>(ab: Traversal<A, B>): Traversal<S, B>
compose an Optional with a Traversal
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose an Optional with a Fold
composeSetter
<B>(ab: Setter<A, B>): Setter<S, B>
compose an Optional with a Setter
composeLens
<B>(ab: Lens<A, B>): Optional<S, B>
compose an Optional with a Lens
composePrism
<B>(ab: Prism<A, B>): Optional<S, B>
compose an Optional with a Prism
composeIso
<B>(ab: Iso<A, B>): Optional<S, B>
compose an Optional with a Iso
composeGetter
<B>(ab: Getter<A, B>): Fold<S, B>
compose an Optional with a Getter
Traversal
class Traversal<S, A> {
constructor(readonly modifyF: <F>(F: Applicative<F>) => (f: (a: A) => HKT<F, A>) => (s: S) => HKT<F, S>)
}
an optic that focuses on multiple elements in a data structure. See fromTraversable
Methods
modify
(f: (a: A) => A): (s: S) => S
modify each element focused by a traversal using the passed function
set
(a: A): (s: S) => S
set the value of each element focused by the traversal
asFold
(): Fold<S, A>
view a Traversal as a Fold
asSetter
(): Setter<S, A>
view a Traversal as a Setter
compose
<B>(ab: Traversal<A, B>): Traversal<S, B>
compose a Traversal with another Traversal
composeTraversal
Alias of compose
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose a Traversal with a Fold
composeSetter
<B>(ab: Setter<A, B>): Setter<S, B>
compose a Traversal with a Setter
composeOptional
<B>(ab: Optional<A, B>): Traversal<S, B>
compose a Traversal with a Optional
composeLens
<B>(ab: Lens<A, B>): Traversal<S, B>
compose a Traversal with a Lens
composePrism
<B>(ab: Prism<A, B>): Traversal<S, B>
compose a Traversal with a Prism
composeIso
<B>(ab: Iso<A, B>): Traversal<S, B>
compose a Traversal with a Iso
composeGetter
<B>(ab: Getter<A, B>): Fold<S, B>
compose a Traversal with a Getter
Getter
class Getter<S, A> {
constructor(readonly get: (s: S) => A)
}
Methods
asFold
(): Fold<S, A>
view a Getter as a Fold
compose
<B>(ab: Getter<A, B>): Getter<S, B>
compose a Getter with a Getter
composeGetter
Alias of compose
composeFold
<B>(ab: Fold<A, B>): Fold<S, B>
compose a Getter with a Fold
composeLens
<B>(ab: Lens<A, B>): Getter<S, B>
compose a Getter with a Lens
composeIso
<B>(ab: Iso<A, B>): Getter<S, B>
compose a Getter with a Iso
composeTraversal
<B>(ab: Traversal<A, B>): Fold<S, B>
compose a Getter with a Optional
composeOptional
<B>(ab: Optional<A, B>): Fold<S, B>
compose a Getter with a Optional
composePrism
<B>(ab: Prism<A, B>): Fold<S, B>
compose a Getter with a Prism
Fold
class Fold<S, A> {
constructor(readonly foldMap: <M>(M: Monoid<M>) => (f: (a: A) => M) => (s: S) => M)
}
Methods
compose
<B>(ab: Fold<A, B>): Fold<S, B>
compose a Fold with a Fold
composeFold
Alias of compose
composeGetter
<B>(ab: Getter<A, B>): Fold<S, B>
compose a Fold with a Getter
composeTraversal
<B>(ab: Traversal<A, B>): Fold<S, B>
compose a Fold with a Traversal
composeOptional
<B>(ab: Optional<A, B>): Fold<S, B>
compose a Fold with a Optional
composeLens
<B>(ab: Lens<A, B>): Fold<S, B>
compose a Fold with a Lens
composePrism
<B>(ab: Prism<A, B>): Fold<S, B>
compose a Fold with a Prism
composeIso
<B>(ab: Iso<A, B>): Fold<S, B>
compose a Fold with a Iso
find
(p: Predicate<A>): (s: S) => Option<A>
find the first target of a Fold matching the predicate
headOption
(s: S): Option<A>
get the first target of a Fold
getAll
(s: S): Array<A>
get all the targets of a Fold
exist
(p: Predicate<A>): Predicate<S>
check if at least one target satisfies the predicate
all
(p: Predicate<A>): Predicate<S>
check if all targets satisfy the predicate
Setter
class Setter<S, A> {
constructor(readonly modify: (f: (a: A) => A) => (s: S) => S)
}
Methods
set
(a: A): (s: S) => S
compose
<B>(ab: Setter<A, B>): Setter<S, B>
compose a Setter with a Setter
composeSetter
Alias of compose
composeTraversal
<B>(ab: Traversal<A, B>): Setter<S, B>
compose a Setter with a Traversal
composeOptional
<B>(ab: Optional<A, B>): Setter<S, B>
compose a Setter with a Optional
composeLens
<B>(ab: Lens<A, B>): Setter<S, B>
compose a Setter with a Lens
composePrism
<B>(ab: Prism<A, B>): Setter<S, B>
compose a Setter with a Prism
composeIso
<B>(ab: Iso<A, B>): Setter<S, B>
compose a Setter with a Iso
fromTraversable
<T>(T: Traversable<T>): <A>() => Traversal<HKT<T, A>, A>
create a Traversal from a Traversable
Example: reversing strings in a nested array
import { Lens, fromTraversable } from 'monocle-ts'
import { array } from 'fp-ts/lib/Array'
interface Tweet {
text: string
}
interface Tweets {
tweets: Tweet[]
}
const tweetsLens = Lens.fromProp<Tweets>()('tweets')
const tweetTextLens = Lens.fromProp<Tweet>()('text')
const tweetTraversal = fromTraversable(array)<Tweet>()
const composedTraversal = tweetsLens.composeTraversal(tweetTraversal).composeLens(tweetTextLens)
const tweet1: Tweet = { text: 'hello world' }
const tweet2: Tweet = { text: 'foobar' }
const model: Tweets = { tweets: [tweet1, tweet2] }
const newModel = composedTraversal.modify(text =>
text
.split('')
.reverse()
.join('')
)(model)
fromFoldable
<F>(F: Foldable<F>): <A>() => Fold<HKT<F, A>, A>
create a Fold from a Foldable
At
Methods
fromIso
lift an instance of At
using an Iso
<T>(iso: Iso<T, S>): At<T, I, A>
Instances
Set
atSet
<A = never>(setoid: Setoid<A>): At<Set<A>, A, boolean>
StrMap
atStrMap
<A = never>(): At<SM.StrMap<A>, string, Option<A>>
Index
fromAt
<T, J, B>(at: At<T, J, Option<B>>): Index<T, J, B>
Methods
fromIso
lift an instance of Index
using an Iso
<T>(iso: Iso<T, S>): Index<T, I, A>
Instances
Array
indexArray
<A = never>(): Index<Array<A>, number, A>
NonEmptyArray
indexNonEmptyArray
<A = never>(): Index<NonEmptyArray<A>, number, A>
StrMap
indexStrMap
<A = never>(): Index<StrMap<A>, string, A>