jsigbiz / spec

JavaScript signature notation
131 stars 6 forks source link

Consider using required method specification strategy vs type specification. #1

Open thoward opened 11 years ago

thoward commented 11 years ago

From my perspective, types may not be the best way to specify things. I've shown a few examples below, of code snippets from the description, using an alternate syntax.

Example 1

Param array must implement method reduce, with arity of 2.

// !array => !reduce(_,_)
function reduce(array, reducer, seed) {
  return array.reduce(reducer, seed);
}

Since the above method doesn't touch the other objects there's no need to specify them. It's up to the implementation of reduce to indicate requirements. This could vary by the implementation of reduce.

If something is required, it begins with !... In this case, you must pass a non-null instance of array, and it must have the reduce method. The other params, we have no insight to, in the context of this method.

Example 2

// !res => !end(_)
function endpoint(req, res) {
  res.end('foobar')
}

Same with this example, res must be non-null, and end must exist on it, and have an arity of 1 (eg, takes a single param).

Example 3

// ?names => ?join(_,_)
function greet(names) {
  if (!Array.isArray(names)) {
    console.log('hey ' + names)
  } else {
    console.log('hey ' + names.join(', '))
  }
}

Here, our parameter names is not actually required. It's possible that names is null, which means, !Array.isArray(names) will return false, and the next line console.log('hey' + names) will be fine with a null value. Supposing !Array.isArray(names) returns true, then join will be called. Since we don't know if join will be called, we only indicated that it may be called using ?join(_,_).

A more powerful way to do this might be:

Example 4

// ?names => 
//   | :Array => !join(_,_)
function greet(names) {
  if (!Array.isArray(names)) {
    console.log('hey ' + names)
  } else {
    console.log('hey ' + names.join(', '))
  }
}

Indicating again, that a null value for names is ok, but if it is of type Array (types are indicated w/ colon prefix), then it must implement join(_,_).

Consider this logic:

Example 5

// !names => 
//   | :Array => !join(_,_)
//   | !toLowerCase()
function greet(names) {
  if (Array.isArray(names)) {
    console.log('hey ' + names.join(', '))
  } else {
    console.log('hey ' + names.toLowerCase())
  }
}

Here we specify that names must be implemented, and if it's an Array then it must implement join(_,_), but otherwise, regardless of type, it must implement toLowerCase().

Example 6

// ?names => 
//   | :Array => !join(_,_)
//   | ?toLowerCase()
function greet(names) {
  if (Array.isArray(names)) {
    console.log('hey ' + names.join(', '))
  } else {
    console.log('hey ' + names && names.toLowerCase())
  }
}

Here, a null case is allowed, and so, toLowerCase() may not be called and that is indicated by ? prefix for both the param and the method declaration.

junosuarez commented 11 years ago

@thoward thanks for taking the time to put this together! So the parser side of me likes where you're going with the pattern matching syntax in example 3, but I have to wonder 1) would this be clear to JavaScript programmers? I suppose the whitespace isn't significant. In jsig as proposed, the type indicated in example 3 could be written names: ({join: (Value, Value) => Value} | {toLowerCase: () => String})? In long form, this reads:

a value called 'names' 
which either has a method called "join", 
which takes two parameters of any value and returns a value, 
or a method called "toLowerCase" which takes no parameters and returns a string

There are a couple of things I notice that are emphasized by the different notations. jsig makes the return type explicit. Im not sure why it would make sense to specify the arity of a function without type hints. Jsig is intended to communicate to humans, not a method dispatcher. Do you have an example where it would be useful, to know the arity of a function you're consuming, or a callback you're expected to supply, without knowing anything further about the parameters?

I notice that this notation does a good job of being maximally specific, that is, indicating exactly which methods are actually necessary. This is good for ensuring flexibility and polymorphism (Liskov substitution principle). However, these type annotations are for people, not a type checker. Further, since JavaScript does not have classical inheritance, it is redundant to specify that An Array type must have a join method. Unless someone's deleting it from Array.prototype, the principle of least surprise would suggest referring to the default builtin interfaces. In the case where builtins are being augmented (like in mootools), the additions can be specified in addition to the builtins, eg Array&{contains: (Value) => Boolean}. For communications purposes, however, it will often suffice to just specify String or Array, rather than an object with a join method or an object with a toLowerCase method.