coco icon indicating copy to clipboard operation
coco copied to clipboard

Enhance <<< and <<<<

Open jasonkuhrt opened this issue 13 years ago • 21 comments

Currently the import or <<< feature is handy but a version that allows for only undefined values to be imported would help I think.

Below is a common use-case:

human = (args)->
    defaults =
        height: 6
        hair: 'brown'

    args <<< defaults
    console.log args, defaults


human do
    country: 'canada'
    job: 'mail man'
    height: 3

In this example, defaults.height overrides args.height.

Maybe the syntax could be something like:

<<<?
<<<<?

I am not sold on those at all, just potentials. Maybe we could change <<< to behave non-destructively by default. Then we could provide bang syntax to force overrides, like such:

<<<!
<<<<!

jasonkuhrt avatar Mar 14 '11 01:03 jasonkuhrt

Could be useful I guess. Got any compilation ideas?

args <<< defaults

Doesn't defaults <<< args accomplish the same thing in this case? This leaves the passed object unmodified as well.

<<<?

We need to come up with the import counterparts also (for this, something like import? and import all?). Or maybe we can discard these aliases taking this opportunity.

<<<!

Simpler, more frequently used operations should get simpler syntaxes. (The proposed behavior has significantly less use cases.)

satyr avatar Mar 14 '11 02:03 satyr

defaults <<< args

This would accomplish a similar thing. But defaults becomes the identifier for the rest of the scope. Mentally you are trying to work with an identifier ( args ) that has some default fallbacks.

My thought process is:

"I am merging defaults into args", not "I am merging args into defaults".

Or,

"I am plugging holes with defaults" not "I am filling space with args".

I am going to look up some example implementations now.

jasonkuhrt avatar Mar 14 '11 03:03 jasonkuhrt

For starter's here is one implementation I've created recently myself (I wrote it in cs, not coco, and haven't really refactored yet):

    copy: (obj)->
        copied = {}
        for own key,val in obj
            copied[key] = val
        return copied

    mergeDefaults: (defaults,custom)->
        ncustom = this.copy custom
        for own key,val in defaults
            ncustom[key] ?= val
        return ncustom

In this example mergeDefaults has no side-effects, it returns a new object. It is based on John Resig's work I will find some others as I am by no means a master progarmmer, js or otherwise.

jasonkuhrt avatar Mar 14 '11 03:03 jasonkuhrt

But defaults becomes the identifier for the rest of the scope

That's only a matter of naming. You can write for example: human = (args)-> args = {height: 6, hair: \brown, ...args} ...

satyr avatar Mar 14 '11 03:03 satyr

I'm not sure I follow.

args = {height: 6, hair: \brown, ...args}

Does that mean flatten args into the enclosing object literal? And since it's at the end, it would overwrite anything prior?

I have three sources for import-related implementation. Some from JS Patterns, some from JS Cookbook, and the Traits.js library also deals with this subject.

I will look at the three more closely tomorrow.

jasonkuhrt avatar Mar 14 '11 04:03 jasonkuhrt

Does that mean flatten args into the enclosing object literal? And since it's at the end, it would overwrite anything prior?

Right.

satyr avatar Mar 14 '11 04:03 satyr

The splat is nice. But would you still endorse the original impetus? It seems to me that the flexibility for either syntax serve slightly different intentions on the programmer's part.

Here is where my thinking is currently at:

# W/O PROTOTYPE

# into new obj (x y z not affected)
merged_copy_shallow = import x y z
merged_copy_shallow = <<< x y z

# into existing obj (y z not affected)
x import y z
x <<< y z


    # ONLY UNDEFINED

    merged_copy_shallow = import? x y z
    merged_copy_shallow = <<<? x y z

    x import? y z
    x <<<? y z



# W PROTOTYPE

merged_copy_deep = import all x y z
merged_copy_deep = <<<< x y z

x import all y z
x <<<< y z


    # ONLY UNDEFINED

    merged_copy_shallow = import? all x y z
    merged_copy_shallow = <<<<? x y z

    x import? all y z
    x <<<<? y z

I think I like the above flexibility and expression but there is one concept missing which is deep vs shallow importing. What happens when a nested object is encountered? Shallow copying logic means pass by reference, Deep copying logic means iterate through sub-objects properties, too.

Here's an example implementation from JS Patterns:

extendDeep = (parent,child)->
    i
    toStr = Object.prototype.toString
    astr = "[objeect Array]"

    child = child or {}

    for i of parent
        if parent.hasOwnProperty i
            if typeof parent[i] is "object"
                child[i] = if toStr.call(parent[i]) is astr then [] else {}
                extendDeep parent[i], child[i]
            else
                child[i] = parent[i]
    return child

One possible way to support this in coco might be with a bang symbol:

# W/O PROTOTYPE

# into new obj (x y z not affected)
merged_copy_shallow = import! x y z
merged_copy_shallow = <<<! x y z

# into existing obj (y z not affected)
x import! y z
x <<<! y z


    # ONLY UNDEFINED

    merged_copy_shallow = import?! x y z
    merged_copy_shallow = <<<?! x y z

    x import?! y z
    x <<<?! y z



# W PROTOTYPE

merged_copy_deep = import! all x y z
merged_copy_deep = <<<<! x y z

x import! all y z
x <<<<! y z


    # ONLY UNDEFINED

    merged_copy_shallow = import!? all x y z
    merged_copy_shallow = <<<<!? x y z

    x import!? all y z
    x <<<<!? y z

If all the above were solved, I think a fundamental and painful part of JS would suddenly become much easier to write and less error prone. I think <<< is core enough to embed at the language-level, but it just needs to be flushed out a bit, I think.

How do you feel about this?

jasonkuhrt avatar Mar 14 '11 15:03 jasonkuhrt

The so-called deep-copying is less used and very difficult to implement right (even jQuery's has flaws). What if there are:

  • circular references?
  • non-array objects with non-Object.prototype [[Prototype]](regexes, dates, user-defined ones, etc.)?

So for this functionality, it's better to define your own or stick with whatever your libraries provide. Like Coffee, Coco aims to be a bundle of syntax sugars, not a framework.

satyr avatar Mar 14 '11 17:03 satyr

If a full-proof implementation is not likely as you've pointed out then I totally agree.

I appreciate the impetus to not be a framework. I assume you view features like <<< as having large enough use cases and being easy enough to implement to warrant slight exceptions to this goal?

Dropping the shallow / deep aspect, where does that leave us? Do you see value in the aforementioned syntaxes:

merge x import y u w undefined-props-only t import? o t import? o p x to new variable fresh = import u fresh = import x u y

jasonkuhrt avatar Mar 14 '11 18:03 jasonkuhrt

slight exceptions to this goal

They really are simple sugars that help you go DRY:

a <<< b:c, d:e   # a.b = c; a.d = e
a <<<< b         # for(var k in b) a[k] = b[k]

merge x import y u w

Are you suggesting multiple arguments? Note that you can write: x <<< y <<< u <<< w

undefined-props-only t import? o t import? o p x

My only concern for this is obvious/popular use cases.

to new variable fresh = import u fresh = import x u y

fresh = {} <<< u
fresh = {...x, ...u, ...y}

satyr avatar Mar 14 '11 19:03 satyr

merge x import y u w

Are you suggesting multiple arguments?

Yes I think arguments is the right word here.

Note that you can write: x <<< y <<< u <<< w

Ah, I see, but this seems counter-intuitive then. I read 3 operations, not 1. I expect y and u to merge what is to the right of them. So to summarize I expect the difference between the two being this:

x <<< y u w

merge y u w into x

x <<< y <<< u <<<< w

merge all w into u merge u into y merge y into x

My only concern for this is obvious/popular use cases.

I will try to build up a list of use-cases beyond the defaults pattern.

fresh = {} <<< u fresh = {...x, ...u, ...y}

Good points. One advantage is that the { } make it explicit what is happening.

jasonkuhrt avatar Mar 14 '11 19:03 jasonkuhrt

I read 3 operations

3 they are. The key points here are that import:

  • returns the left operand.
  • is left associative.

Meaning that a <<< b <<< c is evaluated as (a <<< b) <<< c and returns a.

satyr avatar Mar 14 '11 20:03 satyr

Ah! I see even more.. ha.

Do you not think a <<< b <<< c is better expressed as a <<< b c? The former seems misleading insofar as b appears to be affected by c. Now I understand that it is not, but readability wise I prefer the latter.

jasonkuhrt avatar Mar 14 '11 20:03 jasonkuhrt

Just toying, the possibility of inline functions.. would need to use a different syntax for merging multiple objects then... Looks ugly in all sorts of ways...

a <<< func b, e; u

evaluated to ((a <<< func(b, e)) <<< u)

If it had to come down to something so boorish.. better as...

a <<< func(b,e) u troy (t <<< o e) << i
( (((a <<< func(b,e)) <<< u) <<< ( ((t <<< o) <<< e) <<< i)

jasonkuhrt avatar Mar 14 '11 20:03 jasonkuhrt

Do you not think a <<< b <<< c is better expressed as a <<< b c

a <<< b c is parsed as a <<< b(c). Making it take arbitrary number of arguments is grammarwise challenging--we'd have to make it require parentheses (like a <<< (b; c)).

satyr avatar Mar 14 '11 20:03 satyr

Ah ok, I've been assuming all along an exception could be made as soon as the compiler hits <<< syntax. But ignoring my theoretical world... I can imagine it's not so simple (or desirable to start breaking fundamental rules like that).

jasonkuhrt avatar Mar 14 '11 20:03 jasonkuhrt

personally I think a <<< b <<< c looks more intuitive anyway.

adrusi avatar May 20 '11 01:05 adrusi

a <<<? b seems useful to me. Some examples would be loading/saving a JSON file, using <<<? to add default values.

ghost avatar Apr 08 '12 01:04 ghost

=)

jasonkuhrt avatar Apr 08 '12 10:04 jasonkuhrt

I implemented part of this, but I'm not sure how to submit a patch properly.

ghost avatar Apr 13 '12 21:04 ghost

For the original use case in this bug, the objective isn't really the soft import, but default "keyword" arguments, which you can kind of get already:

human = (args{height ? 6, hair ? \brown}: args) ->
    console.log args
var human;
human = function(arg$){
  var args, ref$;
  args = arg$, args.height = (ref$ = args.height) != null ? ref$ : 6, args.hair = (ref$ = args.hair) != null ? ref$ : 'brown';
  return console.log(args);
};

It expresses the intention more succinctly, though with pretty ugly syntax. Something like:

human = (args <<< defaults) ->

or

human = (args extends defaults) ->

would be ideal.

qqueue avatar Oct 19 '12 01:10 qqueue