Skip to content

Loading…

Private properties (suggested easy implementation) #2142

Closed
jelmerc opened this Issue · 9 comments

4 participants

@jelmerc

I saw that a few enhancement issues a la "I can haz private properties?" have been shot down because "it's not possible in JS", but here's my 2 cents.

A while ago I implemented my own JavaScript compiler (don't ask me why...) and I added full-fledged classes. That project's over now, and I'm looking into CoffeeScript. I really like the fact that code gets a LOT more readable while the JS stays lightweight and pretty close to the original. I don't think much more syntactic sugar should be added, but I do have a way to add private properties without adding much bloat at all.

Example (skip over the long line of literal JS for now):

class Person
  `function __p(t,s){return(t.__P1||(s={},t.__P1=function(f){return(f===__p)?s:{}}))(__p)}`

  constructor: (name) ->
    __p(this).name = name

  say: (what) ->
    console.log "#{what}, says #{__p(this).name}"

  meet: (other) ->
    console.log "#{__p(this).name} meets #{__p(other).name}"

class Contact extends Person
  `function __p(t,s){return(t.__P2||(s={},t.__P2=function(f){return(f===__p)?s:{}}))(__p)}`

  constructor: (name, phoneNumber) ->
    super name
    __p(this).phoneNumber = phoneNumber

  dial: () ->
    console.log "Calling #{__p(this).phoneNumber}"
    console.log "Private name: #{__p(this).name}"


bob = new Person 'Bob'
harry = new Contact 'Harry', '12341234'
harry.dial()
harry.say 'Hi'
bob.say 'Hello'
bob.meet harry

### output:
Calling 12341234
Private name: undefined
Hi, says Harry
Hello, says Bob
Bob meets Harry
###

So, the way to access a private property in the above example is:

__p(object).name

Where the object would normally be this, of course. This is where syntactic sugar comes in, so that we can write, e.g.:

@_name  # since underscore should already denote private

Or maybe use two underscores, or another operator, that's not really the point here.

Note that the output says "Private name: undefined" because the 'name' property is private to the Person class, and cannot be accessed from the Contact class, even though it's a subclass. So in a sense it's more private than in Java.

The most important addition to the compiled JS output would be this line of extremely compressed code that should be included whenever a class uses private properties:

function __p(t,s){return(t.__P1||(s={},t.__P1=function(f){return(f===__p)?s:{}}))(__p)}

Where __P1 should be some different ID for each class, preferably a pretty long random string so it won't collide with any possible subclass. I.e. something like __privUnDlSD4J5xWh0. Note that this is NOT the key to keeping private vars 'secret'; it's just a way to have a different set of private vars for each class on the same object.

Basically this adds one extra public (!) property to an object (with aforementioned unique ID), which is just a reference to a closured function specific to the class. The function returns an object 's' which contains the private properties and is actually contained in the surrounding closure (as a parameter to remove 4 bytes for 'var '... yeah!). However, before returning this object, the function checks if its single argument is a reference to the function it's wrapped inside of, which obviously has limited scope and hence is only ever accessible from within the class. Result: anyone can 'see' this public method on the object, but it only returns the correct object when supplied with a 'secret' reference available from within the class. Sounds complicated? Nahh ;)

The __p function itself wraps all this up nicely.

Cons:

  • An extra public property on every object of the class, with an ugly name
  • To avoid collision of the public property name, the class-specific ID like __privUnDlSD4J5xWh0 should be pretty long, and needs to be generated at compile time. There is still a very slight chance of name collision of course, and no way to check for it or get around it unless we can use some global variable to register all IDs.
  • There is absolutely no way to get access to private properties from e.g. a debugger! They're hidden, private. Duh.

Pros:

  • Short & sweet!
  • It works.

Worth adding? Who can do it?

@showell

@jelmerc I doubt this proposal will be accepted, but I can suggest an alternative for getting (mostly) private variables.

class Person
  constructor: (@name, password, pin) ->
    @__ =
      password: password
      pin: pin

  is_password_valid: (attempt) => attempt == @__.password

person = new Person("Alice", "w0nder1and", 9999)
console.log person.name # Alice
console.log person.__.password # backdoor for debugging "private" vars
console.log person.is_password_valid("w0nder1and") # true
console.log person.is_password_valid("foo") # false

In the above code, privacy is only enforced by convention, but you will at least avoid accidental corruption of your objects. Also, it's relatively painless to use the "@__" namespace within the class itself.

If you want to create objects with truly private variables, you might want to use the closure style of creating objects shown below:

Person = (name, password, pin) ->
  public_interface =
    name: name
    is_password_valid: (attempt) => attempt == password

person = Person("Alice", "w0nder1and", 9999)
console.log person.name # Alice
console.log person.password # undefined; password only accessed through closure
console.log person.is_password_valid("w0nder1and") # true
console.log person.is_password_valid("foo") # false
@jelmerc

@showell What are your doubts wrt my proposal? How would this accidentally corrupt objects?

I'm not a big fan of your proposal since the properties aren't actually private at all, there's just an extra object... This is why most proposals are shut down because if it can't be done properly then there's no point doing it.

@showell

@jelmerc I think you misread my comment about "corrupting objects". All I was saying was that if you followed my first example, you would at least avoid accidental corruption of your objects. By that, I mean the only way folks would access the password "private" variable is by deliberating going through the "__" namespace. In other words, a non-malicious user of the class wouldn't accidentally access variables that you intended to be private.

The reason that I doubt the proposal will be accepted is that I've never heard it stated as a goal of coffeescript to help enforce private variables. I could be wrong, and it's not my decision to make; it's just a hunch. I'm personally -1 on it, simply because I don't think it's necessary.

@csubagio

I don't know if Coffeescript really needs this, but as showell points out, the use of closures can "hide" away any number of variables you like. E.g.

class Foo 
  constructor: (pwd) ->
    private = "stuff"
    @getPwd = -> pwd 
    @manglePwd = -> ( c for c, i in pwd when i % 2 == 0 ).join('') + private

a = new Foo "silverfish"
console.log a.getPwd()  # read only access
console.log a.manglePwd()  # uses any number of (hur hur) privates
console.log a​​​​​​​​​​​​​​​​​​ # doesn't actually have _any_ properties other than functions!

I try to avoid doing this though, unless I really think I might shoot myself in the foot accidentally later and it's worth sealing things up in another dimension, as again you end up not being able to see things in a debugger.

@jelmerc

@csubagio thanks for your example. I think this is how it's usually done in JS, so it makes sense in CoffeeScript too.

Your example does highlight the reasons why I think it would be profitable to add my solution to CoffeeScript, though:

  • using this method (constructor closures), methods need to be manually assigned to properties by the constructor for each instance. This circumvents prototypes altogether, which is kind of silly.
  • method definition within the constructor looks odd, it defeats the purpose of having a 'class' keyword in CoffeeScript, if it just becomes a wrapper for the constructor function... (i.e. class Foo constructor: (x) -> is exactly the same as Foo (x) ->)
  • each method needs its own closure referring to the constructor scope (for access to private vars); this is expensive if you have many instances or many calls to a method

On a general note: yes, a full-fledged class-based OO model needs private properties. CoffeeScript provides the 'class' keyword, and even 'super', which is a great start to class-based OO, but there are no private properties. The reason why all other class-based OO implementations do have private properties is because it's useful in a world where the person coding a class is not necessarily the same person using or extending the class. How is the second person supposed to know not to use a certain property name because it's used internally?

I know we're 'just' talking about a scripting language, but as JS projects get bigger and bigger (browser-based or server side) we should look at how other languages support big projects.

Plus my solution requires no overhead if not using private properties, and almost none if you are (certainly MUCH less overhead than e.g. the 'super' keyword). Anyone got knowledge of the CoffeeScript compiler (not me :S) and time to implement this?

@csubagio

I don't think having private variables is necessarily a requirement for a "full-fledged" OO scheme at all, especially when the underlying language doesn't support them.

I'm also a all kinds of iffy on inherited classes not being able to see the variable at all, never mind alter it. That's a pretty radical idea, probably more appropriate in an aggregate pattern, rather than an inheritance one. To wit: if you wrote __p(@).name = x in a base class, writing __p(@).name = y in a child class isn't an error, it declares a new private variable. That's pretty counter intuitive for me coming from C++, and could easily get confusing.

I'd certainly not be in favor of that becoming a part of the language.

Having said that, you're free to do as you like :)

Here's an attempt at factoring out your hidden variable (by hiding it in plain sight), reducing it down to one closure per object, no matter the depth of the inheritance, with the assumption that you're not interested in using toString() for anything else. Added bonus: the actual lookup has been reduced to a function call + hashtable lookup, no conditionals.

id = 0
privatize = (cls) ->
  _ = {}
  register = (obj) ->
    id += 1 if "#{obj}" != "#{id}"
    strid = "#{id}"
    obj.toString = ->
      return strid
    _[strid] = {}
  [register, _]

class Foo
  [ register, _ ] = privatize @

  constructor: (name) ->
    register @
    _[@].name = name

  sayHi: ->
    console.log "foo says Hi #{_[@].name}" 

  reportPrivate: ->
    console.log _


class Boo extends Foo
  [ register, _ ] = privatize @

  constructor: (name) ->
    super()
    register @
    _[@].name = name

  booSayHi: ->
    console.log "boo says Hi #{_[@].name}" 

  reportPrivate: ->
    console.log _


a = new Foo "Alice"
b = new Foo "Bob"
c = new Boo "Jane"

a.sayHi()
b.sayHi()
c.sayHi()
c.booSayHi()
a.reportPrivate()
c.reportPrivate()​​​​​​​​​​​​​​​​​​​
@showell

@jelmerc asks: " How is the second person supposed to know not to use a certain property name because it's used internally?"

I hope I'm not taking the question too much out of context, but the answer is simple--put the private variables in a namespace that is private by convention. My suggestion is to use "@__".

Here are some precedents from other languages:

C++/Java -- privacy enforced by compiler, "private" culturally accepted
Ruby -- :private feature available, not much culture acceptance
Python -- privacy never enforced by Python itself (consenting adults), but strong convention of "_" meaning private
JavaScript -- privacy enforced by closure idioms

I personally like Python's philosophy the most.

@showell

@jelmerc This should probably be closed, since it didn't seem to get much support. If this addresses a specific shortcoming in other private-property proposals, you might want to add links between the issues, so your arguments don't get lost.

I'll explicitly give a -1 on this. My objections are purely philosophical--I like privacy to be enforced by convention only.

@vendethiel
Collaborator

#651

One big issue I see with this is having to track and rewrite all private calls. There is going to be too much magic in the core happening for my liking.


Yep, full-blown private methods (inheritable, overridable), are not possible in JavaScript without sacrificing a little more than we're willing to give.

@vendethiel vendethiel closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.