Type-Safe Cast

Type-Safe Cast

Type-Safe Cast ∗ Stephanie Weirich Department of Computer Science Cornell University Ithaca, NY 14853 [email protected] ABSTRACT and retrieving items from the table. In a syntax resembling In a language with non-parametric or ad-hoc polymorphism, Standard ML [10], this interface is it is possible to determine the identity of a type variable at run time. With this facility, we can write a function to con- sig vert a term from one abstract type to another, if the two type table hidden types are identical. However, the naive implementa- val empty : table tion of this function requires that the term be destructed and val insert : ∀α. table → (string × α) → table rebuilt. In this paper, we show how to eliminate this over- val find : ∀α. table → string → α head using higher-order type abstraction. We demonstrate end this solution in two frameworks for ad-hoc polymorphism: intensional type analysis and type classes. However, looking at the type of find reveals something odd: Categories and Subject Descriptors If this polymorphic function behaves parametrically with re- spect to α, that is it executes the same code regardless of D.3.3 [Programming Languages]: Language Constructs the identity of α, there cannot possibly be any correct im- and Features—abstract data types, polymorphism, control plementation of find [15]. All implementations must either structures; F.3.3 [Logics and Meanings of Programs]: diverge or raise an exception. Let us examine several possi- Software—type structure, control primitives, functional con- ble implementations to see where they go wrong. structs We will assume that the data structure used to implement General Terms the symbol table is not the issue, so for simplicity we will Languages use an association list. Keywords val empty = [] Ad-hoc polymorphism, dynamic typing, intensional type anal- ysis, type classes Given a string and a list, the following version of find iter- ates over the list looking for the matching string: 1. THE SETUP Suppose you wanted to implement a heterogeneous symbol table — a finite map from strings to values of any type. let rec find = Λα. You could imagine that the interface to this module would fn table => fn symbol => declare an abstract type for the table, a value for the empty case table of table, and two polymorphic functions for inserting items into [] => raise NotFound | ( (name, value) :: rest ) => ∗This paper is based on work supported in part by the Na- if symbol = name then value tional Science Foundation under Grant No. CCR-9875536. else find[α] rest symbol Any opinions, findings and conclusions or recommendations expressed in this publication are those of the author and do not reflect the views of this agency. Note that, unlike in SML, we make type application explicit in a manner similar to the polymorphic lambda calculus or System F [4, 14]. The notation Λα abstracts the type α and find[α] instantiates this type for the recursive call. Unfortunately, this function is of type ∀α. (string × α) list → string → α. ICFP ’00 Montreal, Quebec Canada If we were looking up an int, the list passed to find could only contain pairs of strings and ints. We would not be able to store values of any other type than int in our symbol This language is interpreted by a type-passing semantics. table. In other words, at run time a type argument is passed to a type abstraction of type ∀α.τ, and can be analyzed by The problem is that, in a statically typed language, all el- typecase. Dually, when we create an object of an existen- ements of a list need to have the same type: It would be tial type, ∃α.τ, the hidden type is included in the package, incorrect to form an association list [ ("x", 1) ; ("y", and when the package is opened, α may also be examined. ML (2,3)) ]. As we do not want to constrain the symbol table In λi , universal and existential types have different prop- to contain only one type, we need to hide the type of the erties from a system with a type-erasure semantics, such as value for each entry in the list. One possibility is to use an the polymorphic lambda calculus. In a type-erasure system, existential type [12]. The instruction types may have no effect on run-time execution and there- fore may be erased after type checking. There, ∀α.α is an empty type (such as void), and ∃β.β is equivalent to the ∃β.τ τ 0 ML pack v as hiding singleton type unit. However, in λi , ∀α.α is not empty, as we can use typecase to define an appropriate value for each 0 type, and ∃β.β is the implementation of type Dynamic, as coerces a value of type τ with τ substituted for β into one we can use typecase to recover the hidden type. of type ∃β.τ. Conversely, the instruction ML In λi , a simple function, sametype, to compare two types and return true if they match, can be implemented with unpack (β,x) = e in e’ nested typecases. The outer typecase discovers the head normal form of the first type, and then the inner typecase 1 destructs an existential package e of type ∃β.τ, binding a compares it to the head normal form of the second. For new type variable β and a new term variable x of type τ product and function types, sametype calls itself recursively within the expression e’. on the subcomponents of the type. Each of these branches binds type variables (such as α1 and α2) to the subcompo- Therefore, we can create a symbol table of type (string × nents of the types so that they may be used in the recursive ∃β.β) list with the expression call. [ ("x", pack 1 as ∃β.β hiding int) ; ("y", pack (2,3) as ∃β.β hiding int × int ] let rec sametype = Λα. Λβ. So the code for insert should just package up its argument typecase (α) of and cons it to the rest of the table. int => typecase (β) of val insert = int => true Λα. fn table => fn (symbol, obj) => | => false (symbol, pack obj as ∃β.β hiding α) :: table | (α1 × α2)=> typecase (β) of (β1 × β2) => However, the existential type has not really solved the prob- sametype[α1][β1] lem. We can create the list, but what do we do when we look andalso sametype[α2][β2] up a symbol? It will have type ∃β.β, but find must return | => false a value of type α. If we use the symbol table correctly, then | (α1 → α2) => α and β will abstract the same type, but we need to verify typecase (β) of this property before we can convert something of type β to (β1 → β2) => type α. We can not compare α and β and still remain para- sametype[α1][β1] metric in α and β. Therefore, it seems as though we need andalso sametype[α2][β2] a language that supports some sort of non-parametric poly- | => false morphism (also called run-time type analysis, overloading, or “ad hoc” polymorphism). Formulations of this feature include Haskell type classes [16], type Dynamic [1, 7, 9], ex- tensional polymorphism [3], and intensional polymorphism [6]. As these nested typecases are tedious to write, we borrow from the pattern matching syntax of Standard ML, and ab- For now, we will consider the last, in Harper and Morrisett’s breviate this function as: ML language λi . This language contains the core of SML plus an additional typerec operator to recursively examine un- known types at run time. For simplicity, we will separate recursion from type analysis and use the operator typecase to discriminate between types, and rec to compute the least 1For brevity, we only include int, product types, and func- fixed point of a recursive equation. tion types in the examples. let rec sametype = cast : ∀α. ∀β. α → β Λα. Λβ. let rec cast = typecase (α, β) of Λα. Λβ. (int,int) => true typecase [δ , δ . δ → δ ](α, β) of α × α β × β 1 2 1 2 | ( 1 2, 1 2) => (int, int) => α β sametype[ 1][ 1] fn (x:int) => x α β andalso sametype[ 2][ 2] | (α × α , β × β ) => α → α β → β 1 2 1 2 | ( 1 2, 1 2) => let val f = cast [α ][β ] α β 1 1 sametype[ 1][ 1] val g = cast [α ][β ] α β 2 2 andalso sametype[ 2][ 2] in | ( , ) => false fn (x:α1 × α2) => (f (fst x), g (snd x)) end However, though this function does allow us to determine | (α1 → α2, β1 → β2) => if two types are equal, it does not solve our problem. In let val f = cast [β1][α1] fact, it is just about useless. If we try to use it in our val g = cast [α2][β2] implementation of find in fn (x:α1 → α2) => g ◦ x ◦ f let rec find = end Λα. fn table => fn symbol => | ( , ) => raise CantCast case table of [] => raise NotFound | ( (name, package) :: rest ) => Figure 1: First Solution unpack (β,value) = package in if symbol = name andalso sametype[α][β] typecase [γ.γ → γ] τ of then value int => fn (x:int) => x + 3 else find[α] rest symbol | ... we discover that this use does not type check. The return In later examples, when we use the pattern matching syntax type for find is the existentially bound β which escapes its for two nested typecases, we will need the schema to have scope. Even though we have added a dynamic check that two free type variables. α is equivalent to β, the check does not change the type of value from β to α.

View Full Text

Details

  • File Type
    pdf
  • Upload Time
    -
  • Content Languages
    English
  • Upload User
    Anonymous/Not logged-in
  • File Pages
    10 Page
  • File Size
    -

Download

Channel Download Status
Express Download Enable

Copyright

We respect the copyrights and intellectual property rights of all users. All uploaded documents are either original works of the uploader or authorized works of the rightful owners.

  • Not to be reproduced or distributed without explicit permission.
  • Not used for commercial purposes outside of approved use cases.
  • Not used to infringe on the rights of the original creators.
  • If you believe any content infringes your copyright, please contact us immediately.

Support

For help with questions, suggestions, or problems, please contact us