Giter VIP home page Giter VIP logo

monocle-ts's Introduction

build status dependency status npm downloads

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 employeeCapitalized = {
  ...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 } 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')

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

const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)

You can use the fromPath API to avoid some boilerplate

import { Lens } from 'monocle-ts'

const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])

const capitalizeName = name.modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true

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 { Optional } from 'monocle-ts'
import { some, none } from 'fp-ts/Option'

const firstLetterOptional = new Optional<string, string>(
  (s) => (s.length > 0 ? some(s[0]) : none),
  (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
)

const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)

assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)

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.5.2, but should run with TypeScript 2.8.0+ too

monocle-ts version required typescript version
2.0.x+ 3.5+
1.x+ 2.8.0+

Note. If you are running < [email protected] you have to polyfill unknown.

You can use unknown-ts as a polyfill.

Documentation

Experimental modules (version 2.3+)

Experimental modules (*) are published in order to get early feedback from the community.

The experimental modules are independent and backward-incompatible with stable ones.

(*) A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

From [email protected]+ you can use the following experimental modules:

  • Iso
  • Lens
  • Prism
  • Optional
  • Traversal
  • At
  • Ix

which implement the same features contained in index.ts but are pipe-based instead of class-based.

Here's the same examples with the new API

interface Street {
  num: number
  name: string
}
interface Address {
  city: string
  street: Street
}
interface Company {
  name: string
  address: Address
}
interface Employee {
  name: string
  company: Company
}

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 employeeCapitalized = {
  ...employee,
  company: {
    ...employee.company,
    address: {
      ...employee.company.address,
      street: {
        ...employee.company.address.street,
        name: capitalize(employee.company.address.street.name)
      }
    }
  }
}

import * as assert from 'assert'
import * as L from 'monocle-ts/Lens'
import { pipe } from 'fp-ts/function'

const capitalizeName = pipe(
  L.id<Employee>(),
  L.prop('company'),
  L.prop('address'),
  L.prop('street'),
  L.prop('name'),
  L.modify(capitalize)
)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)

import * as O from 'monocle-ts/Optional'
import { some, none } from 'fp-ts/Option'

const firstLetterOptional: O.Optional<string, string> = {
  getOption: (s) => (s.length > 0 ? some(s[0]) : none),
  set: (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
}

const firstLetter = pipe(
  L.id<Employee>(),
  L.prop('company'),
  L.prop('address'),
  L.prop('street'),
  L.prop('name'),
  L.composeOptional(firstLetterOptional)
)

assert.deepStrictEqual(
  pipe(
    firstLetter,
    O.modify((s) => s.toUpperCase())
  )(employee),
  employeeCapitalized
)

monocle-ts's People

Contributors

barackos avatar bepremeg avatar gcanti avatar graystrider avatar jethrolarson avatar leighman avatar liamgoodacre avatar mikearnaldi avatar oliverjash avatar pashutk avatar sledorze avatar stouffi avatar thewilkybarkid avatar wmaurer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

monocle-ts's Issues

array examples: get array element by predicate returning with index, insert after by predicate, insert after index

Sorry I am new to this library. I searched issue with keyword array and found some good examples of array operation. But I still couldn't figure out three key use cases I need:

  1. Find array element by a property, return the array element with the index.
  2. Insert to array with one element, or all elements of an array at an array element found by a predicate.
  3. Insert to array with one element, or all elements of an array at an index.

If possible, could you share some sample code?

Thanks in advance.

Prism#asOptional produces unlawful instances?

@LiamGoodacre I think that the current implementation of Prism#asOptional returns unlawful Optionals.

I added this test

assert.deepEqual(optional.set(2)(none), some(2))

AFAIK it should fail and optional.set(2)(none) should return none otherwise the second law is violated

Optional Laws:

  1. getOption(s).fold(() => s, a => set(a)(s)) = s
  2. getOption(set(a)(s)) = getOption(s).map(_ => a)
  3. set(a)(set(a)(s)) = set(a)(s)

The culprit seems the implementation of asOptional

return new Optional(this.getOption, a => s => this.reverseGet(a))

I think that should be

asOptional(): Optional<S, A> {
-  return new Optional(this.getOption, a => s => this.reverseGet(a))
+  return new Optional(this.getOption, a => s => this.set(a))
}

asTraversal could suffer of the same issue since the implementation uses reverseGet, not sure though.

What do you think?

Array read / modify performance issue

πŸ› Bug report

I need to pick an optics lib for a project. I continued to evaluate optics libs and narrowed down to 3:

  • optics-ts
  • partial.lenses
  • monocle-ts

I ran another round of performance test on array and found out monocle-ts' array read / modify by prism predicate is slow compared to the others. Modify is especially slow.

Did my benchmark code do something wrong or monocle-ts' array read / modify by Prism predicate needs some optimization?

Here is the result and code:

result

  prism into array
    βœ“ optics-ts (69ms)
    βœ“ monocle-ts (223ms)
    βœ“ partial.lenses (7ms)
  prism modify array
    βœ“ optics-ts (222ms)
    βœ“ monocle-ts (34769ms)
    βœ“ partial.lenses (344ms)

code for Jest

import * as O from 'optics-ts'

import { fromTraversable, Lens, Prism } from 'monocle-ts'
import { some } from 'fp-ts/lib/Option'

import * as L from 'partial.lenses'

import { array } from 'fp-ts/lib/Array'

const size = 5000
const mid = Math.floor(size / 2)
const id = 'id-' + mid
const name = 'Luke-' + mid
const nameModified = 'Luke-' + mid + '-modified'

const makeNames = () => {
  const arr = []
  for (let i = 0; i < size; i++)
    arr.push({
      id: 'id-' + i,
      name: 'Luke-' + i,
    })

  return arr
}

const data = {
  a: {
    b: {
      c: { d: { e: 'hello' } },
    },
  },

  m: {
    n: {
      names: makeNames(),
    },
  },
}

const run = (fn: () => any) => {
  for (let i = 0; i < repeat; i++) fn()
}

const repeat = 1000 //50000

describe('prism into array', () => {
  it('optics-ts', () => {
    const optics = O.optic<any>()
      .path(['m', 'n', 'names'])
      .elems()
      .when((name: any) => name.id === id)
    let r = undefined
    const fn = () => (r = O.preview(optics)(data))
    run(fn)
    expect(r).toEqual({ id, name })
  })

  it('monocle-ts', () => {
    const getChildPrism = (id: string) =>
      Prism.fromPredicate((child: any) => child.id === id)
    const childTraversal = fromTraversable(array)<any>()

    const optics = Lens.fromPath<any>()(['m', 'n', 'names'])
      .composeTraversal(childTraversal)
      .composePrism(getChildPrism(id))
      .asFold()

    let r = undefined
    const fn = () => (r = optics.headOption(data))
    run(fn)
    expect(r).toEqual(some({ id, name }))
  })

  it('partial.lenses', () => {
    const optics = L.compose(
      L.prop('m'),
      L.prop('n'),
      L.prop('names'),
      L.find((name: any) => name.id === id),
      L.valueOr(undefined)
    )
    let r = undefined
    const fn = () => (r = L.get(optics, data))
    run(fn)
    expect(r).toEqual({ id, name })
  })

})

describe('prism modify array', () => {
  it('optics-ts', () => {
    const optics = O.optic<any>()
      .path(['m', 'n', 'names'])
      .elems()
      .when((name: any) => name.id === id)
    let r = undefined
    const fn = () =>
      (r = O.modify(optics)((s: any) => ({ ...s, name: nameModified }))(data))
    run(fn)

    let w = O.preview(optics)(r)
    expect(w).toEqual({ id, name: nameModified })
  })

  it('monocle-ts', () => {
    const getChildPrism = (id: string) =>
      Prism.fromPredicate((child: any) => child.id === id)
    const childTraversal = fromTraversable(array)<any>()

    const optics = Lens.fromPath<any>()(['m', 'n', 'names'])
      .composeTraversal(childTraversal)
      .composePrism(getChildPrism(id))

    let r = undefined
    const fn = () =>
      (r = optics.modify((s) => ({ ...s, name: nameModified }))(data))
    run(fn)
    let w = optics.asFold().headOption(r)

    expect(w).toEqual(some({ id, name: nameModified }))
  })

  it('partial.lenses', () => {
    const optics = L.compose(
      L.prop('m'),
      L.prop('n'),
      L.prop('names'),
      L.find((name: any) => name.id === id)
    )
    let r = undefined
    const fn = () =>
      (r = L.modify(optics, (s: any) => ({ ...s, name: nameModified }), data))
    run(fn)

    let w = L.get(optics, r)
    expect(w).toEqual({ id, name: nameModified })
  })

})

Your environment

Which versions of monocle-ts are affected by this issue? Did this work in previous versions of monocle-ts?

Software Version(s)
monocle-ts 2.1.0
fp-ts 2.5.4
TypeScript 3.8

Improve Lens reusability

πŸš€ Feature request

Current Behavior

Currently the return type of a modification when using a Lens, is the same return type as the type that the Lens was configured for.

Desired Behavior

The return type of a modification should be the same type as the Input Value instead.

Suggested Solution

Adjust the interfaces so that we infer the input value type, make sure that it extends the type of the Lens, and then return the same type as the input value.

Sample adapter I'm using for the modify function atm:

interface CoolLens<TRequired, TValue> extends Lens<TRequired, TValue> {
  modify(f: (a: TValue) => TValue): <TGiven extends TRequired>(s: TGiven) => TGiven
}

export const convertCoolLens = <TRequired, TValue>(l: Lens<TRequired, TValue>) =>
  l as CoolLens<TRequired, TValue>

const startDateL = convertCoolLens(Lens.fromPath<Pick<TrainTrip, "startDate">>()(["startDate"]))

If I put a TrainTrip in, I get a TrainTrip out. If I get something that looks like an object with startDate with the right type, I get that type out too.

Who does this impact? Who is this for?

Myself, and anyone else who wants to re-use Lenses based on (partial) interfaces.

Describe alternatives you've considered

Using custom adapter from my side, but it gets tedious.

How to get property name used in Lens.fromProp

πŸš€ Feature request

Current Behavior

There is no way to get the prop used from Lens.fromProp to my knowledge.

Currently we can only get the value and set the value.

Desired Behavior

Read the prop name from the lens as a property of the lens.

const lens = Lens.fromProp()('age')

assert(lens.prop).toBe('age')

Suggested Solution

Add the prop as a variable within prop.

Who does this impact? Who is this for?

All users. Not a breaking change in any way.

Since we're adding a generic, users who use these generics to construct types may find their code not working.

Generics would be added for Lens and any other type that composes with Lens.

Describe alternatives you've considered

I was looking for a function that might retrieve this. The prop in the src code reveals that it's not saved as a property anywhere. Instead, it is kept in scope via closures.

Additional context

This reveals that this request it out of scope from what Lens libraries usually provide.
https://stackoverflow.com/questions/44454873/how-to-print-a-monocle-lens-as-a-property-accessor-style-string

Thank you for all your libraries! I love FP now <3 Is there a place for donations?

Add alias `composeLens` for `compose` in `Lens` etc

If, for example, the object up my compose chain used to be a Prism I will later be calling it with composeLens. If I add something to provide a default value instead my object is now a Lens and composeLens fails (must be replaced with compose).

It would be nice if composeLens continued working.

Is this something that could be added? Would be happy to do it, if so.

fromOptionProp gives "Argument of type is not assignable to..." error

fromOptionProp throws the "is not assignable" TSError. fromNullableProp is working as expected. v1.7.1

interface A {
    readonly a: Option<number>;
}

const a: Optional<A, number> = Optional.fromOptionProp<A>("a");

interface B {
    readonly b: number | null;
}

const b: Optional<B, number> = Optional.fromNullableProp<B, number, "b">("b");

assigning None through Optional.fromProp

Hi

I'm still getting my head around lenses, optionals lenses etc. I'm using this library in a situation where I want to use a lens to update deeply nested optional property to None. The Optional instance created via fromProp assumes that you always want to set a Some:

https://github.com/gcanti/monocle-ts/blob/master/src/index.ts#L171

  /** generate an optional from a type and a prop which is a `Option` */
  static fromProp<T extends { [K in P]: Option<any> }, P extends keyof T>(prop: P): Optional<T, T[P]['_A']> {
    return new Optional<T, T[P]['_A']>(
      s => s[prop],
      (a, s) => Object.assign({}, s, { [prop as any]: some(a) })
    )
  }

I guess I can write my own fromProp2:

  /** generate an optional from a type and a prop which is a `Option` */
  static fromProp2<T extends { [K in P]: Option<any> }, P extends keyof T>(prop: P): Optional<T, T[P]['_A']> {
    return new Optional<T, T[P]['_A']>(
      s => s[prop],
      (a, s) => Object.assign({}, s, { [prop as any]: fromNullable(a) })
    )
  }

but I wanted to check if there's a clever built-in mechanism I haven't picked up on instead?

Test cannot be run against typescript@next

I got this error:

Error: /Users/sledorze/projects/monocle-ts/dtslint/index.ts:71:85
ERROR: 71:85  expect  TypeScript@next compile error:
Argument of type '["a", "b", "c", "d"]' is not assignable to parameter of type '[keyof T]'.
  Types of property 'length' are incompatible.
    Type '4' is not assignable to type '1'.

Broken in IE11 when built with Webpack

The package.json file includes main and module fields but is missing a browser field.

The result is that when building with target set to web the import resolves to module which points to es6/index.js which includes arrow functions that break in IE11.

To fix this add a browser field to package.json that points to lib/index.js:

"main": "lib/index.js",
"module": "es6/index.js",
"browser:" "lib/index.js",

see: https://webpack.js.org/configuration/resolve/#resolvemainfields

Prism

πŸ“– Documentation

I have a simple nested tree structure, but I don't know how to update union type and deal with invalid indexes. This code works, but I would like somehow detect whether index(2) contains child and for EditorElementChild, I am lost. Do we have to read the actual value and compose by that? Thank you.

export interface EditorElement extends EditorNode {
  readonly children: (EditorElementChild)[];
}

export type EditorElementChild = EditorElement | EditorText;
const editorElementChildrenLens = Lens.fromProp<EditorElement>()('children');
editorElementChildrenLens.composeOptional(indexArray<EditorElementChild>().index(2)

Create monocle-ts-laws

Is there any plans to create a laws module (monocle-ts-laws, something similar to monocle-laws) ?

Traversal over an array

Hi, I know Monocle from the Scala world, so I was really glad to see a port for it in TypeScript. I'm intending to use Monocle-TS inside the reducers that are required by Redux, which will hopefully make my life much easier while 'mutating' immutable objects.

I came across a use case that I could not figure out how to set up using Monocle-TS, that I was able to solve with Monocle-Scala. I was wondering if this use case is not yet implemented in this library, or if I'm overlooking something.

Given below (in Scala) are Tweet and TweetsViewModel definitions, as well as the lenses/traverals for modifying the text of a Tweet. I can go from TweetsViewModel to List[Tweet] using a lens, compose this with a Traversal over this List[Tweet] and finally compose this with a lens that goes from Tweet to text: String. Then, given an instance of TweetsViewModel I can reverse each text using a simple modify.

import monocle.{Lens, Traversal}
import scalaz.std.list._

case class Tweet(text: String)
case class TweetsViewModel(tweets: List[Tweet] = List.empty)

val tweetsLens = Lens[TweetsViewModel, List[Tweet]](_.tweets)(tweets => _.copy(tweets = tweets))
val tweetTraversal = Traversal.fromTraverse[List, Tweet]
val textLens = Lens[Tweet, String](_.text)(text => _.copy(text = text))

val composedLens = tweetsLens.composeTraversal(tweetTraversal).composeLens(textLens)

val tweet1 = Tweet("hello world")
val tweet2 = Tweet("foobar")
val model = TweetsViewModel(List(tweet1, tweet2))

val newModel = composedLens.modify(_.reverse)(model)

In Monocle-TS I can't figure out how to construct this Traversal.fromTraverse[List, Tweet]. I surely can do this by breaking up the composition into two parts and doing the Traversal myself as shown below, but that is not really as elegant as I was able to do it above. Shouldn't I be able to do a similar thing? Or am I overlooking something?

interface Tweet {
    text: string
}

interface TweetViewModel {
    tweets: Tweet[]
}

const tweetsLens = Lens.fromProp<TweetViewModel, 'tweets'>('tweets')
const tweetTextLens = Lens.fromProp<Tweet, 'text'>('text')

const reverseLens = tweetsLens.modify(tweets =>
                         tweets.map(tweet =>
                             tweetTextLens.modify(text => text.split("").reverse().join(""))(tweet)
                         )
                     )

const tweet1: Tweet = { text: "hello world" }
const tweet2: Tweet = { text: "foobar" }
const model: TweetsViewModel = { tweets: [tweet1, tweet2] }

const newModel = reverseLens(model)

PrismFromRegex ?

Does a PrismFromRegex could be part of this library or is this library considered core and it should be put somewhere else?

asFold on Traversal loses this reference

πŸ› Bug report

This unit test fails

  it('should be convertible to a Fold', () => {
    const numbers = [1, 2, 3, 4]
    const allTraversal = fromTraversable(array)<number>()
    const getFirst = allTraversal.asFold().headOption

    const first = getFirst(numbers)
    assert.equal(first, some(1))
  })

with this error:

TypeError: Cannot read property 'find' of undefined

      696 |   /** get the first target of a Fold */
      697 |   headOption(s: S): Option<A> {
    > 698 |     return this.find(() => true)(s)
          |                 ^
      699 |   }
      700 | }
      701 | 

      at Object.<anonymous>.Fold.headOption (src/index.ts:698:17)
      at Object.<anonymous> (test/Traversal.ts:70:23)

It seems the this reference gets lost somewhere along the way.

I'm still using the 1.x branch. But I verified the same issue exists on master.

When I modify example/Traversal.ts to have similar code, that does succeed.

I am replacing this for the moment with array.findFirst from fp-ts which is more direct, but isn't as nice for use in point-free expressions.

Also in my own code, it doesn't fail everywhere. That's probably why it took me the better part of a day to pin this down.

Software Version(s)
monocle-ts 1.7.2
fp-ts ^1.0.0
TypeScript ^ 3.1.6

Cannot create a generic Lense

The problem started with version 1.5.2, (worked in version 1.5.1).

export interface Indexed {
  index: number
}
const indexedLenseOK = m.Lens.fromPath<Indexed, 'index'>(['index'])
const indexedLenseKO = <T extends Indexed>() => m.Lens.fromPath<T, 'index'>(['index']) // '"index"' does not satisfy the constraint 'ObjectProp<T>'

This is related to
27b587b

Question: How to map over arrays with autoconstruction of a lens per child?

Hi! This might have been addressed in #73 and #77 but I think this is a bit different.
I'm new to all this optics stuff and still wrapping my head around. So the question is wether it is possible to map over some list smartly building a lens per each child. Is there a prior and efficient optic for such cases?

Current solutions I've found:

  • Traversal + filtering Prism: inefficient
  • Array#map + indexArray + filtering out Nones: doesn't compose well
  • Array#map + custom unsafe At: doesn't compose well
    The latter:
export const unsafeIndexArray = <A = never>(): At<A[], number, A> =>
  new At(i => new Lens(list => list[i], value => list => unsafeUpdateAt(i, value, list)));

Neither of them is efficient and convenient.

The use case is very common - rendering forms, lists of controls etc. Any thoughts? Maybe some kind of mapWithLens method for Traversal?

Bad Type Inference when using generics

Hi, I tried creating a lens for accessing elements of an array:

function arrayElem<T>(index: number): Lens<T[], T> {
    function get(s: T[]): T {
        return s[index];
    }

    function set(a: T): (s: T[]) => T[] {
        return (s: T[]): T[] => {
            const copy = [...s];
            copy[index] = a;
            return copy;
        };
    }

    return new Lens<T[], T>(get, set);
}

This works well:

const x = [[0, 1, 2], [3, 4, 5]];
const y = arrayElem(1).compose(arrayElem(2)).set(100)(x);
console.log(y); // [ [ 0, 1, 2 ], [ 3, 4, 100 ] ]

The only problem is that the type inference is messed up. TypeScript thinks that the type of y is Array<{}> (instead of Array<Array<number>>)

y[0][0] // Compile error!

Explicitly providing the type when calling arrayElem fixes things:

const y = arrayElem<Array<number>>(1).compose(arrayElem<number>(2)).set(100)(x);

But it is very annoying to have to explicitly gives types everywhere.

Is there a technique that can be used to create a Generic lens that works well with type inference? Thank you!

lens that fails to focus raises no signals

This could be bug or just a design feature that should be improved.

πŸ› Bug report

When use a lens to modify some data, if the path doesn't exist, lens.modify doesn't do anything but return the existing object.

Is this the right behavior?

I think an exception should be thrown or use either to return a left value indicating a lens has failed to focus.

Clarity about using optics with arrays

Okay, first: I found this PR #48 and discovered that I can use arrayIndex to set/modify values at particular indices in an array, like:

const arr = ['a', 'b', 'c']

indexArray<string>().index(0).set('x')(arr)
// => ['x', 'b', 'c']

...but I'm a little confused, because Lenses can also operate on arrays. However, they aren't quite right, because they convert them into object representations!

fromProp<string[]>()(0).set('x')(arr)
// => { 0: 'x', 1: 'b', 2: 'c' }

However, using a Traversal works as expected and returns an actual array:

fromTraversable(array)<string>().set('x')(arr)
// => ['x', 'x', 'x']

Based on the above, I would expect to be able to use lenses to set/modify values at particular indices in an array & receive back an actual array.

Is this expected or a mistake? Would it be possible to detect when a Setter is acting on an Array and perform an extra step that converts the object representation back into an actual array? Something like Object.values(result) right before returning? Keep in mind, the types already show that you are receiving back an actual array, but if you go to log the result out at runtime you can see that it's actually been converted into an object.

This led me to believe that this behavior is a bug, but, then, I discovered the Index type & arrayIndex, which provide a completely different way to accomplish the same thing without the same quirk.

So, I just wanted to create this issue to clarify. Thoughts?

Help targeting elements in array

Hi. I think this might've been addressed already (here: #73 (comment)) but the usage is confusing me.

I just want to update a particular element in an array. For example:

type Child = {
    readonly id: number;
    readonly value: string;
}

type Parent = {
    readonly children: ReadonlyArray<Child>;
}

What's the easiest/best way to just update (set) the value of the child with id of 1? I'm just having trouble understanding all the options and which to use to achieve this. Do I use Optional if I only need to set (not get)?

Thanks!

"Refinement" is not type-safe

Hi, i'm using monocle-ts Prism, which takes a "Refinement".

I notice that refinement uses the TypeScript is feature:

export interface Refinement<A, B extends A> {
  (a: A): a is B
}

The is feature is completely unsafe and I strive to never use it.

Example:

Prism.fromPredicate<HTMLElement, HTMLCanvasElement>(
    (a): a is HTMLCanvasElement => a.tagName === "canvax")
                                              // Oops! ^
                                              // Typo not caught by the compiler

TypeScript has built-in support for "refinement" types in the form of Discriminated Unions: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions

Here is the Shape example from that page:

type Shape = Square | Rectangle | Circle;

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

And now lets create Prism from "Shape" to "Square":

Prism.fromPredicate<Shape, Square>(
    (a): a is Square => a.kind === "circle")
                           // Oops! ^
                           // The code was incorrectly copy&pasted from the "Circle"
                           // prism and this mistake was not caught by the compiler!

Here the compiler will be a bit more with regards to typos, but will still allow mistakes to slip through. These mistakes can easily happen as a result of an incorrect copy&paste, or as a result of some later code refactoring.

The solution is to never use the is feature of TypeScript. Instead we can define "Refinement" to be:

export interface Refinement<A, B extends A> {
  (a: A): B | undefined
}

Now I can write my "Square" prism like so:

Prism.fromPredicate<Shape, Square>(
    a => a.kind === "square" ? a : undefined)

This is completely bullet proof and 100% checked by the compiler with no room for human error.

If we go back to the "HTMLCanvasElement" Prism, we now have to write it like this:

Prism.fromPredicate<HTMLElement, HTMLCanvasElement>(
    a => a.tagName === "canvas" ? a as HTMLCanvasElement : undefined)

This is still just as dangerous as the original, but at least now the danger is clearly visible because of the as type assertion.

The advantage of my Proposed "Refinement" type is that code-bases that consciously choose to strive to be 100% type-safe can use Discriminated Unions and then be assured that the compiler always has their back

Document At/Index

πŸ“– Documentation

There is no documentation whatsoever for At or Index (atRecord, atSet etc).
It would be helpful to even have some basic docs at the class/function level, if not in markdown.

Question about parent context when traversing nested structure

I'm trying to figure out how to create a fold over leaves in a nested structure, while keeping a reference to the parents somehow. In the same way as how parents are in scope if you do a set of nested maps for example:

data.parents.map(parent => parent.children.map(child => ({ parent, child })))

Working example with the partial.lenses library: https://calmm-js.github.io/partial.lenses/playground.html#MYewdgzgLgBAJgQygmBeGBtAUAbwJZwBcMAjADQzAAWeANnAE4CmYxG+RpJAvhR8SQBMvGP1IBmbgF1euAsUEVqdRizZjBPPvJiDh2zoMkyyczuKU16zVpjHitoneP1Pzx2VKxZQkWMusWAHU8KCoABQQbWHRsABkAOiZaJgBbCFNE6hAQCCYACgAHKJYYgD5MLAByANUwKsyklPTGwrxgAGt8nFq2KQpi6OIAJQSEWgB3BABPCCKSsCgASm4lrCk1rywskFoU4CgAQTmsbtqBhahVtAqMQdKEgksVR7h+nys6kLDI6NNEZBYJZAA

Bug: modify alters functions (ie, `left` and `right` in data structure)

Do you want to request a feature or report a bug?
BUG

What is the current behavior?

const data = {
  people: right( { images: ['foobar'] } ),
  animals: right(  { images: ['foobar'] } )
}
 
const newData = Lens.fromPath<typeof data>()(['people','value','images']).modify( _ => ['foobar'] )

newData.people.isRight === undefined

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://codesandbox.io/ or similar.

https://codesandbox.io/s/m9j0krw3j9

What is the expected behavior?

funcitons are preserved, data on branches not touched are not changed

Which versions of monocle-ts, and which browser and OS are affected by this issue? Did this work in previous versions of monocle-ts?

See codesandbox

more prisms out of the box

Would you be interested in having a few more prisms besides Prism.some defined in this package? So far I've found these two useful also:

import { head } from "fp-ts/lib/Array";
import { fromNullable } from "fp-ts/lib/Option";

function headPrism<A>() {
  return new Prism<A[], A>(
    head,
    a => [a],
  );
}

function fromNullablePrism<A>() {
  return new Prism<Option<A>, A>(
    s => s,
    a => fromNullable(a));
}

Updating fields of class instances drops their prototype, returning raw objects instead

monocle-ts version: 1.2.0

Reproduction:

const { Lens } = require('monocle-ts');

class Foo {
    constructor(fooString) {
        this.fooString = fooString;
    }
}

class Bar {
    constructor(barString) {
        this.barString = barString;
    }
}

class FooBar {
    constructor(fooObject, barObject) {
        this.fooObject = fooObject;
        this.barObject = barObject;
    }
}

const fooBar = new FooBar(new Foo('foo string'), new Bar('bar string'));
const updatedFooBar = Lens.fromPath(['fooObject', 'fooString']).set('new foo string')(fooBar);

console.log(fooBar instanceof FooBar);
console.log(fooBar.fooObject instanceof Foo);
console.log(updatedFooBar instanceof FooBar);
console.log(updatedFooBar.fooObject instanceof Foo);

Expected output:

true
true
true
true

Actual output:

true
true
false
false

Traversal over properties of an object

πŸš€ Feature request

More a question, actually.

Current Behavior

Say I have the following two types:

type A = {
    [key: string]: B
}

type B = {
    foo: string
    bar: string
}

Desired Behavior

I'd like to be able to use monocle to get an instance of {[key: string]: string} by getting all the foo properties from each entry of an A instance.

Is that a thing I can do?

Suggested Solution

I suppose similarly to how it's done with an array? I'm a newb at optics.

Changing the type of the structure

In Haskell you can change the type of the structure:
type Lens s t a b = s -> (a, b -> t) (or whatever representation you choose)
In none of the typescript lens implementations I've seen this.
I tried myself but I couldn't implement it this way without losing type inference.
Have you ever thought about it?

Optional.fromNullableProp is unlawful

πŸ› Bug report

Reproducible example

import { Lens, Prism, Optional } from 'monocle-ts'
import * as O from 'fp-ts/lib/Option'

interface A {
  a: string | undefined
}

const optional1 = Lens.fromProp<A>()('a').composePrism(new Prism<string | undefined, string>(O.fromNullable, a => a))
const optional2 = Optional.fromNullableProp<A>()('a')

import * as assert from 'assert'

const s = { a: undefined }
const a = 'a'
// optional1 and optional2 should be equal but...
assert.deepStrictEqual(optional1.set(a)(s), optional2.set(a)(s)) // throws

Tree map with identity should not return new object

πŸ› Bug report

Maybe I am overlooking something, but when there is no change, the tree should not be changed.
Now modify always changes everything. I suppose it should work like Lens.

interface User {
  name: string;
  age: number;
}

const tree1 = make({ name: 'Boss', age: 42 }, [
  make({ name: 'Manager1', age: 35 }, [
    make({ name: 'Emp1', age: 45 }),
    make({ name: 'Emp2', age: 64 }),
  ]),
]);

const hierarchyTraversal = fromTraversable(tree)<User>();
const tree2 = hierarchyTraversal.modify(identity)(tree1);
// Why it is false?
console.log(tree1 === tree2);

Question on Lens.fromPath usage with generics

Hi, is the following construct possible in TypeScript:

interface Props<P, D> {
  path: P;
  data: D;
}

export const someLibraryFunction = <P extends ???, D>(props: Props<P, D>) => {
  const dataValueLens = Lens.fromPath<D>()(props.path);
  const value = dataValueLens.get(props.data);

  console.log(value);
}

My use case is to receive a valid Lens.fromPath path for a given data type D in a library I'm developing. Ideally I would like to just have path be a Lens passed by the user, but in this particular case I can't force users of the library to install/use monocle-ts unfortunately.

Any ideas how to do something like this, and is it even possible in TS? Thanks!

Performance issues with Props.fromPath

Are you aware of any performance issues with Lens.fromPath and Webstorm? While trying out this library I ran into huge lag spikes when using this function. Other functions, like Lens.fromProp all work fine, but as soon as I type Lens.fromPath() (note: even before I have started typing an actual path), Webstorm becomes very slow (almost freezes) and every key stroke takes like 10 seconds to render.

I think this is TypeScript's type checker that has problems with this function, given it's rather complex type. Have you experienced this behavior as well (or can you replicate this) and/or do you know of any solutions for this issue?

A very simple bench mark of monocle-ts against calmm-js / partial.lenses

I just got started with lens and found that this lib and calmm-js / partial.lenses are the most active ones with considerable stars. Partial.lenses did some performance benchmark against other lens libs but not this lib.

So I wrote some simple bench mark tests for read / write access an object 1,000,000 times with jest. I originally thought partial.lens might perform better since it explicitly ran performance test I assumed it did optimize the lib.

It turned out monocle-ts performed way better. Very impressive...

Here is the result and code:

  Test js lens: clamm-js & monocle-ts performance
    βœ“ plain js read (4ms)
    βœ“ clamm-js read (1131ms)
    βœ“  monocle-ts read (56ms)
    βœ“ plain immutable write with spread (285ms)
    βœ“ clamm-js write (5122ms)
    βœ“  monocle-ts write (1481ms)
    βœ“ clamm-js array read (9344ms)
    βœ“ monocle array read (773ms)

code:

// @ts-nocheck

import * as L from 'partial.lenses'
import * as R from 'ramda'
import { Lens, fromTraversable, Prism } from 'monocle-ts'
import { array } from 'fp-ts/lib/Array'

const lensData = {
  a: {
    b: {
      c: {
        d: 3,
      },
    },
    p: [
      {
        id: 1,
        name: 'Jack',
      },
      {
        id: 2,
        name: 'Jon',
      },
      {
        id: 3,
        name: 'Jay',
      },
    ],
  },
}

const lensRepeat = 1000000

describe('Test js lens: clamm-js & monocle-ts performance', () => {
  it('plain js read', () => {
    let r = undefined
    const run = () => {
      r = lensData.a.b.c.d
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toBe(3)
  })

  it('clamm-js read', () => {
    const path = L.compose(L.prop('a'), L.prop('b'), L.prop('c'), L.prop('d'))
    let r = undefined
    const run = () => {
      r = L.get(path, lensData)
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toBe(3)
  })

  it(' monocle-ts read', () => {
    const path = Lens.fromPath<any>()(['a', 'b', 'c', 'd'])

    let r = undefined
    const run = () => {
      r = path.get(lensData)
    }
    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toBe(3)
  })

  it('plain immutable write with spread', () => {
    let r = undefined
    const run = () => {
      const a = lensData.a
      const b = a.b
      const c = b.c
      const _c = { ...c, d: 4 }

      const _b = { ...b, c: _c }
      const _a = { ...a, b: _b }
      r = { ...lensData, a: _a }
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toEqual({
      a: {
        p: [
          {
            id: 1,
            name: 'Jack',
          },
          {
            id: 2,
            name: 'Jon',
          },
          {
            id: 3,
            name: 'Jay',
          },
        ],
        b: { c: { d: 4 } },
      },
    })
  })

  it('clamm-js write', () => {
    const path = L.compose(L.prop('a'), L.prop('b'), L.prop('c'), L.prop('d'))
    let r = undefined
    const run = () => {
      r = L.set(path, 4, lensData)
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toEqual({
      a: {
        p: [
          {
            id: 1,
            name: 'Jack',
          },
          {
            id: 2,
            name: 'Jon',
          },
          {
            id: 3,
            name: 'Jay',
          },
        ],
        b: { c: { d: 4 } },
      },
    })
  })

  it(' monocle-ts write', () => {
    const path = Lens.fromPath<any>()(['a', 'b', 'c', 'd'])

    let r = undefined
    const run = () => {
      r = path.set(4)(lensData)
    }
    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toEqual({
      a: {
        p: [
          {
            id: 1,
            name: 'Jack',
          },
          {
            id: 2,
            name: 'Jon',
          },
          {
            id: 3,
            name: 'Jay',
          },
        ],
        b: { c: { d: 4 } },
      },
    })
  })

  it('clamm-js array read', () => {
    const path = (id: number) =>
      L.compose(
        L.prop('a'),
        L.prop('p'),
        L.find(R.whereEq({ id })),
        L.prop('name')
      )

    let r = undefined
    const run = () => {
      r = L.get(path(3), lensData)
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toEqual('Jay')
  })

  it('monocle array read', () => {
    const travel = fromTraversable(array)<any>()
    const path = (id: number) =>
      Lens.fromProp<any>()('a')
        .composeLens(Lens.fromProp<any>()('p'))
        .composeTraversal(travel)
        .asFold()
        .find((child: any) => child.id === id)

    let r = undefined
    const run = () => {
      r = path(3)(lensData)
    }

    let i = 0
    while (i < lensRepeat) {
      run()
      i++
    }
    expect(r).toEqual({ _tag: 'Some', value: { id: 3, name: 'Jay' } })
  })
})

Best Practice creating a Lens for StrMap

I would like to know what is the best practice to get and set single elements in a StrMap. My naive idea would be to create Optional like so

const strmapLens = (k: string) => new Optional<any, any>(
  (s) => lookup(id, s),
  (s) => (a) => insert(id, a, s)
);

Is this the way to go or is there something that is already build in ts-monocle?

"Lift" nested Option to outside of object

With monocle, Is it possible to generalise the modification the contents of an object to a different type? For example, below is an implementation of the process for a specific input type Resource that contains a field type: Option<string>, and the required output type Option<Resource2> which contains type: string.

export interface Resource {
  id: number;
  type: Option<string>;
}

const resources = [{ id: 5, type: some("pen") }, { id: 7, type: some("car") }];

export interface Resource2 {
  id: number;
  type: string;
}

const toResource2 = (resource: Resource): Option<Resource2> =>
  resource.type.fold(none, s => some({ ...resource, type: s })); // <- is this supported via monocle?

const resources2 = sequence(option, array)(resources.map(toResource2));

I feel like this is a traversal but I can't figure out how to make it work....

add Prism.fromPredicate function

export function fromPredicate<A>(predicate: Predicate<A>): Prism<A, A> {
  return new Prism<A, A>(
    s => predicate(s) ? some(s) : none,
    a => a
  )
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.