(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'>('company')
const address = Lens.fromProp<Company, 'address'>('address')
const street = Lens.fromProp<Address, 'street'>('street')
const name = Lens.fromProp<Street, 'name'>('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.
class Iso<S, A> {
constructor(readonly get: (s: S) => A, readonly reverseGet: (a: A) => S)
};(s: S) => AAlias of get
;(s: S) => AAlias of get
;(a: A) => SAlias of reverseGet
;(a: A) => SAlias of reverseGet
(): Iso<A, S>reverse the Iso: the source becomes the target and the target becomes the source
(f: (a: A) => A): (s: S) => S(): Lens<S, A>view an Iso as a Lens
(): Prism<S, A>view an Iso as a Prism
(): Optional<S, A>view an Iso as a Optional
(): Traversal<S, A>view an Iso as a Traversal
(): Fold<S, A>view an Iso as a Fold
(): Getter<S, A>view an Iso as a Getter
(): Setter<S, A>view an Iso as a Setter
<B>(ab: Iso<A, B>): Iso<S, B>compose an Iso with an Iso
<B>(ab: Lens<A, B>): Lens<S, B>compose an Iso with a Lens
<B>(ab: Prism<A, B>): Prism<S, B>compose an Iso with a Prism
<B>(ab: Optional<A, B>): Optional<S, B>compose an Iso with an Optional
<B>(ab: Traversal<A, B>): Traversal<S, B>compose an Iso with a Traversal
<B>(ab: Fold<A, B>): Fold<S, B>compose an Iso with a Fold
<B>(ab: Getter<A, B>): Getter<S, B>compose an Iso with a Getter
<B>(ab: Setter<A, B>): Setter<S, B>compose an Iso with a Setter
class Lens<S, A> {
constructor(readonly get: (s: S) => A, readonly set: (a: A) => (s: S) => S)
}// other 9 overloadings
<T, K1 extends keyof T>(path: [K1]): Lens<T, T[K1]>Example
type Person = {
name: string
age: number
address: {
city: string
}
}
const city = Lens.fromPath<Person, 'address', 'city'>(['address', 'city'])
const person: Person = { name: 'Giulio', age: 43, address: { city: 'Milan' } }
console.log(city.get(person)) // Milan
console.log(city.set('London')(person)) // { name: 'Giulio', age: 43, address: { city: 'London' } }<T, P extends keyof T>(prop: P): Lens<T, T[P]>generate a lens from a type and a prop
Example
type Person = {
name: string
age: number
}
const age = Lens.fromProp<Person, 'age'>('age')
const person: Person = { name: 'Giulio', age: 43 }
console.log(age.get(person)) // 43
console.log(age.set(44)(person)) // { name: 'Giulio', age: 44 }<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)) // { name: 'Giulio', age: 44 }
console.log(lens.set({ name: 'Guido', age: 47 })(person)) // { name: 'Guido', age: 47, rememberMe: true }<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, 'inner'>('inner', { value: 0, foo: 'foo' })
const value = Lens.fromProp<Inner, 'value'>('value')
const lens = inner.compose(value)
console.log(lens.set(1)({})) // { inner: { value: 1, foo: 'foo' } }
console.log(lens.get({})) // 0
console.log(lens.set(1)({ inner: { value: 1, foo: 'bar' } })) // { inner: { value: 1, foo: 'bar' } }
console.log(lens.get({ inner: { value: 1, foo: 'bar' } })) // 1(f: (a: A) => A): (s: S) => S(): Optional<S, A>view a Lens as a Optional
(): Traversal<S, A>view a Lens as a Traversal
(): Setter<S, A>view a Lens as a Setter
(): Getter<S, A>view a Lens as a Getter
(): Fold<S, A>view a Lens as a Fold
<B>(ab: Lens<A, B>): Lens<S, B>compose a Lens with a Lens
<B>(ab: Getter<A, B>): Getter<S, B>compose a Lens with a Getter
<B>(ab: Fold<A, B>): Fold<S, B>compose a Lens with a Fold
<B>(ab: Optional<A, B>): Optional<S, B>compose a Lens with an Optional
<B>(ab: Traversal<A, B>): Traversal<S, B>compose a Lens with an Traversal
<B>(ab: Setter<A, B>): Setter<S, B>compose a Lens with an Setter
<B>(ab: Iso<A, B>): Lens<S, B>compose a Lens with an Iso
<B>(ab: Prism<A, B>): Optional<S, B>compose a Lens with a Prism
class Prism<S, A> {
constructor(readonly getOption: (s: S) => Option<A>, readonly reverseGet: (a: A) => S)
}<A>(predicate: Predicate<A>): Prism<A, A><A>(): Prism<Option<A>, A>(f: (a: A) => A): (s: S) => S(f: (a: A) => A): (s: S) => Option<S>(a: A): (s: S) => Sset the target of a Prism with a value
(): Optional<S, A>view a Prism as a Optional
(): Traversal<S, A>view a Prism as a Traversal
(): Setter<S, A>view a Prism as a Setter
(): Fold<S, A>view a Prism as a Fold
<B>(ab: Prism<A, B>): Prism<S, B>compose a Prism with a Prism
<B>(ab: Optional<A, B>): Optional<S, B>compose a Prism with a Optional
<B>(ab: Traversal<A, B>): Traversal<S, B>compose a Prism with a Traversal
<B>(ab: Fold<A, B>): Fold<S, B>compose a Prism with a Fold
<B>(ab: Setter<A, B>): Setter<S, B>compose a Prism with a Setter
<B>(ab: Iso<A, B>): Prism<S, B>compose a Prism with a Iso
<B>(ab: Lens<A, B>): Optional<S, B>compose a Prism with a Lens
<B>(ab: Getter<A, B>): Fold<S, B>compose a Prism with a Getter
class Optional<S, A> {
constructor(readonly getOption: (s: S) => Option<A>, readonly set: (a: A) => (s: S) => S) {}
}<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, 'info'>('info')
const employment = Optional.fromNullableProp<Info, Employment, 'employment'>('employment')
const phone = Optional.fromNullableProp<Employment, Phone, 'phone'>('phone')
const number = Lens.fromProp<Phone, 'number'>('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) // some('555-1234')
numberFromResponse.getOption(response2) // none(f: (a: A) => A): (s: S) => S(f: (a: A) => A): (s: S) => Option<S>(): Traversal<S, A>view a Optional as a Traversal
(): Fold<S, A>view an Optional as a Fold
(): Setter<S, A>view an Optional as a Setter
<B>(ab: Optional<A, B>): Optional<S, B>compose a Optional with a Optional
<B>(ab: Traversal<A, B>): Traversal<S, B>compose an Optional with a Traversal
<B>(ab: Fold<A, B>): Fold<S, B>compose an Optional with a Fold
<B>(ab: Setter<A, B>): Setter<S, B>compose an Optional with a Setter
<B>(ab: Lens<A, B>): Optional<S, B>compose an Optional with a Lens
<B>(ab: Prism<A, B>): Optional<S, B>compose an Optional with a Prism
<B>(ab: Iso<A, B>): Optional<S, B>compose an Optional with a Iso
<B>(ab: Getter<A, B>): Fold<S, B>compose an Optional with a Getter
class Traversal<S, A> {
constructor(readonly modifyF: <F>(F: Applicative<F>) => (f: (a: A) => HKT<F, A>) => (s: S) => HKT<F, S>)
}(f: (a: A) => A): (s: S) => S(a: A): (s: S) => S(): Fold<S, A>view a Traversal as a Fold
(): Setter<S, A>view a Traversal as a Setter
<B>(ab: Traversal<A, B>): Traversal<S, B>compose a Traversal with a Traversal
<B>(ab: Fold<A, B>): Fold<S, B>compose a Traversal with a Fold
<B>(ab: Setter<A, B>): Setter<S, B>compose a Traversal with a Setter
<B>(ab: Optional<A, B>): Traversal<S, B>compose a Traversal with a Optional
<B>(ab: Lens<A, B>): Traversal<S, B>compose a Traversal with a Lens
<B>(ab: Prism<A, B>): Traversal<S, B>compose a Traversal with a Prism
<B>(ab: Iso<A, B>): Traversal<S, B>compose a Traversal with a Iso
<B>(ab: Getter<A, B>): Fold<S, B>compose a Traversal with a Getter
class Getter<S, A> {
constructor(readonly get: (s: S) => A)
}(): Fold<S, A>view a Getter as a Fold
<B>(ab: Getter<A, B>): Getter<S, B>compose a Getter with a Getter
<B>(ab: Fold<A, B>): Fold<S, B>compose a Getter with a Fold
<B>(ab: Lens<A, B>): Getter<S, B>compose a Getter with a Lens
<B>(ab: Iso<A, B>): Getter<S, B>compose a Getter with a Iso
<B>(ab: Traversal<A, B>): Fold<S, B>compose a Getter with a Optional
<B>(ab: Optional<A, B>): Fold<S, B>compose a Getter with a Optional
<B>(ab: Prism<A, B>): Fold<S, B>compose a Getter with a Prism
class Fold<S, A> {
constructor(readonly foldMap: <M>(M: Monoid<M>) => (f: (a: A) => M) => (s: S) => M)
}<B>(ab: Fold<A, B>): Fold<S, B>compose a Fold with a Fold
<B>(ab: Getter<A, B>): Fold<S, B>compose a Fold with a Getter
<B>(ab: Traversal<A, B>): Fold<S, B>compose a Fold with a Traversal
<B>(ab: Optional<A, B>): Fold<S, B>compose a Fold with a Optional
<B>(ab: Lens<A, B>): Fold<S, B>compose a Fold with a Lens
<B>(ab: Prism<A, B>): Fold<S, B>compose a Fold with a Prism
<B>(ab: Iso<A, B>): Fold<S, B>compose a Fold with a Iso
(p: Predicate<A>): (s: S) => Option<A>find the first target of a Fold matching the predicate
(s: S): Option<A>get the first target of a Fold
(s: S): Array<A>get all the targets of a Fold
(p: Predicate<A>): Predicate<S>check if at least one target satisfies the predicate
(p: Predicate<A>): Predicate<S>check if all targets satisfy the predicate
class Setter<S, A> {
constructor(readonly modify: (f: (a: A) => A) => (s: S) => S)
}(a: A): (s: S) => S<B>(ab: Setter<A, B>): Setter<S, B>compose a Setter with a Setter
<B>(ab: Traversal<A, B>): Setter<S, B>compose a Setter with a Traversal
<B>(ab: Optional<A, B>): Setter<S, B>compose a Setter with a Optional
<B>(ab: Lens<A, B>): Setter<S, B>compose a Setter with a Lens
<B>(ab: Prism<A, B>): Setter<S, B>compose a Setter with a Prism
<B>(ab: Iso<A, B>): Setter<S, B>compose a Setter with a Iso
<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'>('tweets')
const tweetTextLens = Lens.fromProp<Tweet, 'text'>('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)
// { tweets: [ { text: 'dlrow olleh' }, { text: 'raboof' } ] }<F>(F: Foldable<F>): <A>() => Fold<HKT<F, A>, A>create a Fold from a Foldable
lift an instance of At using an Iso
<T>(iso: Iso<T, S>): At<T, I, A><A = never>(setoid: Setoid<A>): At<Set<A>, A, boolean><A = never>(): At<SM.StrMap<A>, string, Option<A>><T, J, B>(at: At<T, J, Option<B>>): Index<T, J, B>lift an instance of Index using an Iso
<T>(iso: Iso<T, S>): Index<T, I, A><A = never>(): Index<Array<A>, number, A><A = never>(): Index<StrMap<A>, string, A>