gkz / LiveScript

LiveScript is a language which compiles to JavaScript. It has a straightforward mapping to JavaScript and allows you to write expressive code devoid of repetitive boilerplate. While LiveScript adds many features to assist in functional style programming, it also has many improvements for object oriented and imperative programming.
http://livescript.net
MIT License
2.31k stars 155 forks source link

shortcut for __proto__? #988

Closed determin1st closed 6 years ago

determin1st commented 6 years ago

so it may be used to access base object methods, like:

LS: @@init! =====> JS: this.__proto__.init();

vendethiel commented 6 years ago

Why?

determin1st commented 6 years ago

Why not?

It looks ugly and long to type. @ stands for this, why not some @@ stands for this.__proto__, they are related semanticly.

btw, __proto__ is defined in ECMAScript® 2015, and we like to move forward, right?

https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

rhendric commented 6 years ago

@@ already stands for constructor.

I wouldn't want to add baked-in support for a deprecated language feature anyway.

determin1st commented 6 years ago

No problem, why not add @@ => Object.getPrototypeOf this? constructor may not exist at all.

rhendric commented 6 years ago

Because, again, @@ already stands for constructor. Come up with a different syntax that doesn't conflict with existing syntax (and ideally doesn't evoke something unrelated) and we can discuss.

ymeine commented 6 years ago

There is already the :: syntax which translates to either:

:: and ::right work well in LiveScript class definition blocks since there is an implicit variable named prototype, and actually corresponding to the object being set as the prototype of the defined class.

:: and ::right also work well anywhere else if you defined yourself such a variable with the proper scope.

left:: and left::right work well when left is a class generated by LiveScript since this prototype variable mentioned above is assigned to the constructor. So for a class A you could enhance the prototype later on with A::my_property = .... On instances of A, you can even do instance@@::my_property, since as said in other comments @@ translates to constructor (works the exact same way as ::).

I'm telling this for two reasons:

rhendric commented 6 years ago

Just to clarify, ClassName::instance-method works even if ClassName isn't a LiveScript class.

It's also worth noting that :: inside class blocks refers statically to the enclosing class prototype, not dynamically to the instance prototype. So, for example,

class Foo
  set-default-steve: (value) ->
    ::steve = value

class Bar extends Foo

f = new Foo
b = new Bar
b.set-default-steve 1
console.log f.steve == 1 # true
console.log b.steve == 1 # true

class Foo2
  set-default-steve: (value) ->
    Object.getPrototypeOf this .steve = value

class Bar2 extends Foo2

f = new Foo2
b = new Bar2
b.set-default-steve 1
console.log f.steve == 1 # false
console.log b.steve == 1 # true
ymeine commented 6 years ago

@rhendric Yes, it works anywhere since :: = prototype to sum it up, and that's all the user should remember. Everything else then just follows JavaScript rules regarding scoping and so on. When I said it works in LiveScript classes, I meant: in LiveScript class blocks, there's an implicit prototype variable created at the top, so you can access it. Nice example regarding the difference between using prototype and Object.getprototypeOf in such a use case!

determin1st commented 6 years ago

:: needs a name, also, it works with classes sugar over JS. people often use prototype to change it (why? if you can wrap). what i thought (about __proto__) was an access to down-layer object and it's code reuse. btw, there is a super construct in classes, tried it, but in livescript it translates to superclass which makes things not working staight-forward.

rhendric commented 6 years ago

@determin1st, I'm having a lot of trouble understanding that last comment. Are you saying that you're actually interested in the superclass's prototype and not the instance's prototype? What are you trying with super and what isn't straightforward about it?

determin1st commented 6 years ago

I will post some modified code example (from super docs, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super ):

class Polygon {
  init(height, width) {
    this.name = 'Polygon';
    this.height = height;
    this.width = width;
    console.log('Polygon init');
  }
}
class Square extends Polygon {
  init(length) {
    this.height; // ReferenceError
    super.init(length, length);
    console.log('Square init');
  }
}

this looks (in my interpretation) in livescript like this:

Polygon =
    init: (height, width) !->
        @name = 'Polygon'
        @height = height
        @width = width
        console.log 'Polygon init'

Square = ^^Polygon <<< {
    init: (length) !->
        @height # ReferenceError
        @__proto__.init length, length  # might have been @@init
        console.log 'Square init'
}

so, see, __proto__ looks ugly but it does the same function. if there is no need to change prototype, one could .call base function (@__proto__.init.call @) we could vote for "did you use @@ in your code?" and then make a back-incompatible change. lets vote, why not.

rhendric commented 6 years ago

Why not use classes in the rewrite?

class Polygon
  init: (height, width) !->
    @name = 'Polygon'
    @height = height
    @width = width
    console.log 'Polygon init'

class Square extends Polygon
  init: (length) !->
    @height # ReferenceError
    super length, length
    console.log 'Square init'
determin1st commented 6 years ago

Well,

maybe, there are more restrictions on class syntax. so, dont like to use it.

rhendric commented 6 years ago

Refusing to use the features LiveScript already has to make this sort of thing easier isn't an argument for adding more features to accomplish the same thing a different way.

Also, there's a big problem with using prototypes without classes in the way that your example shows. Suppose we have:

Polygon =
  init: (height, width) !->
    @name = 'Polygon'
    @height = height
    @width = width
    console.log 'Polygon init'

Square = ^^Polygon <<<
  init: (length) !->
    @__proto__.init.call this, length, length
    console.log 'Square init'

UnitSquare = ^^Square <<<
  init: !->
    @__proto__.init.call this, 1
    console.log 'UnitSquare init'

(I changed the __proto__ calls to use .call to keep from mutating Polygon's prototype when initing Square and UnitSquare.)

If you try to run UnitSquare.init!, you'll fall into infinite recursion. Referencing this.__proto__ is a bad way to call methods on a base object because it doesn't statically reference the ‘next’ object in the inheritance chain; it dynamically references whatever object is one level down in the inheritance chain from the instance itself.

If you use classes and super, you don't have this problem. You also don't have to write .call this. If you still don't want to use classes, though, the correct way to write this pattern would be:

Polygon =
  init: (height, width) !->
    @name = 'Polygon'
    @height = height
    @width = width
    console.log 'Polygon init'

Square = ^^Polygon <<<
  init: (length) !->
    Polygon.init.call this, length, length
    console.log 'Square init'

UnitSquare = ^^Square <<<
  init: !->
    Square.init.call this, 1
    console.log 'UnitSquare init'

Which makes no reference of __proto__ at all. Either way, whether you use classes or prototypical inheritance, accessing the instance's prototype isn't necessary.

determin1st commented 6 years ago

That was unexpected. But, imo, you enforced the example, i didn't ever thought about multiple inheritance.. objects go 1+1 (in my code) and there is some clue to continue leveling (through some api).

Polygon =
    init: (name) !->
        @__proto__.init.call @, name

Square =
    init: (name) !->
        @plugin = @[name]!
        ...
    # interface
    Unit: do ->
        # static
        ...
        # constructor
        return ->
            # dynamic
            ...
...
instance = ^^Square <<< Polygon
instance.init 'Unit'
...

a kind of reverse logic... the object of interest is one, prepended with ^^ and extended with base methods <<<.

googled some info about it, seems to be not just a like/dislike code style - https://en.wikipedia.org/wiki/Composition_over_inheritance

rhendric commented 6 years ago

You seem to be describing mixins, not composition. Mixins are a variation on inheritance.

That example is starting to clarify some of your earlier comments for me. Can you construct a more complete, working example, or point me to code in a public repository somewhere? I strongly suspect that LiveScript classes are very useful for the kind of code that you're describing, but I probably can't convince you of that without showing you how I would use them to solve your actual problems.

determin1st commented 6 years ago

I'm trying to design widget library, it's not ready yet, so I can't show working code for now. Here is some schematic code with parts, where __proto__ is used:

w3ui = do ->
    ...
    WIDGET =
        construct: (name) -> (selector, opts) -> # {{{
            # check
            if not (name of WIDGET.store)
                console.log 'w3ui: widget «'+name+'» is not loaded'
                return null
            # get widget prototype
            widget = WIDGET.store[name]
            # query DOM
            if not (node = QUERY selector)
                console.log 'w3ui: DOM query failed for «'+selector+'»'
                return null
            # create new widget object
            widget = ^^widget <<< WIDGET.baseMethods <<< {
                # base properties
                name: name
                node: node
            }
            # initialize api and sub-objects
            widget.init!
            # create
            if not widget.create opts
                widget.log 'failed to create'
                return null
            # done
            return widget.api
        # }}}
        baseMethods:
            init: !-> ... # here__proto__ is used
            create: (opts) -> ... # here__proto__ is used
            setup: (key, val) -> ... # here __proto__ is used
            attach: !-> ...
            log: (msg) !-> ...
        ...
    ...
    return new Proxy QUERY, {
        set: (obj, key, val) -> # load widget prototype {{{
            # check
            if key of WIDGET.store
                console.log 'w3ui: widget «'+key+'» already exist, check your code'
                return true
            # store
            WIDGET.store[key] = val
            return true
        # }}}
        get: (obj, key) -> # w3ui accessor {{{
            # stand-alone function
            return DEP[key] if key of DEP
            # widget constructor
            return WIDGET.construct key
        # }}}
    }

later on, i declare widget:

w3ui and w3ui.accordion = {.../* widget definition */...}

and then, it is used (widget instance is created) like this :

a = w3ui.accordion '#someCssSelector', {.../* accordion widget options */...}

I will revise code and post link to working example later... if it's not enough.

rhendric commented 6 years ago

So, let me describe what I see here before I propose a solution:

Your w3ui object is a proxy that intercepts assignments and treats them as widget type declarations, storing the assigned value for later use as a prototype when a widget of that name is requested.

At instantiation time, you create a new widget object first by making a prototypical clone of the widget type declaration (^^widget) and then assigning methods and properties to it. Behind the scenes, the prototypical clone is creating a new function object (basically an anonymous class), assigning the widget type declaration as its prototype, and then creating an instance with that function.

The assigned methods are using the __proto__ property to access properties on the widget type declaration. This is fragile, for the reasons I laid out in my earlier comment: if anyone ever makes a prototypical clone of your widget objects, the baseMethods methods that use __proto__ are likely to break and/or loop forever. This code works by a kind of coincidence: __proto__ refers to ‘this object's prototype, whatever this object happens to be and however many layers of prototyping have been applied to it’, whereas you seem to always want it to refer to ‘the widget type definition used to create this widget’, and those two things are only the same because you happen to be using inheritance the way you are and because you're assuming that nothing else is going to use inheritance on the objects you create.

If that assumption is good enough for you, well, it's your project. But to anyone considering using __proto__ in this way (and by extension, anyone who might want syntax sugar to assist them in using __proto__ in this way), I would argue that there is a simple and more direct way to say what you mean and avoid future bugs: make baseMethods a function that accepts a widget type definition, and replace all instances of @__proto__ with that argument.

# create new widget object
widget = ^^widget <<< WIDGET.baseMethods widget <<< {
  # base properties
  name: name
  node: node
}
...
baseMethods: (base) ->
  init: !->
    # replace `@__proto__.init.call this, other, args` with
    base.init.call this, other, args
  ...

(FYI, the widget creation can take advantage of several LiveScript idioms to be more succinct, if that's of interest to you:)

# create new widget object
widget = widget with { name, node, ...WIDGET.baseMethods widget }

An alternate solution (because I promised classes!) hinges on observing that the clone operation throws away an identical anonymous class each time it's invoked, and that class could be moved to the widget declaration function and preserved. Here I'll use explicit class syntax so as to also take advantage of the super keyword:

set: (obj, key, val) -> # load widget prototype {{{
  # check
  if key of WIDGET.store
    console.log 'w3ui: widget «'+key+'» already exist, check your code'
    return true
  # store
  WIDGET.store[key] = class extends ((->) <<< prototype: val)
    name: key
    # move all WIDGET.baseMethods here
    init: !->
      # replace `@__proto__.init.call this, other, args` with
      super other, args
    ...
  return true
# }}}

And now widget creation is simply creating a new instance of that class and assigning any per-instance properties:

# create new widget object
widget = new widget <<< { node }
determin1st commented 6 years ago

Yes, you see all correct, except:

if anyone ever makes a prototypical clone of your widget objects, the baseMethods methods that use __proto__ are likely to break and/or loop forever... If that assumption...

this is not an assumption, because widget instance is not returned, only the widget.api. Thanks for your help, i will use that widget with {...} syntax.

rhendric commented 6 years ago

Ah yes, you're right!

Okay, happy to help. I'm closing this issue, but if you have a different use case for accessing an object's prototype that you think is common enough to merit its own syntax and isn't covered by existing LiveScript features, let's reopen.