-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide a narrowing read-only interface for Property #363
Comments
Eventually we may redo the type hierarchy in #343 but until then this is working great: declare type ReadonlyProperty<T> = Omit<Property<T>, 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred'>; We can add it to axon once axon supports TypeScript. In fact, I think I could add it now since JS sims won't depend on it. Testing... |
This patch adds Index: main/axon/js/ReadonlyProperty.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/main/axon/js/ReadonlyProperty.ts b/main/axon/js/ReadonlyProperty.ts
new file mode 100644
--- /dev/null (date 1636523233606)
+++ b/main/axon/js/ReadonlyProperty.ts (date 1636523233606)
@@ -0,0 +1,11 @@
+// Copyright 2021, University of Colorado Boulder
+import Property from './Property.js';
+
+type getValue<T> = {
+ get value(): T
+};
+type ReadonlyProperty<T> =
+ Omit<Property<T>, 'value' | 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred'>
+ & getValue<T>;
+
+export default ReadonlyProperty;
\ No newline at end of file
Index: main/bending-light/js/more-tools/view/ChartNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/main/bending-light/js/more-tools/view/ChartNode.ts b/main/bending-light/js/more-tools/view/ChartNode.ts
--- a/main/bending-light/js/more-tools/view/ChartNode.ts (revision 145e8a32029a2b8cad467a99e898a990e14e7ee7)
+++ b/main/bending-light/js/more-tools/view/ChartNode.ts (date 1636522803498)
@@ -9,6 +9,7 @@
import createObservableArray from '../../../../axon/js/createObservableArray.js';
import Property from '../../../../axon/js/Property.js';
+import ReadonlyProperty from '../../../../axon/js/ReadonlyProperty.js';
import Bounds2 from '../../../../dot/js/Bounds2.js';
import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js';
import Node from '../../../../scenery/js/nodes/Node.js';
@@ -31,7 +32,7 @@
* frames
* @param {Bounds2} chartBounds - bounds of the chart node
*/
- constructor( series: Series, modelViewTransformProperty: Property<ModelViewTransform2>, chartBounds: Bounds2 ) {
+ constructor( series: Series, modelViewTransformProperty: ReadonlyProperty<ModelViewTransform2>, chartBounds: Bounds2 ) {
super();
Index: main/bending-light/js/more-tools/view/SeriesCanvasNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/main/bending-light/js/more-tools/view/SeriesCanvasNode.ts b/main/bending-light/js/more-tools/view/SeriesCanvasNode.ts
--- a/main/bending-light/js/more-tools/view/SeriesCanvasNode.ts (revision 145e8a32029a2b8cad467a99e898a990e14e7ee7)
+++ b/main/bending-light/js/more-tools/view/SeriesCanvasNode.ts (date 1636523341941)
@@ -12,10 +12,11 @@
import bendingLight from '../../bendingLight.js';
import DataPoint from '../model/DataPoint.js';
import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js';
+import ReadonlyProperty from '../../../../axon/js/ReadonlyProperty.js';
class SeriesCanvasNode extends CanvasNode {
seriesProperty: Property<DataPoint[]>;
- modelViewTransformProperty: Property<ModelViewTransform2>;
+ modelViewTransformProperty: ReadonlyProperty<ModelViewTransform2>;
color: string;
/**
@@ -25,13 +26,20 @@
* @param {string} color - color of the series
* @param {Object} [providedOptions] - options that can be passed on to the underlying node
*/
- constructor( seriesProperty: Property<DataPoint[]>, modelViewTransformProperty: Property<ModelViewTransform2>,
+ constructor( seriesProperty: Property<DataPoint[]>,
+ modelViewTransformProperty: ReadonlyProperty<ModelViewTransform2>,
color: string, providedOptions?: NodeOptions ) {
super( providedOptions );
this.seriesProperty = seriesProperty; // @private
this.modelViewTransformProperty = modelViewTransformProperty; // @private
this.color = color; // @private
+
+ console.log( modelViewTransformProperty.value );
+ modelViewTransformProperty.link( m => {console.log( m );} );
+ modelViewTransformProperty.reset();
+ modelViewTransformProperty.value = ModelViewTransform2.createIdentity();
+ modelViewTransformProperty.setValue( ModelViewTransform2.createIdentity() );
}
/**
For convenience, here is the type definition ReadonlyProperty.ts: // Copyright 2021, University of Colorado Boulder
import Property from './Property.js';
type getValue<T> = {
get value(): T
};
type ReadonlyProperty<T> =
Omit<Property<T>, 'value' | 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred'>
& getValue<T>;
export default ReadonlyProperty; And a screenshot showing how you can get values and link but not reset or change the values: It appears that adding this TypeScript file to axon would not disrupt any pure JavaScript sims, so I think it is safe to add despite our "no TypeScript in common code repos yet" policy. This is because this new file would only be used by TypeScript simulations (since it is a TypeScript *.ts type declaration). For JavaScript sims, it would neither be picked up in the webpack step nor would it require a |
Re: > type ReadonlyProperty<T> =
+ Omit<Property<T>, 'value' | 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred'>
+ & getValue<T>; I guess I can see how this might be considered OK from a structural-typing perspective. But from an OOP perspective, it's backwards - ReadonlyProperty would be the base class, and a "writeable" Property would extend it to make it mutable. Re use cases... I see how declaring a constructor/method parameter as ReadOnlyProperty can be used -- you can pass in a Property argument, and the client is limited to the subset of the API that deals with observing and reading the Property (the API is narrowed). I'm not clear on how ReadOnlyProperty would be used in the more common case, where a class has a Property field that is private/protected writable, but public read-only. Is something like this what you had in mind? : class Thermometer {
// Writeable by this class and its subclasses.
protected readonly temperatureProperty: NumberProperty;
// Requires an additional public reference to someProperty that is read-only ??
public readonly temperatureReadOnlyProperty: ReadOnlyProperty<number>;
constructor(...) {
this.temperatureProperty = new NumberProperty(..., {
range: new Range (...),
...
} );
this. temperatureReadOnlyProperty = this.temperatureProperty;
// constructor and methods can then set this.temperatureProperty
}
}
// client
const thermometer = new Thermometer();
thermometer.temperatureProperty.link( ... ); // ERROR, private
thermometer.temperatureReadOnlyProperty.link( ... ); // OK Questions?
|
I considered 2 strategies for how to allow the class to write to the Property but only allowing clients to read the Property. Strategy A is the one you mentioned above, which seems good to me. An alternative is to only have one field for it, but to provide access via a narrowing getter like so: Strategy B: class Thermometer {
// Writeable by this class and its subclasses.
protected readonly _temperatureProperty: NumberProperty;
constructor() {
this._temperatureProperty = new NumberProperty(0);
// constructor and methods can then set this._temperatureProperty
}
get temperatureProperty():ReadOnlyProperty<number>{
return this._temperatureProperty;
}
winterFreeze(){
this._temperatureProperty.set(-100);
}
}
// client
const thermometer = new Thermometer();
thermometer.temperatureProperty.set( ... ); // Type Error, method does not exist in interface
thermometer.temperatureProperty.link( ()=>{} ); // OK
thermometer.winterFreeze(); // OK Strategy A: the duplicate class attribute is not great and the naming is confusing.
Let's discuss whether Strategy B is preferable before deciding on the naming for Strategy A.
I think strategy A or B would work with any Property types or subtypes that match the interface. New questions:
|
I like Strategy B, but...
|
Read-only is a compound adjective. And compound adjectives are typically converted to camel case - e.g. "well-behaved three-legged dog" would be |
We could make a more general ReadOnly generic like: type ReadOnly<X, T> =
Omit<X, 'value' | 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred'>
& getValue<T>; Usage would look like: get temperatureProperty(): ReadOnly<NumberProperty, number> {
return this._temperatureProperty;
} Call site is OK: thermometer.temperatureProperty.rangeProperty; // OK
Perhaps we would make a way to add more restrictions in the ReadOnly type. type ReadOnly<X, T, Z extends string> =
Omit<X, 'value' | 'set' | 'setPropertyValue' | 'setInitialValue' | 'reset' | 'dispose' | 'setDeferred' | Z>
& getValue<T>; get temperatureProperty(): ReadOnly<NumberProperty, number, 'rangeProperty'> { thermometer.temperatureProperty.rangeProperty; // ERROR: rangeProperty does not exist on the type
The proposal to think of
I agree, we should go with New Questions:
|
In general, yes.
This is a feature that will undoubtedly be used widely, and it seems like "type narrowing" is potentially a good direction. But it's difficult to evaluate whether |
The proposal for this issue seems reasonable, but the recommendation is to wait until we have discussed #343. Going on hold until then. |
This issue has been assigned to me to review for a really long time. I'm unsure if it is still the right place for it, but here is a review of axon files being converted to typescript, which seems to have occurred here. Please let me know if there is more that I should do here, or assign me a side issue if this one is on hold or too long. Review: Property,ts
NumberProperty
I don't have any comments that I can see for DerivedProperty, except that it the Types declared at the top are really awesome. Perhaps as I use it more I will have thoughts, but right now I am mostly impressed with how seamless it feels with the current implementation. |
Regarding the review in the preceding comment, as far as I can tell all those issues have been resolved in other work. We no longer need the generic described in #363 (comment). We have discussed Strategy B from #363 (comment) in dev meeting, it seems useful sometimes. @zepumph and @pixelzoom can you think of remaining work for this issue? Can it be closed? |
Not for me! |
Me neither. Closing. |
From phetsims/chipper#1137 (comment) we identified that we would like a way to say that a Property can have its value read and linked but not set.
This may overlap with #343
The text was updated successfully, but these errors were encountered: