1 Number-parameterized types

This article is also available in PDF. This Wiki page is not the master file: rather, it is the result of the SXML->Wiki conversion.

2 Abstract

This paper describes practical programming with types parameterized by numbers: e.g., an array type parameterized by the array’s size or a modular group type `Zn` parameterized by the modulus. An attempt to add, for example, two integers of different moduli should result in a compile-time error with a clear error message. Number-parameterized types let the programmer capture more invariants through types and eliminate some run-time checks.
We review several encodings of the numeric parameter but concentrate on the phantom type representation of a sequence of decimal digits. The decimal encoding makes programming with number-parameterized types convenient and error
messages more comprehensible. We implement arithmetic on decimal number-parameterized types, which lets us statically typecheck operations such as array concatenation.
Overall we demonstrate a practical dependent-type-like system that is just a Haskell library. The basics of the number-parameterized types are written in Haskell98.

2.1 Keywords:

3 Contents

4 Introduction

Discussions about types parameterized by values — especially types of arrays or finite groups parameterized by their size — reoccur every couple of months on functional programming languages newsgroups and mailing lists. The often expressed wish is to guarantee that, for example, we never attempt to add two vectors of different lengths. As one poster said,
“This [feature] would be helpful in the crypto library where I end up having to either define new length Words all the time or using lists and losing the capability of ensuring I am manipulating lists of the same length.” Number-parameterized types as other more expressive types let us tell the typechecker our intentions. The typechecker may then help us write the code correctly. Many errors (which are often trivial) can be detected at compile time. Furthermore, we no longer need to litter the code with array boundary match checks. The code
therefore becomes more readable, reliable, and fast. Number-parameterized types when expressed in signatures also provide a better documentation of the code and let the invariants be checked across module boundaries.

In this paper, we develop realizations of number-parameterized types in Haskell that indeed have all the above advantages. The numeric parameter is specified in decimal rather than in binary, which makes types smaller and far easier to read. Type error messages also become more comprehensible. The programmer may write or the compiler can infer equality constraints (e.g., two argument

vectors of a function must be of the same size), arithmetic constraints (e.g., one vector must be larger by some amount), and inequality constraints (e.g., the size of the argument vector must be at least one). The violations of the constraints are detected at compile time. We can remove run-time tag checks in functions like

vhead

, which are statically assured to receive a non-empty vector.

Although we come close to the dependent-type programming, we do not extend either a compiler or the language. Our system is a regular Haskell library. In fact, the basic number-parameterized types can be implemented entirely in Haskell98. Advanced operations such as type arithmetic require commonly supported Haskell98 extensions to multi-parameter classes with functional dependencies and higher-ranked types.

Our running example is arrays parameterized over their size. The parameter of the vector type is therefore a non-negative integer number. For simplicity, all the vectors in the paper are indexed from zero. In addition to vector constructors and element accessors, we define a

zipWith

-like operation to map two vectors onto the third, element by element. An attempt to map vectors of different sizes should be reported as a type error. The typechecker will also guarantee that there is no attempt to allocate a vector of a negative size. In Section sec:arithmetic we introduce operations

vhead

,

vtail

and

vappend

on number-parameterized vectors. The types of these operations exhibit arithmetic and inequality constraints.

The present paper describes several gradually more sophisticated number-parameterized Haskell libraries. We start by paraphrasing the approach by Chris Okasaki, who represents the size parameter of vectors in a sequence of data constructors. We then switch to the encoding of the size in a sequence of type constructors. The resulting types are phantom and impose no run-time overhead. Section sec:unary-type describes unary encoding of numerals in type constructors, Sections sec:decimal-fixed and sec:decimal-arb discuss decimal encodings. Section sec:decimal-fixed introduces a type representation for fixed-precision decimal numbers. Section sec:decimal-arb removes the limitation on the maximal size of representable numbers, at a cost of a more complex implementation and of replacing commas with unsightly dollars signs. The decimal encoding is extendible to other bases, e.g., 16 or 64. The latter can be used to develop
practical realizations of number-parameterized cryptographically interesting groups.

Section sec:arithmetic describes the first
contribution of the paper. We develop addition and subtraction of “decimal types”, i.e., of the type constructor applications representing non-negative integers in decimal notation. The implementation is significantly different from that for more common unary numerals. Although decimal numerals are notably difficult to add, they make number-parameterized programming practical. We can now write arithmetic equality and inequality constraints on number-parameterized types.

Section sec:dynamic briefly describes working with number-parameterized types when the numeric parameter, and even its upper bound, are not known until run time. We show one, quite simple technique, which assures a static constraint by a run-time check — witnessing. The witnessing code, which must be trustworthy, is notably compact. The section uses the method of blending of static and dynamic assurances that was first described in stanamic-trees.

Section sec:related compares our approach with the phantom type programming in SML by Matthias Blume, with a practical dependent-type system of Hongwei Xi, with statically-sized and generic arrays in Pascal and C, with the shape inference in array-oriented languages, and with C++ template meta-programming. Section sec:conclusions concludes.

5 Encoding the number parameter in data constructors

The first approach to vectors parameterized by their size encodes the size as a series of data constructors. This approach has been used extensively by Chris Okasaki. For example, in Okasaki99 he describes square matrixes whose dimensions can be proved equal at compile time. He digresses briefly to demonstrate vectors of statically known size. A similar technique has been described by McBride. In this section, we develop a more naive
encoding of the size through data constructors, for introduction and comparison with the encoding of the size via type constructors in the following sections.

Our representation of vectors of a statically checked size is reminiscent of the familiar representation of lists:

data List a = Nil | Cons a (List a)

List a

is a recursive datatype. Lists of different sizes have the same recursive type. To make the types different (so that we can represent the size, too) we break the explicit recursion in the datatype declaration. We introduce two data constructors:

We get a type error, with a clear error message (the quoted message, here and elsewhere in the paper, is by GHCi. The Hugs error message is essentially the same). The typechecker, at the compile time, has detected that the sizes of the vectors to add elementwise do not match. To be more precise, the sizes are off by one.

For vectors described in this section, the element access operation,

vat

, takes O(n) time where n is the size of the vector. Chris Okasaki has designed more sophisticated number-parameterized vectors with element access time O(log n). Although this is an improvement, the overhead of accessing an element adds up for many operations. Furthermore, the overhead of data constructors,

:+:

in our example, becomes noticeable for longer vectors. When we encode the size of a vector as a sequence of data constructors, the latter overhead cannot be eliminated.
Although we have achieved the separation of the shape type of a vector from the type of its elements, we did so at the expense of a sequence of data constructors,

:+:

, at the term level. These constructors add time and space overheads, which increase with the vector size. In the following sections we show more efficient representations for number-parameterized vectors. The structure of their type will still tell us the size of the vector; however there will be no corresponding term structure, and, therefore, no space overhead of storing it nor run-time overhead of traversing it.

6 Encoding the number parameter in type constructors, in unary

To improve the efficiency of number-parameterized vectors, we choose a better run-time representation: Haskell arrays. The code in the present section is in Haskell98.

module UnaryT (..elided..)whereimport Data.Array

First, we need a type structure (an infinite family of types) to encode non-negative numbers. In the present section, we will use an unary encoding in the form of Peano numerals. The unary type encoding of integers belongs to programming folklore. It is also described in Blume01 in the context of a foreign-function interface library of SML.

data Zero = Zero
data Succ a = Succ a

That is, the term

Zero

of the type

Zero

represents the number 0. The term

(Succ (Succ Zero))

of the type

(Succ (Succ Zero))

encodes the number two. We call these numerals Peano numerals because the number n is represented as a repeated application of n type (data) constructors

Succ

to the type (term)

Zero

. We observe a one-to-one correspondence between the types of our numerals and the terms. In fact, a numeral term looks precisely the same as its type. This property is crucial as we shall see on many occasions below. It lets us “lift” number computations to the type level. The property also makes error messages lucid [1].
We place our Peano numerals into a class

determines the predecessor for a positive Peano numeral. The definition for that function may seem puzzling: it is undefined. We observe that the callers do not need the value returned by that function: they merely need the type of that value. Indeed, let us examine the definitions of the method

c2num

in the above two instances. In the instance

Card Zero

, we are certain that the argument of

c2num

has the type

Zero

. That type, in our encoding, represents the number zero, which we return. There can be only one non-bottom value of the type

Zero

: therefore, once we know the type, we do not need to examine the value. Likewise, in the instance

Card (Succ c)

, we know that the type of the argument of

c2num

is

(Succ c)

, where

c

is itself a

Card

numeral. If we could convert a value of the type

c

to a number, we can convert the value of the type

(Succ c)

as well. By induction we determine that

c2num

never examines the value of its argument. Indeed, not only

c2num (Succ (Succ Zero))

evaluates to 2, but so does

c2num (undefined::(Succ (Succ Zero)))

.

The same correspondence between the types and the terms suggests that the numeral type alone is enough to describe the size of a vector. We do not need to store the value of the numeral. The shape type of our vectors could be phantom (as in Blume01).

newtype Vec size a = Vec (Array Int a)derivingShow

That is, the type variable

size

does not occur on the right-hand size of the

Vec

declaration. More importantly, at run-time our

Vec

is indistinguishable from an

Array

, thus incurring no additional overhead and providing constant-time element access. As we mentioned earlier, for simplicity, all the vectors in the paper are indexed from zero. The data constructor

Vec

is not exported from the module, so one has to use the following functions to construct vectors.

We can now introduce functions to operate on our vectors. The functions are similar to those in the previous section. As before, they are polymorphic in the shape of vectors (i.e., their sizes). This polymorphism is expressed differently however. In the present section we use just the parametric polymorphism rather than typeclasses.

returns the element of a vector at a given zero-based index. The function

velems

, which gives the list of vector’s elements, is the left inverse of

listVec

. The function

vzipWith

elementwise combines two vectors into the third one by applying a user-specified function

f

to the
corresponding elements of the argument vectors. The polymorphic types of these functions indicate that the functions generically operate on number-parameterized vectors of any

size

. Furthermore, the type of

vzipWith

expresses the constraint that the two argument vectors must have the same size. The result will be a vector of the same size as that of the argument vectors. We rely on the fact that the function

zipWith

, when applied to two lists of the same size, gives the list of that size. This justifies our use of

listVec'

.
We have introduced two functions that yield the size of their argument vector. One is the function

vlength_t

: it returns a value whose type represents the size of the vector. We are interested only in the type of the return value — which we extract statically from the type of the argument vector. The function

vlength_t

is a compile-time function. Therefore, it is no surprise that its body is

undefined

. The type of the function is its true definition. The function

vlength

in contrast retrieves vector’s size from the run-time representation as an array. If we export

listVec

from the module

UnaryT

but do not export the constructor

Vec

, we can guarantee that

c2num . vlength_t

is equivalent to

vlength

: our number-parameterized vector type is sound.
From the practical point of view, passing terms such as

(Succ (Succ Zero))

to the functions

vec

or

listVec

to construct vectors is inconvenient. The previous section showed a better approach. We can implement it here too: we let the user enumerate the values, which we accumulate into a list, counting them at the same time:

7 Fixed-precision decimal types

Peano numerals adequately represent the size of a vector in vector’s type. However, they make the notation quite verbose. We want to offer a programmer a familiar, decimal notation for the terms and the types representing non-negative numerals. This turns out possible even in Haskell98. In this section, we describe a fixed-precision notation, assuming that a programmer will never need a vector with more than 999 elements. The limit is not hard and can be readily extended. The next section will eliminate the limit altogether.

We again will be using Haskell arrays as the run-time representation for our vectors. In fact, the implementation of vectors is the same as that in the previous section. The only change is the use of decimal rather than unary types to describe the sizes of our vectors.

module FixedDecT (..export list elided..)whereimport Data.Array

Since we will be using the decimal notation, we need the terms and the types for all ten digits:

data D0 = D0
data D1 = D1
...data D9 = D9

For clarity and to save space, we elide repetitive code fragments. The full code is available. To manipulate the digits uniformly (e.g., to find out the corresponding integer), we put them into a class

Digit

. We also introduce a class for non-zero digits. The latter has no methods: we use

The decimal notation is so much convenient. We can now define long vectors without pain. As before, the type of our vectors — the size part of the type — looks precisely the same as the corresponding size term expression:

*FixedDecT> :type v12c
Vec (D1, D2)Char

We can use the sample vectors in the tests like those of the previous section. If we attempt to elementwise add two vectors of different sizes, we get a type error:

The error message literally says that 12 is not equal to 13: the typechecker expected a vector of size 12 but found a vector of size 13 instead.

8 Arbitrary-precision decimal types

From the practical point of view, the fixed-precision number-parameterized vectors of the previous section are sufficient. The imposition of a limit on the width of the decimal numerals — however easily extended — is nevertheless intellectually unsatisfying. One may wish for an encoding of arbitrarily large decimal numbers within a framework that has been set up once and for all. Such
an SML framework has been introduced in Blume01, to encode the sizes of arrays in their types. It is interesting to ask if such an encoding is possible in Haskell. The present section demonstrates a representation of arbitrary large decimal numbers in Haskell98. We also show that typeclasses in Haskell have made the encoding easier and precise: our decimal types are in
bijection with non-negative integers. As before, we use the decimal types as phantom types describing the shape of number-parameterized vectors.

— is a sequence of one digit, digit 9. The application of the constructor

D1

to the latter sequence gives us

D1 (D9 Sz)

, a two-digit sequence of digits one and nine. Compositions of data/type constructors indeed encode sequences of digits. As before, the terms and the types look precisely the same. The compositions can of course be arbitrarily long:

We should point out a notable advantage of Haskell typeclasses in designing of sophisticated type families — in particular, in specifying constraints. Nothing prevents a programmer from using our type constructors, e.g.,

D1

, in unintended ways. For example, a programmer may form a value of the type

However, such types do not represent decimal sequences. Indeed, an attempt to pass either of these values to

ds2num

will result in a type error:

*ArbPrecDecT> ds2num (undefined::D1 Bool)0
No instance for (Digits Bool)
arising from use of `ds2num' at <interactive>:1
In the definition of `it': ds2num (undefined:: D1 Bool)0

In contrast, the approach in Blume01 prevented the user from constructing (non-bottom) values of these types by a careful design and export of value constructors. That approach relied on SML’s module system to preclude the overt mis-use of the decimal type system. Yet the user can still form a (latent, in SML) bottom value of the “bad” type, e.g., by attaching an appropriate type signature to an empty list, error function or other suitable polymorphic value. In a non-strict language like Haskell such values would make our approach, which relies on phantom types, unsound. Fortunately, we are able to eliminate ill-formed decimal types at the type level rather than at the term level. Our class

Digits

admits those and only those types that represent sequences of digits.

To guarantee the bijection between non-negative numbers and sequences of digits, we need to impose an additional restriction: the first, i.e., the major, digit of a sequence must be non-zero. Expressing such a restriction is surprisingly straightforward in Haskell, even Haskell98.

represents non-negative integers. A non-negative integer is realized here as a sequence of decimal digits — provided, as the instances specify, that the sequence starts with a digit other than zero. We can now define the type of our number-parameterized vectors:

newtype Vec size a = Vec (Array Int a)derivingShow

which looks precisely as before, and polymorphic functions

vec

,

listVec

,

vlength_t

,

vlength

,

velems

,

vat

, and

vzipWith

— which are identical to those in Section sec:unary-type. We can define a few sample vectors:

The typechecker complains that 2 is not equal to 3: it found the vector of size 13 whereas it expected a vector of size 12. The decimal types make the error message very clear.

We must again point out a significant difference of our approach from that of Blume01. We were able to state that only those types of digital sequences that start with a non-zero digit correspond to a non-negative number. SML, as acknowledged in Blume01, is unable to express such a restriction directly. The paper, therefore, prevents the user from building invalid decimal sequences by relying on the module system: by exporting carefully-designed value constructors. The latter use an auxiliary phantom type to keep track of “nonzeroness” of the major digit. Our approach does not incur such a complication. Furthermore, by the very inductive construction of the classes

Digits

and

Card

, there is a one-to-one correspondence between types, the members of

Card

, and the integers in decimal notation. In Blume01, the similar mapping holds only when the family of decimal types is restricted to the types that correspond to constructible values. A user of that system may
still form bottom values of invalid decimal types, which will cause run-time errors. In our case, when the digit constructors are misapplied, the result will no longer be in the class

Card

, and so the error will be detected statically by the typechecker:

*ArbPrecDecT> vec (D1$ D0$ D0$ True)0
No instance for (Digits Bool)
arising from use of `vec' at <interactive>:1
In the definition of `it': vec (D1 $(D0 $(D0 $ True)))0*ArbPrecDecT> vec (D0$ D1$ D0 Sz)0
No instance for (Card (D0 (D1 (D0 Sz))))
arising from use of `vec' at <interactive>:1
In the definition of `it': vec (D0 $(D1 $(D0 Sz)))0

9 Computations with decimal types

The previous sections gave many examples of functions such as

vzipWith

that take two vectors statically known to be of equal size. The signature of these functions states quite detailed invariants whose violations will be reported at compile-time. Furthermore, the invariants can be inferred by the compiler itself. This use of the type system is not particular to Haskell: Matthias Blume has derived a similar parameterization of arrays in SML, which can express such equality of size constraints. Matthias Blume however cautions one not to overstate the usefulness of the approach because the type system can express only fairly simple constraints: “There is still no type that, for example, would force two otherwise arbitrary arrays to differ in size by exactly one.” That was written in the context of SML however. In Haskell with common extensions we can define vector functions whose type contains arithmetic constraints on the sizes of the argument and the result vectors. These constraints can be verified statically and sometimes even inferred by a compiler. In this section, we consider the example of vector concatenation. We shall see that the inferred type of

vappend

manifestly affirms that the size of the result is the sum of the sizes of two argument vectors. We also introduce the functions

vhead

and

vtail

, whose type specifies that they can only be applied to non-empty vectors. Furthermore, the type of

vtail

says that the size of the result vector is less by one than the size of the argument vector. These examples are quite unusual and almost cross into the realm of dependent types.
We must note however that the examples in this section require the Haskell98 extension to multi-parameter classes with functional dependencies. That extension is activated by flags

-98

of Hugs and

-fglasgow-exts -fallow-undecidable-instances

of GHCi.
We will be using the arbitrary precision decimal types introduced in the previous section. We aim to design a ‘type addition’ of decimal sequences. Our decimal types spell the corresponding non-negative numbers in the conventional (i.e., big-endian) decimal notation: the most-significant digit first. However, it is more convenient to add such numbers starting from the least-significant digit. Therefore, we need a way to reverse digital sequences, or more precise, types of the class

Digits

. We use the conventional sequence reversal algorithm written in the accumulator-passing style.

We are now ready to build the addition machinery. We draw our inspiration from the computer architecture: the adder of an arithmetical-logical unit (ALU) of the CPU is constructed by chaining of so-called full-adders. A full-adder takes two summands and the carry-in and yields the result of the summation and the carry-out. In our case, the summands and the result are decimal rather than
binary. Carry is still binary.

are type constructors. The functional dependencies of the class tell us that the summands and the input carry uniquely determine the result digit and the output carry. On the other hand, if we know the result digit, one of the summands,

d1

, and the input carry, we can determine the other summand. The same relation

FullAdder

can therefore be used for addition and for subtraction. In the latter case, the carry bits should be more properly called borrow bits.

. The exhaustive enumeration verifies the functional dependencies of the class. The number of instances could be significantly reduced if we availed ourselves to an overlapping instances extension. For generality however we tried to use as few Haskell98 extensions as possible. Although 200 instances seems like quite many, we have to write them only once. We place the instances into a module and separately compile it. Furthermore, we did not write those instances by hand: we used Haskell itself:

That function is ready for Template Haskell. Currently we used a low-tech approach of cutting and pasting from an Emacs buffer with GHCi into the Emacs buffer with the code.

We use

FullAdder

to build the full adder of two little-endian decimal sequences

ds1

and

ds2

. The relation

DigitsSum ds1 ds2 cin dsr

holds if

ds1+ds2+cin = dsr

. We add the digits from the least significant onwards, and we propagate the carry. If one input sequence turns out shorter than the other, we pad it with zeros. The correctness of the algorithm follows by simple induction.

such that the latter is the sum of the formers. The two summands determine the sum, or the sum and one summand determine the other. The class can be used for addition and subtraction of sequences. The dependencies of the sole

CardSum

instance spell out the algorithm. We reverse the summand sequences to make them little-endian, add them together with the zero carry, and reverse the result. We also make sure that the subtraction and summation are the exact inverses. The addition algorithm

DigitsSum

never produces a sequence with the major digit zero. The subtraction algorithm however may result in a sequence with zero major digits, which have to be stripped away, with the help of the relation

is expected to be 8 rather than 9.
We can define other operations that extend or shrink our vectors. For example, Section sec:unary-type introduced the operator

&+

to make the entering of vectors easier. It is straightforward to implement such an operator for decimally-typed vectors.
We must point out that the type system guarantees that

vhead

and

vtail

are applied to non-empty vectors. Therefore, we no longer need the corresponding run-time check. The bodies of

vhead

and

vtail

may safely use unsafe versions of the library functions

head

and

tail

, and hence increase the performance of the code without compromising its safety.

10 Statically-sized vectors in a dynamic context

In the present version of the paper, we demonstrate the simplest method of handling number-parameterized vectors in the dynamic context. The method involves run-time checks. The successful result of a run-time check is marked with the appropriate static type. Further computations can therefore rely on the result of the check (e.g., that the vector in question definitely has a particular size) and avoid the need to do that test over and over again. The net advantage is the reduction in the number of run-time checks. The complete elimination of the run-time checks is quite difficult (in general, may not even be possible) and ultimately requires a dependent type system.

For our presentation we use an example of dynamically-sized vectors: reversing a vector by the familiar accumulator-passing algorithm. Each iteration splits the source vector into the head and the tail, and prepends the head to the accumulator. The sizes of the vectors change in the course of the computation, to be precise, on each iteration. We treat vectors as if they were lists. Most of the vector processing code does not have such a degree of variation in vector sizes. The code is quite simple:

11 Related work

This paper was inspired by Matthias Blume’s messages on the newsgroup comp.lang.functional in February 2002. Many ideas of this paper were first developed during the USENET discussion, and posted in a series of three messages at that time. In more detail Matthias Blume described his method in Blume01, although that paper uses binary rather than decimal types of array sizes for clarity. The approaches by Matthias Blume and ours both rely on phantom types to encode additional information about a value (e.g., the size of an array) in a manner suitable for a typechecker. The paper exhibits the most pervasive and thorough use of phantom types: to represent the size of arrays and the constness of imported C values, to encode C structure tag names and C function prototypes.

However, paper was written in the context of SML, whereas we use Haskell. The language has greatly influenced the method of specifying and enforcing complex static constraints, e.g., that digit sequences representing non-negative numbers must not have leading zeros. The SML approach in Blume01 relies on the sophisticated module system of SML to restrict the availability of value constructors so that users cannot build values of outlawed types. Haskell typeclasses on the other hand can directly express the constraint, as we saw in Section sec:decimal-arb. Furthermore, Haskell typeclasses let us specify arithmetic equality and inequality constraints — which, as admitted in Blume01, seems quite unlikely to be possible in SML.

Arrays of a statically known size — whose size is a part of their type — are a fairly popular feature in programming languages. Such arrays are present in Fortran, Pascal, C [2]. Pascal has the most complete realization of statically sized arrays. A Pascal compiler can therefore typecheck array functions like our

vzipWith

. Statically sized arrays also contribute to expressiveness and efficiency: for example, in Pascal we can copy one instance of an array into another instance of the same type by a single assignment, which, for small arrays, can be fully inlined by the compiler into a sequential code with no loops or range checks. However, in a language without the parametric polymorphism statically sized arrays are a great nuisance. If the size of an array is a part of its type, we cannot write generic functions that operate on arrays of any size. We can only write functions dealing with arrays of specific, fixed sizes. The inability to build generic array-processing libraries is one of the most serious drawbacks of Pascal. Therefore, Fortran and C introduce “generic” arrays whose size type is not statically known. The compiler silently converts a statically-sized array into a generic one when passing arrays as arguments to functions. We can now build generic array-processing libraries. We still need to know the size of the array. In Fortran and C, the programmer must arrange for passing the size information to a function in some other way, e.g., via an additional argument, global variable, etc. It becomes then the responsibility of a programmer to make sure that the size information is correct. The large number of Internet security advisories related to buffer overflows and other array-management issues testify that programmers in general are not to be relied upon for correctly passing and using the array size information. Furthermore, the silent, irreversible conversion of statically sized arrays into generic ones negate all the benefits of the former.

A different approach to array processing is a so-called shape-invariant programming, which is a key feature of array-oriented languages such as APL or SaC. These languages let a programmer define operations that can be applied to arrays of arbitrary shape/dimensionality. The code becomes shorter and free from explicit iterations, and thus more reusable, easier to read and to
write. The exact shape of an array has to be known, eventually. Determining it at run-time is greatly inefficient. Therefore, high-performance array-oriented languages employ shape inference Scholz01, which tries to statically infer the dimensionalities or even exact sizes of all arrays in a program. Shape inference is, in general, undecidable, since arrays may be dynamically allocated. Therefore, one can either restrict the class of acceptable shape-invariant programs to a decidable subset, resort to a dependent-type language like Cayenne, or use “soft typing”. The latter approach is described in Scholz01, which introduces a non-unique type system based on a hierarchy of array types: from fully specialized ones with the statically known sizes and dimensionality, to a type of an array with the known dimensionality but not size, to a fully generic array type whose shape can only be determined at run-time. The system remains decidable because at any time the typechecker can throw up hands and give to a value a fully generic array type. Shape inference of SaC is specific to that language, whose type system is otherwise deliberately constrained: SaC lacks parametric polymorphism and higher-order functions. Using shape inference for compilation of
shape-invariant array operations into a highly efficient code is presented in Kreye. Their compiler tries to generate as precise shape-specific code as possible. When the shape inference fails to give the exact sizes or dimensionalities, the compiler emits code for a dynamic shape dispatch and generic loops.

There is however a great difference in goals and implementation between the shape inference of SaC and our approach. The former aims at accepting more programs than can statically be inferred shape-correct. We strive to express assertions about the array sizes and enforcing the programming style that assures them. We have shown the definitions of functions such as

vzipWith

whose the argument and the result vectors are all of the same size. This constraint is assured at compile-time — even if we do not statically know the exact sizes of the vectors. Because SaC lacks parametric
polymorphism, it cannot express such an assertion and statically verify it. If a SaC programmer applies a function such as

vzipWith

to vectors of unequal size, the compiler will not flag that as an error but will compile a generic array code instead. The error will be raised at run time during a range check.

The approach of the present paper comes close to emulating a dependent type system, of which Cayenne is the epitome. We were particularly influenced by a practical dependent type system of Hongwei Xi Xi98XiThesis, which is a conservative extension of SML. In Xi98, Hongwei Xi et al. demonstrated an application of their system to the elimination of array bound checking and list tag checking. The related work section of that paper lists a number of other dependent and pseudo-dependent
type systems. Using the type system to avoid unnecessary run-time checks is a goal of the present paper too.

C++ templates provide parametric polymorphism and indexing of types by true integers. A C++ programmer can therefore define functions like

vzipWith

and

vtail

with equality and even arithmetic constraints on the sizes of the argument vectors. Blitz++ was the first example of using a so-called template meta-programming for generating efficient and safe

array code. The type system of C++ however presents innumerable hurdles to the functional style. For example, the result type of a function is not used for the overloading resolution, which significantly restricts the power of the type inference. Templates were introduced in C++ ad hoc, and therefore, are not well integrated with its type system. Violations of static constraints expressed via
templates result in error messages so voluminous as to become incomprehensible.

McBride gives an extensive survey of the emulation of dependent type systems in Haskell. He also describes number-parameterized arrays that are similar to the ones discussed in Section sec:Okasaki. The paper by Fridlender and Indrika shows another example of emulating dependent types within the Hindley-Milner type system: namely, emulating variable-arity functions such as generic

zipWith

. Their technique relies on ad hoc codings for natural numbers which resemble Peano numerals. They aim at defining more functions i.e., multi-variate functions), whereas we are concerned with making functions more restrictive by expressing sophisticated invariants in functions’ types. Another approach to multivariate functions — multivariate composition operator — is discussed in mcomp.

12 Conclusions

Throughout this paper we have demonstrated several realizations of number-parameterized types in Haskell, using arrays parameterized by their size as an example. We have concentrated on techniques that rely on phantom types to encode the size information in the type of the array value. We have built a family of infinite types so that different values of the vector size can have their own distinct type. That type is a decimal encoding of the corresponding integer (rather than the more common unary, Peano-like encoding). The examples throughout the paper illustrate that the decimal notation for the number-parameterized vectors makes our approach practical.

We have used the phantom size types to express non-trivial constraints on the sizes of the argument and the result arrays in the type of functions. The constraints include the size equality, e.g., the type of a function of two arguments may indicate that the arguments must be vectors of the same size. More importantly, we can specify arithmetical constraints: e.g., that the size of the vector after concatenation is the sum of the source vector sizes. Furthermore, we can write inequality constraints by means of an implicit existential quantification, e.g., the function

vhead

must be applied to a non-empty vector. The programmer should benefit from more expressive function signatures and from the ability of the compiler to statically check complex invariants in all applications of the vector-processing functions. The compiler indeed infers and checks non-trivial constraints involving addition and subtraction of sizes — and presents readable error messages on violation of the constraints.