phetsims / tandem

Simulation-side code for PhET-iO
MIT License
0 stars 5 forks source link

IOType can supply default state methods by using `valueType` #213

Closed zepumph closed 3 years ago

zepumph commented 4 years ago

This is explained in https://github.com/phetsims/tandem/issues/211#issuecomment-698819668, but deserves its own issue for investigation.

samreid commented 4 years ago

Here is the relevant part of that comment:

If we automatically leverage valueType in ObjectIO like so:

ObjectIO = new IOType( 'ObjectIO', {
  isValidValue: () => true,
  supertype: null,
  documentation: 'The root of the IO Type hierarchy',
  toStateObject: function( coreObject ) {
    return coreObject.toStateObject ? coreObject.toStateObject() : coreObject;
  },
  fromStateObject: function( stateObject ) {
    return this.valueType && this.valueType.fromStateObject ? this.valueType.fromStateObject( stateObject ) : stateObject;
  },
  stateToArgsForConstructor: function( stateObject ) {
    return this.valueType && this.valueType.stateToArgsForConstructor ? this.valueType.stateToArgsForConstructor( stateObject ) : [];
  },
  applyState: function( coreObject, stateObject ) {
    coreObject.applyState ? coreObject.applyState( stateObject ) : null;
  }
} );

Then 7 IO Type declarations in Natural Selection change from looking like this:

Wolf.WolfIO = new IOType( 'WolfIO', {
  valueType: Wolf,
  toStateObject: wolf => wolf.toStateObject(),
  stateToArgsForConstructor: Wolf.stateToArgsForConstructor,
  applyState: ( wolf, stateObject ) => wolf.applyState( stateObject )
} );

to looking like this:

Wolf.WolfIO = new IOType( 'WolfIO', {
  valueType: Wolf
} );
samreid commented 4 years ago

Why does it make sense to take some things from the core type, such as CoreType.fromStateObject, but other things cannot be taken from the core type, such as CoreType.phetioMethods being taken as the IO Type methods or CoreType.phetioDocumentation being taken as the IO Type documentation?

samreid commented 4 years ago

Note the philosophy for this issue was introduced in https://github.com/phetsims/tandem/issues/188, may be good to skim that issue before closing this one, to make sure our bases are covered.

samreid commented 4 years ago

@zepumph and I discussed these APIs

// Reasonable
Bunny.BunnyIO = new IOType( 'BunnyIO', {
  valueType: Bunny,
  fromCoreType: true
} );

// Possibly better?
Bunny.BunnyIO = IOType.fromCoreType( 'BunnyIO', Bunny );
samreid commented 4 years ago

It may be more seamless to move static methods to the core types if we support static attribute, see https://github.com/phetsims/tasks/issues/1048

samreid commented 4 years ago

@pixelzoom can you please review the proposal in https://github.com/phetsims/tandem/issues/213#issuecomment-699190995 ? Does it seem too tricky/magical to you? Or an efficient way to eliminate unnecessary code?

pixelzoom commented 4 years ago

Can you explain what an IO Type does other than serialization and type verification? If the IO Type is delegating to the Core Type, and the IO Type doesn't have other responsibilities, then why is an IO Type necessary?

samreid commented 4 years ago

In addition to serialization, IO types form a type system for PhET-iO Elements for wrapper interoperability. They describe methods, documentation, type hierarchy & events that can be emitted.

pixelzoom commented 4 years ago

Could those responsibilities also be folded into the Core Type, or is there value in keeping them separate?

samreid commented 4 years ago

There are parts that make that difficult or impossible, such as:

That being said, in some cases we could move some things to static elements on the core type. Since we don't support static attributes yet, the syntax could look like:

CoreType.phetioDocumentation = 'An element that etc.etc.';
new IOType('CoreTypeIO',{
   valueType: CoreType
})

as opposed to the current syntax:

new IOType('CoreTypeIO',{
   documentation: 'An element that etc.etc.
   valueType: CoreType
})
pixelzoom commented 4 years ago

OK, thanks for clarifying. So it sounds like doing away with IO Types altogether is not an option.

So back to your original question:

... Does it seem too tricky/magical to you? Or an efficient way to eliminate unnecessary code?

If I understand the various options that have been proposed... I like having both the "non-magical" long form, with a factory method for the "magical" short form. For example:

// Long-form
Wolf.WolfIO = new IOType( 'WolfIO', {
  valueType: Wolf,
  toStateObject: wolf => wolf.toStateObject(),
  stateToArgsForConstructor: Wolf.stateToArgsForConstructor,
  applyState: ( wolf, stateObject ) => wolf.applyState( stateObject )
} );

// Short-form, convenience
Wolf.WolfIO = IOType.fromCoreType( 'WolfIO', Wolf, { ... } );
zepumph commented 4 years ago

I really like this idea! How about the above commit. Please review. I'm mainly wondering if it is too strict to make sure that options doesn't have all 4 of these things:


    if ( assert && options ) {
      assert && assert( !options.hasOwnProperty( 'valueType' ), 'fromCoreType sets its own valueType' );
      assert && assert( !options.hasOwnProperty( 'toStateObject' ), 'fromCoreType sets its own toStateObject' );
      assert && assert( !options.hasOwnProperty( 'stateToArgsForConstructor' ), 'fromCoreType sets its own stateToArgsForConstructor' );
      assert && assert( !options.hasOwnProperty( 'applyState' ), 'fromCoreType sets its own applyState' );
    }
pixelzoom commented 3 years ago

@zepumph asked:

... I'm mainly wondering if it is too strict to make sure that options doesn't have all 4 of these things:

I think that's perfect for now. If we find a reason that one of those options should be applied, we can change later.

Question... I see this in fromCoreType:

225 stateToArgsForConstructor: CoreType.stateToArgsForConstructor,

What happens in CoreType doesn't have stateToArgsForConstructor? I'd like to fromCoreType for several other IO Types in Natural Selection that have this boilerplate, and the Core Type does not define stateToArgsForConstructor:

GenePair.GenePairIO = new IOType( 'GenePairIO', {
  valueType: GenePair,
  toStateObject: genePair => genePair.toStateObject(),
  applyState: ( genePair, stateObject ) => genePair.applyState( stateObject )
} );
zepumph commented 3 years ago

What about this patch?

  static fromCoreType( ioTypeName, CoreType, options ) {

    if ( assert && options ) {
      assert && assert( !options.hasOwnProperty( 'valueType' ), 'fromCoreType sets its own valueType' );
      assert && assert( !options.hasOwnProperty( 'toStateObject' ), 'fromCoreType sets its own toStateObject' );
      assert && assert( !options.hasOwnProperty( 'stateToArgsForConstructor' ), 'fromCoreType sets its own stateToArgsForConstructor' );
      assert && assert( !options.hasOwnProperty( 'applyState' ), 'fromCoreType sets its own applyState' );
    }

    options = merge( {
      valueType: CoreType,
      toStateObject: coreType => coreType.toStateObject(),
      forwardStateToArgesForConstructor: true, // Only needed for dynamic-element deserialization
      forwardApplyState: true // Only needed for reference-type deserialization
    }, options );

    if ( options.forwardApplyState ) {
      options.applyState = ( coreType, stateObject ) => coreType.applyState( stateObject );
    }
    if ( options.forwardStateToArgesForConstructor ) {
      options.stateToArgsForConstructor = CoreType.stateToArgsForConstructor;
    }
    return new IOType( ioTypeName, options );
  }

?

pixelzoom commented 3 years ago

What about this patch?

I don't like forwardStateToArgesForConstructor and forwardApplyState, they are a little too "indirect" for my tastes.

I also don't understand this line in the patch:

forwardApplyState: true // Only needed for reference-type deserialization

Is that comment correct? WolfIO uses dynamic element serialization, and it defines options.applyState. And is applyState really optional with fromCoreType? I guess I'm not clear on which serialization situations use fromCoreType, and that should be documented.

With those caveats, and if stateToArgsForConstructor and applyState are truly optional....

How about this implementation?

  static fromCoreType( ioTypeName, CoreType, options ) {

    if ( assert && options ) {
      assert && assert( !options.hasOwnProperty( 'valueType' ), 'fromCoreType sets its own valueType' );
      assert && assert( !options.hasOwnProperty( 'toStateObject' ), 'fromCoreType sets its own toStateObject' );
      assert && assert( !options.hasOwnProperty( 'stateToArgsForConstructor' ), 'fromCoreType sets its own stateToArgsForConstructor' );
      assert && assert( !options.hasOwnProperty( 'applyState' ), 'fromCoreType sets its own applyState' );
    }

    assert && assert( CoreType.prototype.toStateObject, 'class CoreType must implement method toStateObject' );

    options = merge( {
      valueType: CoreType,
      toStateObject: coreType => coreType.toStateObject(),
    }, options );

    if ( CoreType.stateToArgsForConstructor ) {
      options.stateToArgsForConstructor = CoreType.stateToArgsForConstructor;
    }

    if ( CoreType.prototype.applyState ) {
      options.applyState = ( coreType, stateObject ) => coreType.applyState( stateObject );
    }

    return new IOType( ioTypeName, options );
  }
}

One more nit... options should be config in all of the above, because the param to IOType constructor is config.

zepumph commented 3 years ago

Sounds reasonable to me!

One small issue:

    if ( CoreType.prototype.applyState ) {
      options.applyState = ( coreType, stateObject ) => coreType.applyState( stateObject );
    }

This will not look up the hierarchy. We could recurse though.

pixelzoom commented 3 years ago

Good point. And this will also not recurse (in case you missed this addition):

    assert && assert( CoreType.prototype.toStateObject, 'class CoreType must implement method toStateObject' );
zepumph commented 3 years ago

We can use Inheritance.js as an example of how to recurse for these checks.

pixelzoom commented 3 years ago

@zepumph if you need test cases for fromCoreType, feel free to modify the Natural Selection IO Types listed in https://github.com/phetsims/natural-selection/issues/255.

zepumph commented 3 years ago

One more nit... options should be config in all of the above, because the param to IOType constructor is config.

I disagree. fromCoreType doesn't require anything to be provided via its options, because it sets the require pieces in fromCoreType (valueType).

I added documentation saying that toStateObject is required, but any deserialization method is gracefully supported. I felt good enough to convert all usages worth it in Natural Selection, and I also converted Vector2 to test fromStateObject (which I added support for too).

@pixelzoom please review.

pixelzoom commented 3 years ago

One more nit... options should be config in all of the above, because the param to IOType constructor is config.

I disagree. fromCoreType doesn't require anything to be provided via its options, because it sets the require pieces in fromCoreType (valueType).

OK. I took it for granted that the config param to fromCoreType was correct, but that's apparently not the case since you changed it to options. In any case, they params are now consistent, and I don't see anything config-y in fromCoreType, so bravo!

Closing.