Ticket #249 (new defect)

Opened 1 year ago

Last modified 1 year ago

The types [T], [T1,T2,...], and ArrayLike

Reported by: lth Assigned to: lth
Type: defect Priority: major
Milestone: Component: Spec
Version: 4 Keywords:
Cc: brendan,graydon,dherman,cormac,jeffdyer

Description

(Result of email discussion between Brendan and myself.)

What does it mean that something matches an array-structural type? The white paper says that [int] is a subtype of Array.

Brendan objects that that's too specific, why aren't array-like things like Vector possible? At least, should it not be true that Vector.<int> is like [int]?

My response is that since Array has a lot of intrinsic methods, several of which have signatures not remotely compatible with the corresponding methods on Vector, this is not obvious. On the other hand, array structural types are magic, so we can do what we want.

We can possibly finess it by saying that [T] as a type expression means that (a) it has a fixture called length whose value is constrained to be uint and (b) the elements named by uints are constrained to be T. (We can't require it to be dynamic, because Vector isn't, at least not if the purpose is to match Vector.)

Obviously then generic array methods could take arguments whose types are like [*], which is an improvement, though I could also express that as { length: uint }.

Attachments

Change History

Changed 1 year ago by lth

And of course, [int,string] is the same, it just has different constraints on uint-named slots.

Changed 1 year ago by brendan

All of the ad-hoc shape testing I know of simply probes for length being numeric (not even uint32). I thought I saw something that looked for a slice method, but MochiKit? does only this (Base.js):

    isArrayLike: function () {
        for (var i = 0; i < arguments.length; i++) {
            var o = arguments[i];
            var typ = typeof(o);
            if (
                (typ != 'object' && !(typ == 'function' && typeof(o.item) == 'function')) ||
                o === null ||
                typeof(o.length) != 'number'
            ) {
                return false;
            }
        }
        return true;
    },

The o.item jazz is for DOM nodelists and the like, only in the case where those have type 'function' in Firefox and other SpiderMonkey?-based browsers (see #251).

The intrinsic fixtures in Object are implied by a record type R = {p: int} and that's probably ok. There are only six (or seven if toJSONString is included) such methods.

But for array structural types, saying that [int] contains 20 intrinsic-qualified fixtures, most of which have restrictive type annotations tied to Array, reduces the utility of array structural types to the point where the shape-testing done by lots of Ajax libraries and Web JS can't use a is like []. That seems a loss.

Human factors come into play: users don't think of the prototype properties as being "in" the object, and by analogy, prototype methods are *not* "in" the structural type. But the intrinsic fixture counterparts to the prototype methods are implicit. Implicit is worse than explicit.

If we remove these fixtures, then Array <: [T], and the structural array and tuple types are more general than Array in the sense that they require only length:uint32 and index properties bounded by length. They're less general than array in that, e.g., [int] is monotyped and [int, boolean, string] is a tuple, but that's a separable, and useful, design decision.

So what goes wrong if we generalize array structural types by removing the intrinsic fixtures that currently hide in them via their relation to Array?

/be

Changed 1 year ago by lth

One problem is that if I say something is [T] I really have no idea about the methods that are on offer. Certainly the Array methods need not be there if all we require is length. In other words, this program type-checks:

   function f(x: [*]) { ... }
   f([1,2,3]:[int])
   f({length: 10} : {length:uint})

Presumably this one won't type check:

   function f(x: [*]) { x.slice(...) }

I think the key is that array-ness is more than just length:uint but it *may* be less than the full signature of Array.

Consider how structural array types are used during construction: they annotate array literals (or can be used in new expressions but it's the same thing). The objects thus constructed are unquestionably Array objects, though more constrained (they have side conditions). They are never Vector objects, nor even instances of subclasses of Array.

re Brendan's note, I understand the motivation but the problem with [int] matching Vector.<int> seems to be bad implicitness too, when [int] in all other contexts means an Array that's constrained to [int].

How about creating a predefined type to capture what we want to capture?

   type ArrayLike = { length: uint }

Changed 1 year ago by brendan

Let me just record this thought, which Lars and I were discussing in person today:

type ArrayLike = {
    length: uint,
    intrinsic::map: function (this:ArrayLike, Mapper, Object=): ArrayLike,
    intrinsic::concat: function (...items): ArrayLike,
    ...
};

Given such a type, would all of !Array <: ArrayLike, !Vector <: ArrayLike, and of course the structural types [int] <: ArrayLike, etc.? The hard case would be formal parameters that want to be of type Array in an Array method, but making the matching param in ArrayLike? have type ArrayLike? would be covariant.

/be

Changed 1 year ago by brendan

Never mind the recursive structural types! Lars suggests loosening up to use Callable as the type of the methods:

type ArrayLike = {
    length: uint,
    intrinsic::concat: Callable,
    intrinsic::every: Callable,
    intrinsic::filter: Callable,
    intrinsic::forEach: Callable,
    intrinsic::indexOf: Callable,
    intrinsic::join: Callable,
    intrinsic::lastIndexOf: Callable,
    intrinsic::map: Callable,
    intrinsic::pop: Callable,
    intrinsic::push: Callable,
    intrinsic::reverse: Callable,
    intrinsic::shift: Callable,
    intrinsic::slice: Callable,
    intrinsic::some: Callable,
    intrinsic::sort: Callable,
    intrinsic::splice: Callable,
    intrinsic::unshift: Callable
};

If anyone wants a looser type to match things like DOM nodelists, {length:uint} is short enough. But it is not the base Array structural type.

/be

Changed 1 year ago by lth

  • summary changed from What do [int] and [int,string] actually mean? to The types [T], [T1,T2,...], and ArrayLike

Add to the above set of methods intrinsic::reduce, intrinsic::reduceRight.

The proposals on the table are:

  • to affirm that types [T] and [T1,T2] describe only instances of Array; and
  • to include a predefined structural type ArrayLike in ES4, for use by code that wants to work with objects that have method suites like Array

Changed 1 year ago by cormac

Just to clarify, it seems that we have two separate design choices:

  • what values are of type [T]
  • what values are of type like [T]

where of course like [T] describes more values that [T].

Note: See TracTickets for help on using tickets.