Writable Getters
lea.verou.me
lea.verou.me
With that said, this seems like a recipe for debugging nightmares. Getters and setters already make life difficult, hiding statefulness and complexity in a way that’s not apparent at the call site (you don’t even know it’s a call site!). Adding magic where sometimes a value is computed and sometimes it’s not amplifies those problems.
Maybe my FP is showing, but all of this could be much more easily accomplished with plain value type data structures and functions. Computed values can be derived either at construction time (ideal, fewer nullish properties and less chance of passing incomplete values off to something that expects them to be complete) or by passing the structure to a function (or calling a method if you must, but here `this` is just an implicit argument to allow access to other properties of the structure).
Replacing a computed value is as simple as constructing a new value structure with the value set explicitly.
All of that is a lot easier to reason about (in terms of just reading the source to see what it does), almost certainly has better performance characteristics, and definitely makes concurrency less complex.
These principles work very well in a general FP environment, but they can be applied to great benefit even in more imperative environments like JS.
let lea = {
name:"Lea verou",
getId:function() { return this.name.toLowerCase().replace(/\W+/g, "-");}
};
lea.getId() // lea-verou
lea.getId=function() { return this.name; }
lea.getId() // Lea Verou
"syntactic sugar causes cancer of the semicolon" -- A. PerlisThat said, I don't use JS and I agree it still looks like a bad idea, although this is not too heretic in a prototype-based-like language.
const createLea = (data) => ({
...data,
id: (
data.id ??
data.name
.toLowerCase()
.replace(/\W+/g, '-')
),
});
No mutation, no getters (function or implicit), dead simple to use, dead simple to reassign (just call the function again with the same data but provide a new `id` and super easy to understand at a glance (easier with types, but I digress).> prototype-based-like language
One of the benefits of the approach I describe is you’re no longer working in a prototype based language. I mean, of course you are, but you don’t have to care.
Edit: even on mobile I couldn’t stand the formatting.
Edit 2: since I mentioned performance in my original comment, I’d add that there are two benefits here (one real, one possible with JITs): getters and setters (and anything with Object.defineProperty) perform worse than regular values; if you program in this style, the JIT has an opportunity to recognize that your values are generally immutable and optimize object construction e.g. with the spread operator as persistent data structures the same way FP languages do.
The proper place for this mess is the setter.
This is my subjective opinion.
rather than cut off this feature & not use it, do you have any thoughts for how an ide or debugger might present some up front information about the nature of said objects? how can we reduce the risk of using the language via tooling? it'd be nice in my opinion to not retreat from the language's flexible, powerful, useful expressiveness?
Since you asked, the tooling is pretty goodish with VSCode if you use typescript, but still misleading for dependencies (getters are just treated as readonly properties, setters are indistinguishable from writeable properties).
I think in practice this ends up being mostly a function of what's idiomatic in the ecosystem. The most common JS style guide recommends _against_ using getters and setters, so it would be surprising for something in JS to do so. The most common Python style guide recommends _for_ using properties, so the syntactical near-equivalent ends up being something "expected".
> Note 2: Try to keep the functional behavior side-effect free, although side-effects such as caching are generally fine.
Personally, and I'm happy to to be wrong, I can't think of any situation where mutating data in a getter is a good idea... in any language ever. Every single time I've caught myself thinking about it, if I take a step back, what I'm about to do is hot garbage and thankfully my hot-garbage-sense caught on.
Having a dynamic default value makes sense, but instead of having the object reconfigure itself on the fly, why not design a single virtual field that intelligently chooses between two distinct sources of data? For example, you could write a class like this:
class MyObject {
#customFoo;
get foo () {
return this.#customFoo ?? this.#getDefaultFoo();
}
set foo (val) {
this.#customFoo = val;
}
#getDefaultFoo () {
// dynamically generate and return foo value
}
}
That way you could get a dynamically created value up until a custom value is set, with the option to switch back to the dynamic default simply by setting the field to null or undefined.Also, I'm not sure if you were implying that it's a good thing, but the concept of "properties which execute code to produce side effects when they are get or set" seems a bit risky to me, especially the "get" part... it can make it very difficult to reason about the state of a system when simply accessing a property on an object has side effects. Obviously side effects don't need to be avoided at all costs, but IMHO it's best if they only occur during explicit function calls.
I'm probably missing something since the author is a renowned author, and I'm just a dabbler. What am I missing though?
let lea = {
name: "Lea Verou",
get id() {
return this.explicitId ?? this.name.toLowerCase().replace(/\W+/g, "-");
},
set id(val) {
this.explicitId = val;
}
};
console.log(lea.id);
lea.id = 'lv';
console.log(lea.id); const lea = {
name: "Lea Verou",
_id: null,
get id() {
return this._id || this.name.toLowerCase().replace(/\W+/g, "-");
},
set id(v) {
this._id = v;
}
} pirl @> sub hats : lvalue { my ($self) = @_; $self->{hats}; }
@var = ();
pirl @> $obj = bless {}
@var = (
bless( {}, 'Shell::Perl::sandbox' )
);
pirl @> $obj->hats
@var = (
undef
);
pirl @> $obj
@var = (
bless( {}, 'Shell::Perl::sandbox' )
);
pirl @> $obj->hats = 1
@var = (
1
);
pirl @> $obj
@var = (
bless( {
'hats' => 1
}, 'Shell::Perl::sandbox' )
);
pirl @> $obj->hats
@var = (
1
);
pirl @> $obj->hats++
@var = (
1
);
pirl @> $obj->hats
@var = (
2
);
pirl @>it was not well documented. ever since I've avoided mutating getters.