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.
// !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).
// ?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:
// ?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:
// !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()
.
// ?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.
@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 readsa 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 thanan object with a join method
oran object with a toLowerCase method
.