Method.invoke with variable list of parameters

I would like to write a class representing a Method and an variably-sized array of parameters to pass into the method on invocation.

Is there a more elegant solution to calling invoke of the Method than one call for each of the number of parameters I want to support?

class EvccTask {
    private var _method as Method;
    private var _args as Array<Object>;

    public function initialize( method as Method, args as Array<Object>? ) {
        _method = method;
        _args = args == null ? new Array<Object>[0] : args;
    }

    public function invoke() as Void {
        if( _args == null || _args.size() == 0 ) {
            _method.invoke();
        } else if (_args.size() == 1 ) {
            _method.invoke( _args[0] );
        } else if (_args.size() == 2 ) {
            _method.invoke( _args[0], _args[1] );
        } else if (_args.size() == 3 ) {
            _method.invoke( _args[0], _args[1], _args[2] );
        } else {
            throw new OperationNotAllowedException( "EvccTask: too many arguments!" );
        }
    }
}

Top Replies

  • executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checker

    executeTask( aClass.method( :aMethod ) ); // this works

    This has come up before, so I opened a bug…

  • there's no varargs

    I guess Method.invoke() actually does use varargs somehow, but I don't know of any way for *us* to use varargs.

    So it seems like Lang.Method uses at least 2 "special…

All Replies

  • You could change the methods to receive an array or dictionary instead of different number of params.

  • I did not want to change the called functions, because they are also called outside of this task wrapper. So instead I opted for replacing Method with an interface:

    typedef EvccTask as interface {
        function invoke() as Void;
    };

    Now I can pass it in either Method objects, or wrapper objects that have a method inside, but also store parameters and apply them for the invoke. The actual execution can therefore always be without parameters, something like this:

    function executeTask( task as EvccTask ) as Void {
        task.invoke();
    }

    However the interface is not really the same as Method.invoke, apart from the Void as return, the variable number of parameters seems to be something that cannot be declared for normal functions. But there seems to be an inconsistency in the type checker that I could exploit to still pass in Method objects without having to disable type checker for the functions that execute tasks:

    executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checker

    executeTask( aClass.method( :aMethod ) ); // this works
  • You could change the methods to receive an array or dictionary instead of different number of params.
    I did not want to change the called functions, because they are also called outside of this task wrapper.

    Is the specific concern here style/readability or type safety?

    If it's type safety, you could change the functions so they accept a tuple as an argument. A tuple is a fixed-length array where each indexed item has a distinct type.

    Given that calling a function with the wrong number of args is a run-time error, and there's no varargs or array spread operation in Monkey C, I don't think there's a way to do exactly what you want.

  • executeTask( new Method( aClass, :aMethod ) ); // this trips up the strict type-checker

    executeTask( aClass.method( :aMethod ) ); // this works

    This has come up before, so I opened a bug report:

    forums.garmin.com/.../type-checker-cannot-infer-exact-type-of-callback-created-with-lang-method-but-it-can-do-so-for-object-method

    EDIT: maybe I misunderstood the issue and bug report isn't that relevant :/

    EDIT2: no, I think I understood the issue after all. I've added a comment to the bug report explaining what I think you're seeing (although additional context, like examples of aClass and aMethod, would be helpful)

    https://forums.garmin.com/developer/connect-iq/i/bug-reports/type-checker-cannot-infer-exact-type-of-callback-created-with-lang-method-but-it-can-do-so-for-object-method?CommentId=1712190c-530b-474d-8022-51aaaf418602

    TL;DR

    - new Lang.Method() always produces a generic Method (with unknown args and return type), it cannot fulfill your interface contract (and that's why it also produces a warning/error when used as one of the standard callback types in the Connect IQ API)

    - Object.method() produces a method with correct information about the args and return type of the referenced function, so it can fulfill your interface contract, provided the function exists and type of args and return value match that of invoke() in your interface.

    e.g.

    import Toybox.Graphics;
    import Toybox.WatchUi;
    import Toybox.Lang;
    
    typedef EvccTask as interface {
        function invoke() as Void;
    };
    
    function executeTask( task as EvccTask ) as Void {
        task.invoke();
    }
    
    class SomeClass extends Object {
        // does not fulfill EvccTask (signature does not match)
        function randomMethod(x as Number) as Void {}
        // does fulfill EvccTask (signature matches)
        function randomMethod2() as Void {}
    }
    
    function foo() {
        // in the case of new Method() the type checker has no clue about the method signatures (args, return value)
        executeTask(new Method(SomeClass, :randomMethod)); // ERROR: Invalid '$.Toybox.Lang.Method' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
        executeTask(new Method(SomeClass, :nonExistentSymbol)); // ERROR: Invalid '$.Toybox.Lang.Method' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
    
        // in the case of Object.method(), the type checker does know about the method signatures (args, return value),
        // as long as the passed-in symbol refers to an known, existing function
        var someObject = new SomeClass();
        executeTask(someObject.method(:randomMethod)); // ERROR: Invalid '$.Toybox.Lang.Method(x as $.Toybox.Lang.Number) as Void' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
        executeTask(someObject.method(:randomMethod2)); // this produces no warning/error, and I don't expect it to
        executeTask(someObject.method(:nonExistentSymbol)); // ERROR: Invalid '$.Toybox.Lang.Method() as Any' passed as parameter 1 of type 'interface { function invoke() as Void; }'.
    
    }

  • there's no varargs

    I guess Method.invoke() actually does use varargs somehow, but I don't know of any way for *us* to use varargs.

    So it seems like Lang.Method uses at least 2 "special" language features that don't really work (or aren't fully supported) in other contexts:

    - varargs (no way to to declare / write our own function that uses varargs)

    - accepting class / module name as an argument. Yes, we can write a function that does this, but certain things don't work well - like if you assign a class name to a variable, then type checking doesn't work properly

  • - new Lang.Method() always produces a generic Method (with unknown args and return type), it cannot fulfill your interface contract (and that's why it also produces a warning/error when used as one of the standard callback types in the Connect IQ API)

    - Object.method() produces a method with correct information about the args and return type of the referenced function (provides the function exists), so it can fulfill your interface contract, provided the referenced functions args and return value matches that of invoke() in your interface.

    Yes, that sounds like what I am seeing!