Skip to content
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

enforce constraints on implemented functions; start by at least checking their signatures #4

Open
gabejohnson opened this issue Jun 16, 2017 · 6 comments

Comments

@gabejohnson
Copy link

gabejohnson commented Jun 16, 2017

Barring static types, it would be nice to have checks ensuring that if a protocol intends a field to be a method that an implementation at least defines is as a function (preferably of the same length).

protocol A {
  a; // maybe a function
  b(); // definitely a function
  c(d, e); // definitely a function of at least length two
}

class B {
  [A.c](d, e){}
}

B.prototype[A.a] = 'a';
B.prototype[A.b] = 'b';

B implements A; // Error: Symbol(A.b) must be a function

class C {
  [A.b](){}
  [A.c](){}
}

C.prototype[A.a] = 42;

C implements A; // Error: Symbol(A.c) must be a function of length >= 2
@michaelficarra
Copy link
Member

Very nice idea! I would love to explore even richer constraints, but this seems like a nice first step.

For now, I will leave this out of the proposal proper until I get a feeling for its support as-is.

@Jamesernator
Copy link

Personally I would love some manner of more powerful constraints, although after writing an experimental pattern matching library (here) I found that trying to pattern match functions themselves to be pretty futile and gave up leaving just a generic func pattern which does nothing but check call-ability.

The problem with functions specifically is that there's many valid implementations with different argument lengths thanks to arguments/...args:

function add(a, b) {
    return a + b
}

function add(a, ...[b]) {
    return a + b
}

function add(...[a, b]) {
    return a + b
}

function add() {
    // And we can't forget the old arguments object
    return arguments[0] + arguments[1]
}

So there's no way at class creation time to validate that sort've thing, however you could definitely make it so that it's runtime checked e.g.:

// Supposing some arbitrary fictional syntax
interface Monad {
    bind(Function) -> Monad
}

// Checking on the prototype might be too eager as an instance might still be correct
class Fizz implements Monad {
    constructor() {
        this[Monad.bind] = someInvalidBind
    }
}

class Foo implements Monad {
    [Mond.bind]() {
        return 3
    }
}

class Bar implements Monad {
    [Monad.bind](func) {
        return 'Not a Monad'
    }
}

new Fizz() // InterfaceError, object definitely doesn't satisfy the Monad interface at this point
new Foo().bind(3) // InterfaceError, incorrect arguments for method bind under the Monad interface
new Bar().bind(x => x**2) // InterfaceError returned type doesn't satisfy interface Monad

How feasible this is I'm not certain but it's probably worth looking into, it definitely doesn't enable arbitrary type-checking, but it might allow things to become incrementally "type" checkable.

@gabejohnson
Copy link
Author

@Jamesernator I was thinking a simple Function.prototype.length check when implements is invoked. At the very least, it ensures that the number of "required" parameters matches.

@Jamesernator
Copy link

Function length is a really really unreliable way of checking things, it breaks with even just a simple decorator for example:

function cached(prop) {
    ...
    // This function has length zero
    return func(...args) {
        ...
    }
}

protocol Foo {
    a(b, c)
}

class Bar implements Foo {
    // @cached a(b, c) has length 0 and so would fail the interface
    // which seems counter-useful
    @cached
    a(b, c) {
        ...
    }
}

That's why I came up with some other syntax so that'd be more towards the goals of what you actually want to validate which is generally protocols themselves.

For example: You care that Monad.bind returns another Monad but you shouldn't really ever care that's its implementation uses ...args or not.

class Future implements Monad {
    [Monad.bind](func) {
        
    }
}

class Future implements Monad {
    // Length 0 but it doesn't matter, it's still a valid implementation
    [Monad.bind](...args) {
        if (args.length === 0 || typeof args[0] !== 'function') {
            throw new Error(`Expected a function`)
        }
        return ...
    }
}

It's very similar to the anti-pattern of checking if a function is an async function, while occasionally useful for introspection it tends to be more common for people to mistakenly use this as a way to try and check if something is synchronous or not, but any function can return a Promise not just async function. Effectively it's the same thing here, the details you care about (whether or not it actually takes two arguments) is separate from the implementation detail of real positional arguments.

I made a small couple functions for experimenting with type-based protocols and it seems to work just fine.

@gabejohnson
Copy link
Author

The decorators proposal is Stage 2 AFAIK so I'm not too concerned with interop, but your point is taken @Jamesernator.

@michaelficarra michaelficarra changed the title Distinguish method declarations from other properties enforce constraints on implemented functions; start by at least checking their signatures Jul 18, 2017
@ljharb
Copy link
Member

ljharb commented Jul 25, 2018

I very much want the ability (totally fine to not do it by default, as in the current proposal) to enforce constraints.

Some use cases include "arraylike", but also define a protocol that describes data records - a "data property descriptor" or "accessor property descriptor" protocol, say.

One suggestion I'd have is augmenting the syntax inside protocol blocks (and a corresponding option name in the dynamic API) that allows an additional "has errors" predicate function to be supplied - the function would return something falsy if there were no errors, and a truthy string if there were any errors - and that string would be used as part of the error message thrown by Protocol.implement et al. That way, if i return nothing, it's valid, but if i return a string, something's wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants