jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.49k stars 1.99k forks source link

?!. - make an object if it doesn't exist #1394

Closed jamesonquinn closed 13 years ago

jamesonquinn commented 13 years ago

Currently I can write:

a ?= 1

But if I write a.b?.c ?= 1 and a.b is undefined, then nothing happens.

I'd like to propose a ?!. operator, so that a.b?!.c is equivalent to (a.b ?= {}).c. It would be useful for namespace-like things, where you don't want to worry about who defines it first.

jashkenas commented 13 years ago

Neat idea, but I'm afraid it's a bit too special-case for it's own syntax, and is entirely possible through defining a helper function. For previous tickets about this, see: #1141, and #881.

jamesonquinn commented 13 years ago

I think that this proposal is significantly lighter and more general-purpose than the others you linked to. coco does this (as I realized only after making this proposal) using a.b!c. That syntax is obviously terser than the one I've proposed, but by the same token it is less clear and harder-to-search-the-docs-for. Still, I think that the fact that two people independently came up with this proposal, and both used some syntax including !, suggests that it is intuitive.

If by "too special-case", you mean that not enough use-cases have been shown, I'm sure that could be resolved.... :)

jashkenas commented 13 years ago

Yes -- real world use cases are always the best motivation when arguing for a feature. If this is only really useful for defining the namespace at the top of the file, then eh... But if you can show a bunch of real-world code that's clarified by adding it, that's compelling.

jamesonquinn commented 13 years ago

Basically, this helps when refactoring related code into several subfiles. I'm working on a web app, and so most of my files are putting various things in places like window.myapp.models, window.myapp.views, etc. I'd like to be able to split any one of these files in two or move code from one to another without worrying about loading order or annoying extra checks for existence of those sub-namespaces. Currently I'm using code like ((windows.myapp ?= {}).models ?= {}).MyModel = MyModel but that is definitely less-readable than my suggestion.

jamesonquinn commented 13 years ago

As to the specific syntax involved, I'm not too attached to my specific suggestion.

jashkenas commented 13 years ago

Instead of including all that noise in every file -- it's much nicer to have your namespaces built-out in your first JS file, no? We do:

// Provide top-level namespaces for our javascript.
(function() {
  window.dc = {};
  dc.controllers = {};
  dc.model = {};
  dc.app = {};
  dc.ui = {};
})();

... and then never have to worry about it.

jamesonquinn commented 13 years ago

Sure, that's one way to do it. But you wanted real-world use cases, I gave one. My way is more flexible in terms of being able to mix and match js files without needing to always remember to include that "first js file" at the top. My way also means there's no "first js file" for various developers to collide in on source control. It's a matter of preference.

I can also imagine using this kind of thing if I were doing monkey-patching-like addition of attributes to an object that wasn't created locally. I'd want to be sure I set up a namespace only the first time I "patched" an object, and this would make that (even more) trivial (that is, a couple of characters, instead of one line.)

jashkenas commented 13 years ago

Right -- and what I was saying before is that module namespaces is the only use-case that's ever cited when talking about this feature. Just that use-case isn't quite enough to justify adding special syntax for, and can already be covered quite well by a helper function.

jamesonquinn commented 13 years ago
toucher =
  touch: (obj) ->
    obj._toucher?!.touched = true
  abuse: (obj) ->
    obj._toucher?!.abused = true
  is_touched: (obj) ->
    obj._toucher?.touched?
  untouch: (obj) ->
    delete obj._toucher

Obviously, it's a silly example, but I can certainly imagine real-world cases analagous to the above.

jamesonquinn commented 13 years ago

Another real-world example:

Patching backbone.js:

_add: (model, options) ->
  super model, options
  if model.resource_uri
    (this._byurl ?= {})[model.resource_uri] = model
sxv commented 11 years ago

+1. I assign nested properties to objects on the fly frequently.

For example, I might want to assign

chart.margin.left = 12

before I've established that chart has a "margin" property. Because some use cases need no chart.margin property at all, it becomes inelegant to define chart = { margin: {} } in a "namespace" as @jashkenas recommends above, especially since I nest several deep properties this way. Imagine:

obj[id][category][attr] = value

is currently a major pain but could become painless (and beautiful) as

obj[id]![category]![attr] = value

or, as @jamesonquinn suggests:

obj[id]?![category]?![attr] = value

This seems philosophically similar to RDBMS (define all structure at the beginning) vs NoSQL (define arbitrarily deep hierarchies on the fly), and it is hard to deny the momentum and benefits of the latter. Am I missing something?

edit: moved operators out of key, per comment below.

vendethiel commented 11 years ago

should not be in the key imho

michaelficarra commented 11 years ago
(chart.margin ?= {}).left = 12

See the other issues where we agree that that is sufficiently succinct while also sufficiently versatile.

sxv commented 11 years ago

@michaelficarra Can you demonstrate the same example using square bracket notation rather than dot notation? In my use case the property names, 'margin' and 'left' would not be known, but passed as vars.

vendethiel commented 11 years ago

Just use bracket notation. (chart[prop] ?= {}).foo = 1

sxv commented 11 years ago

Ok got it, thanks. So the example from above currently looks like this:

((obj[id] ?= {})[category] ?= {})[attr] = value

which I can live with. Not that much more verbose (at least at two-levels deep) than

obj[id]?![category]?![attr] = value
epidemian commented 11 years ago

Dynamically expandable objects? That would be a great use case for Proxies IMO :smiley:


@sxv, if you're doing this a lot maybe a helper function can help:

nestedAssign = (obj, props..., lastProp, value) ->
  baseObj = obj
  for prop in props
    baseObj = (baseObj[prop] ?= {})
    # Maybe you can throw an error here if typeof baseObj isnt 'object'
  baseObj[lastProp] = value
  obj

Then you can use it as:

nestedAssign {}, 'foo', 'bar', 'baz', 42 # -> {foo:{bar:{baz:42}}}

Or, in your case:

nestedAssign obj, id, category, attr, value
thedeeno commented 11 years ago

+1.

I think @sxv's ! suggestion is clearly superior to any purposed work-around. It's concise, communicative, and expressive.

I don't understand the arguments against this feature. Why is this 'establishment operator' more specific and less useful than the 'existential operator'? It seems if one is good so is the other. Are we saying this is a bad pattern or something?

Notice how the discussion doesn't talk about problem domains, specific implementations, or even specific types. How is this specific? Further, if we think about how this would be implemented, it's not hard nor impractical.

This what the existential operator does:

[when chaining] if all of the properties exist then you'll get the expected result, if the chain is broken, undefined is returned instead of the TypeError that would be raised otherwise.

This is what the establishment operator would do:

[when chaining] if all the properties exist then you'll get the expected result, if the chain is broken, objects are created to prevent the ReferenceError that would be raised otherwise.

Seems generic, clear, and symmetric. Shouldn't we reopen this?

thedeeno commented 11 years ago

After sleeping on it I take this back. I think the best argument is that it is a bad pattern. If we can't guarantee that an object exists that uncertainty ripples through the rest of our code. Much better to explicitly create it in one spot (like in @jashkenas namespace example).

billymoon commented 10 years ago

I would love to see this implemented as either foo?!bar or foo!bar (second format makes much more sense to me though). My use case, is building an object to send to server, which updates a sparse object with only the keys I have populated.

If I want to add an item to the customer entity, I don't want to have to deal with the entire customer object which is shared between different parts of the app, I simply want to do...

customer!hat!types = ['bowler', 'trilby']
ajax.put resturi, customer

Rather than...

customer = customer or {}
((customer ?= {}).hat ?= {}).types = ['bowler', 'trilby']

ajax.put resturi, customer

Gets better the more nesting that is involved.

It is not really relevant to generate an empty customer object in my case, but I have many nested properties that I need to generate.

vendethiel commented 10 years ago

I agree that (semi) autovivification is nice.