Abstract. When type parameters don't need to be named, Dafny has a scheme for filling them in automatically. Omitting the type parameters reduces clutter in function signatures. This note describes how the type parameters get filled in.
As in many languages, types in Dafny can be parameterized by other types.
For example, the type set<int>
denotes finite sets of integers and is
parameterized by int
. As another example, if List
is a type defined as
datatype List<A> = Nil | Cons(A, List<A>)
and Expr
is a type denoting expressions, then List<Expr>
is a type that is parameterized by Expr
. In the declaration of List
,
A
is a formal type parameter, and in the type List<Expr>
, Expr
is
the actual type parameter (or, actual type argument).
Since set
and List
each requires a type parameter, they are not
themselves types. That is, it doesn't make sense for a variable to have
type set
. We say that set
and List
are type constructors. More
precisely, they are unary type constructors, since they take one type
parameter. Each type constructor has an arity, which says how many type
parameters it needs. So, a unary type constructor has arity 1. (There
are no type constructors with arity 0; such a thing is simply a type.)
In Dafny, type constructors are not first class. This means that every mention of a type constructor must always be fully instantiated. However, the instantiation need not be explicit in the program text.
In the signature declarations of datatypes, functions, and methods, Dafny allows type constructors to be used without explicit type parameters. In this note, I describe Dafny's scheme for filling in those missing type parameters.0
Let's start by looking at two examples that show the advantages of having such a scheme.
Here are functions for computing the length of a list and for computing the set of elements in a list.
function method Length(list: List): nat {
match list
case Nil => 0
case Cons(_, tail) => 1 + Length(tail)
}
function method Elements(list: List): set {
match list
case Nil => {}
case Cons(x, tail) => {x} + Elements(tail)
}
At first glance, they look to be exactly what you expect. But if you
think some more, you will notice that List
and set
have no given type
parameters. This is delightful! Both Length
and Elements
have more
to do with the structure of the list than with the types of the elements
stored in the list, and therefore it's nice that the definitions of
these functions don't need to be cluttered up with the declaration
and uses of the type parameters.
If you wrote the type parameters explicitly, the signatures of the functions above would look like this:
function method Length<A>(list: List<A>): nat
function method Elements<A>(list: List<A>): set<A>
You'll agree this is rather heavy on the <A>
's.1
So that you can use these abridged signatures effectively, let's now look at how omitted type parameters are filled in.
In the type signatures of functions and methods and in the right-hand side of
(the "=
" in) datatype definitions, type parameters can be omitted.
The rule is that if the type signature or
right-hand side mentions a type constructor without any type
arguments, then Dafny will fill these in from a
prefix of the formal type parameters of the enclosing function, method,
or datatype.
Suppose the program text includes the following function signature:
function method ReverseAux<A
>(list: List<A>, acc: List): List<A>
<A>
Note that List
in the type of acc
does not have an explicit type
argument. Dafny now fills in the type parameter of List
using the type
parameter of function ReverseAux
, completing the type of acc
List<A>
,
as illustrated by the arrow above.2
The type arguments to the other occurrences of List
in the function's type signature
can be filled in in the same manner. So, the same function ReverseAux
can
be declared simply as:
function method ReverseAux<A
>(list: List, acc: List): List
<A>
<A>
<A>
Consider a datatype Path
with two type parameters:
datatype Path<L,R> = Stop | Left(L, Path<L,R>) | Right(R, Path<L,R>)
A value of type Path<L,R>
represents a finite path of left- and right-turns.
Each left-step is accompanied by a value (call it a “breadcrumb”, if you wish)
of type L
and every right-step is accompanied by a breadcrumb of type R
.
Here are two functions on paths. Hansel
picks up all the breadcrumbs
encountered on left turns and Gretel
picks up all the breadcrumbs
encountered on right turns.
function Hansel<L,R>(p: Path<L,R>): List<L> {
match p
case Stop => Nil
case Left(l, rest) => Cons(l, Hansel(rest))
case Right(_, rest) => Hansel(rest)
}
function Gretel<L,R>(p: Path<L,R>): List<R> {
match p
case Stop => Nil
case Left(_, rest) => Gretel(rest)
case Right(r, rest) => Cons(r, Gretel(rest))
}
Using Dafny's type-parameter completion, we can write these as:
datatype Path<L,
R> = Stop | Left(L, Path) | Right(R, Path)
function Hansel<L,
R>(p: Path): List
function Gretel<L,
R>(p: Path): List<R>
<L,R>
<L,R>
<L,R>
<L>
<L,R>
Note that both occurrences of Path
in the right-hand side of the declaration
of type Path
get both type arguments filled in with the type's type
parameters <L,R>
given in the left-hand side of the declaration. In both of
the functions, Path
also gets the arguments <L,R>
as declared
in the functions. Moreover, for Hansel
, type List
automatically gets the
argument <L>
, because List
needs one argument and therefore it only picks
up a prefix of the function's type parameters <L,R>
. Function Gretel
has result type List<R>
. Here, it is not possible to elide the type
argument list <R>
, since <R>
is not a prefix of <L,R>
.
The example illustrates how type-parameter completion picks a prefix of
the enclosing function's type parameters. This allows you to write just
List
instead of List<L>
. However, since L
and R
otherwise play
symmetric roles in this example, you can of course also choose to write
out List<L>
explicitly. You may argue that doing so more clearly highlights
the difference between functions Hansel
and Gretel
. You would then
write the following signatures:
datatype Path<L,R> = Stop | Left(L, Path) | Right(R, Path)
function Hansel<L,R>(p: Path): List<L>
function Gretel<L,R>(p: Path): List<R>
which still looks pretty clean.
Type-parameter completion applies not just to the outermost types. For example,
datatype Tree<A> = Node(children: List<Tree<A>>)
can also be written as
datatype Tree<A
> = Node(children: List<Tree>)
<A>
Note that if you supply any type parameters at all, then Dafny will not change or complete the list you have given. The completion rule supplies the parameters to a type constructor only if the type constructor is mentioned with no type arguments at all (that is, without any angle brackets).
Type-parameter completion also applies in the right-hand side of datatypes.
So, if you want to, you can declare List
as follows:
datatype List<A> = Nil | Cons(A, List)
What I described so far pertains to the actual type arguments of type constructors in signatures. There's one more part to Dafny's type-parameter completion. For functions and methods, but not for datatypes, if the function or method is declared without explicit formal type parameters, then Dafny provides these as well. This list of formal type parameters in this completion will be made long enough to cover the length of any missing type-argument list described in Section 2 above.
As we saw in Section 2.0 above,
function ReverseAux
can omit the uses of its type parameter A
.
Consequently, there is no need for the signature to mention A
by name.
Dafny's type-parameter auto-declaration rule allows us to omit A
altogether. By doing so, we can declare ReverseAux
simply as
function method ReverseAux(list: List, acc: List): List
<A>
<A>
<A>
This declaration says that ReverseAux
takes two lists and returns a list.
There's no need to explicitly mention the type of list elements, and it
is tacitly understood that the three lists mentioned have the same type
of elements.
We already saw two other examples in the motivational Section 3.0:
Length
and Elements
. Function Elements
relies on type-parameter
completion in three ways: both the user-defined type constructor List
and the built-in type constructor set
are completed with the same
type parameter and this (un-named) type parameter is added to the formal
type parameters of the function.
function method Elements(list: List): set
<A>
<A>
Note that you cannot take advantage of the auto-declaration part of
type-parameter completion if you have a need to mention
the type parameter. For example, you can shorten the signature of Snoc
to:
function method Snoc<A>(list: List, a: A): List {
match list
case Nil => Cons(a, Nil)
case Cons(x, tail) => Cons(x, Snoc(tail, a))
}
But since a
's type needs to be mentioned as A
, you must declare
A
explicitly. That is, you cannot abbreviate Snoc
's type signature
more than this.
Actual type parameters are completed in the right-hand side of
datatype declarations, but auto-declaration of formal type parameters
does not apply to datatypes. For example, you cannot omit the
<A>
in the left-hand side of the declaration of List
.
Recall that if you supply any type parameters at all, then Dafny will not extend the list you have given. The completion rule will supply type parameters to a function or method only if the function or method is declared with no type parameters at all (that is, without any angle brackets).
Finally, remember that Dafny's type-parameter completion is a feature, not a requirement. If you don't want to make use of this completion, then by all means, feel free to write out all formal type parameters and actual type arguments explicitly in your own programs.
0.Dafny's scheme for filling in type parameters in signature declarations is one of two mechanisms that the language employs to reduce type clutter in the program text. The other mechanism is type inference, which tries to figure out types of variables and expressions based on how these are used. In this Power note, I will not talk about type inference. ↩
1.The compiled function Elements
makes use of one other thing that
Dafny fills in automatically, namely the fact that the argument to
set
needs to be a type that supports equality in compiled contexts.
If you wrote this explicitly, the function signature would be
function method Elements<A(==)>(list: List<A>): set<A>
In this note, I won't say more about equality-supporting types or how Dafny tries to infer these. ↩
2.The Dafny IDE in Visual Studio displays type information in tool tips.
If you hover the mouse over acc
in this IDE, the tool tip that pops
up will tell you the full type of acc
, namely List<A>
.
In the future, the other Dafny IDEs may also show such type tool-tips.
↩