Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dealing with objects whose constructor is not Object #40

Closed
polytypic opened this issue Jan 10, 2017 · 7 comments
Closed

Dealing with objects whose constructor is not Object #40

polytypic opened this issue Jan 10, 2017 · 7 comments

Comments

@polytypic
Copy link
Member

polytypic commented Jan 10, 2017

Currently property lenses, L.prop, only deal with objects whose constructor is the fundamental object constructor Object. (Index lenses have a corresponding association with the Array constructor.) This has been by intention, because non-plain objects may need to be created by calling the constructor function. Naïvely creating an object with a non-Object constructor and assigning properties to it may not produce a correct result.

Here is an example of the current behaviour:

function XYZ(x,y,z) {this.x=x; this.y=y ;this.z=z}
L.get("x", new XYZ(1,2,3))
// undefined
L.set("x", -1, new XYZ(1,2,3))
// { x: -1 }

Essentially a property lens requires the constructor (or type) of the target to be Object. Otherwise the target is treated as undefined. I believe this behaviour is reasonable and consistent and should not be changed.

However, there are cases where one would like to use lenses with objects whose prototypes are not Object. There are a number of more or less plausible use cases with different requirements:

  • Optics are simply used to query something from the data structure. There is no need to construct objects with non-Object constructors.
  • The application doesn't really care that the objects have non-Object constructors. It is OK to essentially ignore the non-Object constructor and simply construct plain Objects.
  • The constructor and prototype should be preserved (but the objects can be manipulated in a functional manner — the constructor does not perform side-effects other than initialising the object).
polytypic added a commit that referenced this issue Jan 10, 2017
polytypic added a commit that referenced this issue Jan 10, 2017
polytypic added a commit that referenced this issue Jan 10, 2017
@polytypic
Copy link
Member Author

polytypic commented Jan 11, 2017

I've been thinking about this a further while working on PR #41 and an alternative approach came to mind. I think that the current behaviour of prop and index lenses is reasonable. However, it might be better to "relax" their behaviour.

Currently prop lenses require an Object and construct an Object. Relaxing prop lenses to only require an instanceof Object and to construct (unchanged) an Object would allow prop lenses to be used in cases where the application is not interested in the constructors of the objects being queried.

The downside of a more relaxed prop is that it can then unintentionally access properties of just about anything. It is difficult to know how much of a problem this would be in practise, but it seems likely that it would be relatively rare (and possibly more surprising) to be tripped by such behaviour.

In cases where the constructors of objects must be preserved upon construction, one could then precompose the prop with something like L.rewrite(o => Object.assign(Object.create(Constructor.prototype), o)).

Similar relaxation could be useful for index lenses as well, allowing an index lens to access array like objects and then construct an Array, which could then be rewriten to desired type (e.g. String or some typed array type) when it matters. Unfortunately there are many array like types in JavaScript and it may be inefficient to try to robustly check for each. Perhaps a really naïve test like

const isNat = x => Number.isInteger(x) && 0 <= x
const seemsArrayLike = x => x instanceof Object && isNat(x.length)

would be reasonable.

The main advantage of this arrangement, more relaxed prop and index lenses, is that property and index access of, respectively, non-Object and non-Array objects could be made fast (and likely without degrading performance when accessing plain Objects or Arrays) and without need to convert whole objects to access the property or index. It seems likely that in many cases, constructing the result of an update operation as, respectively, an Object or Array, by essentially ignoring the constructor, is also a useful. In cases where the constructor must be preserved, users can then precompose with an appropriate rewrite. There could be some helpers for that as well.

@polytypic
Copy link
Member Author

polytypic commented Jan 15, 2017

The approach of relaxing the type predicates of property and index lenses has basically now been implemented in PR #42.

The first part of the approach is that property lenses are changed to accept just about any instanceof Object and index lenses are changed to accept Strings and any instanceof Object that has a length that is a non-negative integer.

Frankly, I find this a bit scary, but I nevertheless believe it will work out nicely.

The reason why I believe that the relaxed type predicates will likely be usually sufficient is that one usually uses optics to target very specific elements of data structures. The focus of a property lens is most likely either undefined or the very object that is desired to be targeted. And similarly for an index lens. Furthermore, in cases where the naïve type predicates are insufficient, one can e.g. precompose the lens with L.when with a suitable type predicate.

The second part of the approach is that despite the relaxed input, the outputs of property and index lenses are not changed. A property lens always produces an Object and an index lens always produces an Array. This may seem weird at first, but it is really the way to make things work properly. The reason is that the target of a lens is always optional, so a lens doesn't necessarily have a target value whose type to use as a reference. To construct non-Object or non-Array results, one then simply needs to precompose with e.g. L.rewrite to construct an object of the desired type.

@polytypic
Copy link
Member Author

Here are few random examples on manipulating non-Objects.

Property lenses can indeed access properties of pretty much any object:

L.get("length", ["a","b"])
// 2

And the results are not always what one might naïvely think, but they are consistent:

L.set("length", 1, ["a","b"])
// { '0': 'a', '1': 'b', length: 1 }

More interestingly though, objects with non-Object prototypes can be accessed:

function XYZ(x,y,z) {
  this.x = x
  this.y = y
  this.z = z
}
XYZ.prototype.norm = function () {
  return this.x*this.x + this.y*this.y + this.z*this.z
}

L.get("y", new XYZ(3,1,4))
// 1
L.set("y", -1, new XYZ(3,1,4))
// { x: 3, y: -1, z: 4 }

Also not mentioned previously, but object targeting traversals also work on non-Objects:

L.modify(L.sequence, x => -x, new XYZ(3,1,4))
// { x: -3, y: -1, z: -4 }
L.modify(L.branch({x: L.identity, z: L.identity}), x => -x, new XYZ(3,1,4))
// { x: -3, y: 1, z: -4 }

Non-objects cannot be accessed, however:

L.get("length", "strings are not objects")
// undefined

When written through, the result is consistently an Object:

L.set("length", 5, "strings are not objects")
// { length: 5 }

@polytypic
Copy link
Member Author

Here are a few random examples of manipulating non-Arrays.

Index lenses can access strings, typed arrays and pretty much anything that has a length that is a non-negative integer:

L.get(1, "LOLA")
// 'O'
(function () { return L.get(1, arguments) })(true, "args", 2)
// 'args'

When written through, the result is always an Array:

L.set(1, "A", "LOLA")
// [ 'L', 'A', 'L', 'A' ]

To fix the result type, if desired, L.rewrite can be used:

L.set([L.rewrite(R.join("")), 1], "A", "LOLA")
// 'LALA'

The L.sequence traversal can also access non-Arrays:

L.modify(L.sequence, x => x+1, Int8Array.of(0, 127))
// [ 1, 128 ]

And L.rewrite can be used with them as well:

L.modify([L.rewrite(xs => Int8Array.from(xs)), L.sequence],
         x => x+1,
         Int8Array.of(0, 127))
// Int8Array [ 1, -128 ]

polytypic added a commit that referenced this issue Jan 16, 2017
…-semantics

Relaxed treatment of arrays & objects to address #40
@shtanton
Copy link
Contributor

shtanton commented Jul 2, 2018

When using this library with graphqljs, the data received by the server becomes difficult to work with as it is formed using Object.create(null) which means that obj instanceof Object === false.

Perhaps using obj !== null && typeof obj === "object" would be more robust?

I really don't know but right now I have to make custom lenses for this

Thanks

@polytypic
Copy link
Member Author

polytypic commented Jul 2, 2018

I feel your pain. This is a known issue, #127, that comes up every now and then. I'll try and see if I could find a few days next week to make version 14.0.0 happen.

@shtanton
Copy link
Contributor

shtanton commented Jul 2, 2018

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants