Submitted to the Workshop on Programming Language Technologies for XML
Type isomorphisms simplify XML programming Frank Atanassow and Johan Jeuring Institute of Information & Computing Sciences Utrecht University The Netherlands
{franka,johanj}@.cs.uu.nl
ABSTRACT
Why use a data binding? XML data binding has taken the XML world by storm. A look at the alternatives can help explain why.
A program that processes XML documents can be implemented using an XML data binding. Programming with an XML data binding can be painful, because the classes or types generated by the data binding are not in the style native to the host language. JAXB, a Java-XML data binding, supports binding customizations to mitigate this problem. Programming with a type-safe binding such as our HaskellXML data binding is even worse. To address this problem, rather than employing binding customizations, we exploit the theory of type isomorphisms to generically produce conversions between the complex datatypes generated by our binding, and more conventional Haskell-style datatypes. We give a Generic Haskell program that produces, for many pairs of canonically isomorphic types, the requisite pair of isomorphisms. This approach obviates the practical need for binding customizations and simplifies XML programming with a type safe Haskell-XML binding. Furthermore, a straightforward generalization of this technique allows us to treat schema subtyping in a simple way.
1
• The DOM API represents an XML document as a value of a universal tree type. Consequently, it is very easy to construct an invalid XML document, which will only be reported when you validate the constructed element. In contrast, data binding effectively translates documents to a typed tree, so a type-safe translation can co-opt the host language’s static type system to eliminate many validation errors. Like programs based on data binding, though, DOM applications can also suffer from high memory requirements because they typically keep the whole document in memory at once. • The SAX API effectively represents a document as a stream of lexemes. Although, compared to DOM, this can decrease the memory requirements of some applications, programs which use this low-level approach tend to mingle code for parsing an XML document and code for ‘business logic’. Consequently SAX-based programs are hard to implement, test, and maintain. • Special-purpose programming languages for XML, such as the W3C’s XSLT [27], XDuce [11], Yatl [6], XMλ [17, 23], SXSLT [12], Xtatic [9], etc., usually lack optimising compilers, debuggers, programming environments, etc. Furthermore, few users are willing to learn a new programming language when they can use one with which they already have expertise.
INTRODUCTION
A program that processes XML documents can be implemented using an XML data binding. An XML data binding [18] translates an XML document to a value of some programming language. Such bindings have been defined for a number of programming languages including Java [16, 19], Python [20], Prolog [7] and Haskell [26, 28, 1].
Binding customization. To use an XML data binding for a particular programming language, a programmer must know how an XML document is translated to a value in the programming language. For example, is a date element translated to a new abstract type Date derived from the schema structure, an extant (legacy) type such as a localized date type Date UK, a ternary product of Int, or perhaps even a nested pair of binary products? Does the order of the date components follow the document order, or are they permuted and if so how? Ideally, a programmer can specify how an XML document is to be bound to a value. A binding customization specifies how an XML document is translated; it provides a mechanism for influencing and customizing the translation. Often this customization is given in the form of another XML document which conforms to a binding schema. (The term
c
-Notice
1
‘XML binding schema’ is also often abused to refer to the customizations themselves, rather than their format; we will avoid this misnomer.)
above into: EQ E doc (E doc (Elem () (EQ T docType (T docType (Seq (A key (Attr (EQ T string (T string "homer-iliad")))) (Seq (Rep (ZI [EQ E author (E author (Elem () (EQ T string (T string "Homer"))))])) (Seq (EQ E title (E title (Elem () (EQ T string (T string "The Iliad"))))) (Rep (ZS Nothing (Rep ZZ ))))))))))
A binding customization assumes the existence of a schema (or DTD) for an input XML document, and specifies for example how a schema type is translated to a class (in Java) or a datatype (in Haskell). XML binding tools which do not support binding customization may force the programmer to master the internals of a tool in order to understand how a document is translated. For example, how does the tool treat a choice, or a sequence of elements?
Conflicting issues in UUXML. UUXML’s usability issues are a side effect of several of its design goals. First, UUXML is type-safe and preserves as much static type information as possible to eliminate the possibility of constructing invalid documents. In contrast, Java-XML bindings tend to ignore a great deal of type information, such as the types of repeated elements (probably largely due to the limitations of Java collections). Second, UUXML translates (a sublanguage of) XML Schema types rather than the less expressive DTDs. This entails additional complexity compared with bindings such as HaXML [28] that merely target DTDs. For example, Schema2 supports not just one but two distinct notions of subtyping and a more general treatment of mixed content than DTDs. Third, the UUXML translation closely follows the Model Schema Language (MSL) formal semantics [4], even going so far as to replicate that formalism’s abstract syntax as nearly as Haskell’s type syntax allows. This has advantages: we have been able to prove the soundness of the translation, that is, that valid documents translate to typeable values, and the translator is relatively easy to correctly implement and maintain. However, our strict adherence to MSL has introduced a number of ‘dummy constructors’ and ‘wrappers’ which could otherwise be eliminated. Fourth, as we shall explain later, since Haskell does not directly support subtyping and Schema does, our binding tool emits a pair of Haskell datatypes for each schema type t: an ‘equational’ variant which represents documents which validate exactly against t, and a ‘down-closed’ variant, which represents all documents which validate against all subtypes of t. Our expectation was that a typical Haskell user would read a document into the down-closed variant, patternmatch against it to determine which exact/equational type was used, and do the bulk of their computation using that. Finally, the last reason for UUXML’s complexity is that it was intended to support the development of ‘schema-aware’ XML applications using Generic Haskell. This moniker describes programs, such as our XML compressor XComprez [1], which operate on documents of any schema, but not necessarily parametrically—XComprez, for example, exploits the type information of a schema to improve compression ratios. Because Generic Haskell works by traversing the structure of datatypes, we could not employ methods, such as those in WASH [26], which encode schema information in nonstructural channels such as Haskell’s type class system.
Lastly, the default translation scheme of a data binding may produce unwieldy, convoluted types and values. Our own Haskell-XML binding, called UUXML1 [1], suffers from this problem.
1.1
The problem with UUXML
We will describe UUXML in more detail later, but let us briefly give the reader a sense of the magnitude of the problem. Consider the following XML schema, which describes simple bibliographic records.
An example document which validates against this schema is: Homer The Iliad
Our binding tool translates the type doc into LE E doc— we elide its definition for now—and the example document 1
2
We write ‘Schema’ to denote the W3C XML Schema Recommendation or type system, and ‘schema’ to denote the entities described in that Recommendation, that is, (sets of) types themselves.
Hardcore advocates may pronounce this “Use XML!”
Frank Atanassow and Johan Jeuring, October 30, 2003
2
PLAN-X
1.2
Exploiting isomorphisms to make UUXML usable
thors named “Dubya”, and writes the result to standard output.
UUXML is not a failed experiment, though advocates of untyped languages might be quick to label it so. Though UUXML is burdensome for non-generic programs, it has nevertheless proved useful and tractable for developing schemaaware applications in Generic Haskell (GH). Part of the reason can be attributed to the nature of generic programs—they rarely need to explicitly deconstruct deeply nested values such as our example document above. However, another reason can be discerned in the way that GH facilitates identifying structurally equivalent datatypes. This observation, elaborated below, adumbrates our solution to UUXML’s usability problem. Let us consider what our ideal translation target might look like. Here is an obvious, very conventional, Haskellstyle translation image of doc: data Doc = Doc {key :: authors :: title :: pubDate ::
1.3
= =
toDoc
=
work
=
interact work unparse{|E doc|} . expand {|E doc|} . reduce{|Doc|} expand {|Doc|} . reduce{|E doc|} . parse{|E doc|} toE doc . (λd → d {authors = filter (6≡ "Dubya") (authors d )}) . toDoc
The reduce/expand paradigm
The type signatures of the generic functions involved are: parse{|t|} unparse{|t|} reduce{|t|} expand {|t|}
String, [String], String, Maybe PubDate}
:: :: :: ::
String → t t → String t → Univ Univ → t
The first two are just generic functions which unmarshal and marshal an XML document from a character stream. The next two form the heart of this paper. reduce{|t|} takes any type and converts it into a universal, normalized representation denoted by the type Univ; expand {|t0 |}, its dual, converts such a universal value back to a ‘regular’ value, if possible. If t = t0 , then expand {|t|} and reduce{|t|} are mutual inverses. If t and t0 are merely isomorphic, then expansion may fail; it always succeeds if the two types are canonically isomorphic, t ∼ = t0 , a congruence which is characterized by the following algebraic theory.
data PubDate = PubDate {year :: Integer, month :: Integer} Observe in particular that: • the target types Doc and PubDate have Haskellish names; • the fields are typed by conventional Haskell datatypes like String, lists and Maybe; • the difference between the attribute key and other elements has been extinguished; and • intermediate ‘wrapper’ elements like title and year have been elided and do not generate new types; • the positional information encoded in wrappers is available in the field projection names; • the field authors is not the same as the element name author, which is appropriate since authors projects a list whereas each author tag wraps a single author.
∼ = ∼ =
a :*: Unit Unit :*: a (a :*: b) :*: c a :+: Zero Zero :+: a (a :+: b) :+: c
a a
∼ = ∼ =
a
∼ = ∼ =
a :+: (b :+: c)
a :*: (b :*: c) a
The first two laws express the fact that the nullary product Unit is a left and right identity for binary product :*:; the next says that :*: is associative. The next three assert the analogous laws for Zero, the empty sum, and binary sum :+:. The functions witnessing these identities are the obvious ones; indeed, they are the only total functions of that type, when a, b and c are read as polymorphic type variables. As an example, consider the following expression:
Achieving an analogous result in Java with JAXB would require annotating (editing) the source schema directly, or writing a binding customization file which uses XPath syntax to denote the parts which require customization—a maintenance hazard since the schema structure may change. With our proposed system, provided that the document is known to be exactly of type doc and not a proper subtype, all that is required is the above Haskell declaration plus the following modest incantation:
(expand {|(Bool, Bool :+: (Int :+: String))|} . reduce{|(Bool, ((), (Bool :+: Int) :+: String))|}) (True, ((), Inl (Inr 7)))
expand {|Doc|} . reduce{|E doc|}
which evaluates to:
This expression denotes a function of type:
(True, Inr (Inl 7))
E doc → Doc
Our functions also implement two additional coercions which are not invertible:
which converts UUXML’s unwieldy representation of doc into the idealized form above. For example, the following GH program reads in a docconforming document from standard input, deletes all auFrank Atanassow and Johan Jeuring, October 30, 2003
main toE doc
3
a :*: b
→
a
a
→
a :+: b PLAN-X
Generic Haskell of the reduce{|t|} and expand {|t|} functions just discussed. Finally, in the last section we discuss conclusions and related and future work.
The canonical witnesses here are the first projection of a product and the left injection of a sum. Thanks to these reductions, the following expression: (expand {|Either Bool Int|} . reduce{|(Bool, Int)|}) (True, 4)
2
evaluates to Left True; note that it cannot evaluate to Right 4 because such a reduction would involve projecting a suffix and injecting into the right. Now let us look at some examples which fail. This conversion:
JAXB [19] is a popular3 XML data binding tool for Java which maps a schema into Java by translating each type as an interface. From the doc schema described earlier, JAXB produces Java code of approximately the following form:
expand {|(Bool, Int)|} . reduce{|(Int, Bool)|}
public interface Doc extends Element, DocType {} public interface DocType { Key getKey(); Year getYear(); List getAuthor(); Title getTitle(); } public interface Title { String getValue(); void setValue(); } public interface Year { BigInteger getYear(); void setYear(BigInteger value); } ...
fails because our theory does not include commutativity of :*:. The next conversion fails because the types are not isomorphic or coercible: expand {|Bool|} . reduce{|Int|} The conversion: expand {|Bool|} . reduce{|Either () ()|} fails because we chose to represent certain base types like Bool as abstract: they are not destructured when reducing. Understanding canonical isomorphisms. Types which fall into the same isomorphy class can be regarded as mutually representable: if t ∼ = t0 then t can be used 0 to implement t and vice versa. Such isomorphisms are a special case of what are typically called coercions or views—special because they are invertible and entail no loss of information. Canonical isomorphisms are more special yet because they are determined uniquely by the types involved and furthermore satisfy a set of properties called coherence conditions, which ensure that algebraic manipulation of the types, in particular, substitution, always induces a unique canonical iso witness. Consider for example the type Double. An archetypal example of a view between this type and itself is the one which converts feet to meters. This function is invertible, but not canonical; the only canonical iso of type Double → Double is the identity. Unfortunately, for space reasons we cannot delve deeper into the significance of coherence. We refer the interested reader to Mac Lane’s classic treatment [14], or any number of other categorical texts, for example Mac Lane’s own book [15]. What our function reduce{|t|} does is pick a type in each isomorphy class which serves as a normal form, and use the canonical witness to convert values of t to that form. For technical reasons, these values are represented in a special way in the abstract type Univ, whose internals a typical user need not understand unless expand {|t0 |} fails. If t and t0 are ‘essentially’ the same, yet concretely substantially different, as is the case for Doc and E doc, then this automatic conversion can save the user a lot of typing, time and effort.
along with class implementations which can read and write (‘unmarshal’ and ‘marshal’) XML documents conforming to the schema. The implementations also include a validation method which verifies that an object can be written to a schema-conforming XML document. There are a few points worth noting about this translation: • Each child element is associated with its parent via a get method, and also a set method if the type is immutable like String. • Primitive types like string and integer are mapped to existing Java types like String and BigInteger, while user-defined types like doc and title generate new types like Doc and Title. • When maxOccurs is greater than one, the repeated element is translated as a List. This entails some loss of static type information, since the fact that the list members are typeable as Author is not expressible in Java. • Optional elements like year are translated in the same way as mandatory elements since all object references in Java may have the special value null. By default, JAXB does not produce exactly the code above, but rather something a bit more convenient: public interface DocType { String getKey(); void setKey(String value);
Outline of paper. The rest of this paper is organized as follows. In the next section we examine JAXB, a popular data binding tool for Java and criticize some of its weaknesses. In section 3, we take a brief but closer look at our own UUXML binding and how it represents XML documents. Next, in section 4 we describe the implementation in Frank Atanassow and Johan Jeuring, October 30, 2003
A JAVA-XML BINDING
3
JAXB was a finalist in the 2003 JavaWorld Editors’ Choice Awards for “Best Java-XML” tool—it was also the only data binding tool among the finalists. 4
PLAN-X
2.2 BigInteger getYear(); void setYear(BigInteger value);
Limitations of JAXB
Although binding files allow the user to customize the JAXB translation, the mechanism is limited in a number of ways.
List getAuthor();
• Some parts of the static type information available in the schema are lost in the translation. For example, repeated elements are translated simply as Lists and optional elements are treated in the same way as mandatory ones by using the fact that every Java object reference can be null. Also, with some configuration options, JAXB maps choices to the Object type.
String getTitle(); void setTitle(String value); }
This translation includes a few optimizations; for example, the title type is translated to a String rather than an interface because String would be the translation of title’s content. Although this sort of optimization slightly complicates the correspondence with input schemas, it makes the translation more wieldy. In this case it avoids forcing the programmer to perform an extra indirection; for example, thanks to this optimization, rather than write:
• One can only rename/regroup types which are grouped together by the concrete syntax of the schema. • One can only map schema types to user-defined implementations, and not interfaces, except for simple types. • To use a legacy (i.e., extant) class to implement a schema type, the user must first extend it (using inheritance) to add methods which (un)marshal the XML data, using the JAXB runtime.
String quotedTitle(Doc d) { Title t = d.getTitle(); return "\"" + t.getValue() + "\""; }
Except for the loss of static type information, which it assiduously seeks to avoid, UUXML also suffers from many of the same problems, and more. These issues have motivated our work on the type isomorphism approach of this paper.
the programmer simply writes: String quotedTitle(Doc d) { return "\"" + d.getTitle() + "\""; }
2.1
3
Binding customization
In this section we very briefly outline UUXML, a data binding between Haskell and XML; more (informal and formal) details of the translation are available in another paper [1]. We know of two other Haskell-XML data bindings, HaXML [28] and WASH [26], both of which employ DTDs as their XML-side type formalism. HaXML translates DTD types to algebraic datatypes in a fairly structural fashion; WASH instead exploits Haskell’s type class system to encode the state machine corresponding to a tree parser. UUXML more closely resembles HaXML as generalized to the setting of XML Schema.
It is possible to influence JAXB’s translation by creating a “binding file”. (Customization can also be performed by adding annotations directly in a schema file.) For example, if we want the JAXB-generated interface for doc to be called Document rather than Doc, we can include a declaration of this sort:
Using the same mechanism, one can also regroup fields to a certain extent. For example, if we want
Generated types. As already discussed in the introduction, UUXML goes to lengths to faithfully represent schema type information. Let us start by considering the doc element of that section. Our translation produces from this (and the rest of the schema) a pair of Haskell datatype declarations:
produces
data E doc u = E doc (Elem LE E doc LE T docType u) data LE E doc u = EQ E doc (E doc u) | LE E publication (LE E publication u)
public interface DocType { String getKey(); void setKey(String value); DocType.Sequence getAuthorAndTitleAndYear(); void setAuthorAndTitleAndYear( DocType.Sequence value); public interface Sequence { BigInteger getYear(); void setYear(BigInteger value); List getAuthor(); String getTitle(); void setTitle(String value); } }
Frank Atanassow and Johan Jeuring, October 30, 2003
A HASKELL-XML DATA BINDING
The element type doc is represented by a pair of datatypes, E doc and LE E doc, the latter being a ‘down-closed’ variant of the former, used to model subtyping: so publication is a (hypothetical) manifest subtype of doc. Recall that we decided to separate the equational variant from the downclosed one in order to allow users to better exploit the equational capabilities of Haskell. 5
PLAN-X
If g is a document type (such as LE E author) then Rep g b u is the type of repetitions of g which can accur b times in mixity context u. If we write the bounds b as {m, n}, where m is the minimum and n the maximum number of occurrences, then {m, n} is encoded as:
Elements and mixity. To explain the purpose of Elem, let us first show a simple example translation. A document like:
Hello, world!
could (depending on the schema) be represented by a value:
{0, 0} = ZZ {0, m + 1} = ZS {0, m} {0, ∞} = ZI {m + 1, n + 1} = SS {m, n}
Elem () (Mix (Elem "Hello, " Empty) "world!") Elem is one of the standard UUXML types, rather than a generated one like E doc. Its definition is4 :
Some sample encodings are:
data Elem e g u = Elem u g
{0, 1} = ZS ZZ {2, 4} = SS (SS (ZS (ZS ZZ)))
The type e is a so-called phantom type (a type which appears on the lefthand side but not the righthand side of a type declaration) which simply gives the tag name of the element. The type argument u models the ‘mixity’ of XML documents: when an element appears in mixed context, u is instantiated to String and character content preceding an element is stored in its place; otherwise it is instantiated to (). This mixity is threaded down through a document to its leaves. The type Mix, defined as:
{0, ∞} = ZI {2, ∞} = SS (SS ZI) UUXML includes some other features not mentioned here, for example, interleaving and of course attributes. Although UUXML does not attempt to model every feature of XML Schema (in particular, facets and ID types have not been addressed), it treats nearly all the constructs handled in the MSL formal semantics [4], which form a representative set of the core Schema language.
data Mix g u = Mix (g String) String initiates mixed content by passing String as the mixity argument to its children, and ignoring the mixity argument passed in from its parent. Character content at the end of Mix’s body is stored in the second field.
4
In this section, we describe how to automatically generate isomorphisms between pairs of datatypes. Our implementation platform is Generic Haskell, and in particular we used dependency-style GH [13], though our presentation here does not reveal that fact. This section assumes a basic familiarity with Generic Haskell, but the definitions are all remarkably simple. We address the problem in four parts, treating first the products and sums isos in isolation, then showing how to merge those implementations. Finally, we describe a simple modification of the resulting program which implements the non-invertible coercions. In each case, we build the requisite morphism by reducing a value v :: t to a value of a universal data type u = reduce{|t|} v :: Univ. The type Univ plays the role of a normal form from which we can then expand to a value expand {|t0 |} u :: t0 of the desired type, where t → t0 canonically, or t ∼ = t0 for the isos.
Model groups. Other standard types like Seq (sequence), Empty (empty sequence), Or (choice) and Rep (repetition) are used to structure a document. For example, the type LE E docType of our main example is defined: data T docType u = T docType (Seq A key (Seq (Rep LE E author ZI) (Seq LE E title (Rep LE E pubDate (ZS ZZ)))) u) Because, like Elem, they must thread the mixity through the document, they possess one argument more than one would expect: data Seq f g u data Empty u data Or f g u
= = =
Seq (f u) (g u) Empty Or1 (f u) | Or2 (g u)
For an example of their use, again, see the Homer example in the introductory section.
Handling products. Here is a definition of the functions reduce{|t|} and expand {|t|} in which the type isomorphisms expressing associativity and identities of binary products:
Repetition. The repetition type Rep is a bit special because it involves another sort of entity we call a bound which encodes the number of allowable occurrences of a document. Bounds are expressed in a sort of paired unary notation. data Rep g b u data ZZ g u data ZI g u data ZS b g u
= = = =
data SS b g u
=
a :*: Unit ∼ = a ∼ Unit :*: a = a ∼ a :*: (b :*: c) (a :*: b) :*: c =
Rep (b g u) ZZ ZI [g u] ZS (Maybe (g u)) (Rep g b u) SS (g u) (Rep g b u)
are implemented. We assume a set of base types, which may include integers, strings and so on. For brevity’s sake, we mention only integers.
4 The actual definition includes a kludge to force the kind of e to be inferred as ? → ?.
Frank Atanassow and Johan Jeuring, October 30, 2003
GENERIC ISOMORPHISMS
data UBase = UInt Int 6
PLAN-X
The following two functions merely serve to convert back and forth between the larger world and our little universe of base types. type ReduceBase{[?]} t
= t → UBase
reducebase{|t :: κ|} reducebase{|Int|} i
:: ReduceBase{[κ]} t = UInt i
type ExpandBase{[?]} t
= UBase → t
Handling sums. We now turn to the treatment of associativity and identity laws for sums: a :+: Zero ∼ = a Zero :+: a ∼ = a ∼ a :+: (b :+: c) (a :+: b) :+: c = We can implement Zero as an abstract type with no (visible) constructors. The universal type is a right-associated sum of values:
expandbase{|t :: κ|} :: ExpandBase{[κ]} t expandbase{|Int|} (UInt i) = i
data Zero data Univ = UInl UBase | UInr Univ
Now, as Schemers well know, if we ignore the types and remove all occurrences of Unit, a right-associated tuple is simply a cons-list, hence our representation, Univ is defined:
Note that this datatype Univ is isomorphic to:
type Univ = [UBase]
data Univ = UIn Int UBase
Our implementation of reduce{|t|} depends on an auxiliary function red {|t|}, which accepts a value of t along with an accumulating argument of type Univ; it returns the normal form of the t-value w.r.t. the laws above. reduce{|t|} just ‘primes’ red {|t|} with an empty list. type Red{[?]} t
= t → Univ → Univ
red {|t :: κ|} red {|Int|} i u red {|Unit|} red {|a :*: b|} (a :*: b)
:: = = =
reduce{|t :: ?|} reduce{|t|} x
:: t → Univ = red {|t|} x [ ]
We prefer the latter as it simplifies some definitions. We also add a second integer field: data Univ = UIn Int Int UBase If u = UIn r a b then we shall call a the arity of u—it remembers the “width” of the sum value we reduced; we call r the rank of u—it denotes a zero-indexed position within the arity, the choice which was made. We guarantee, then, that 0 6 r < a. Of course, unlike Unit, Zero has no observable values so there is no representation for it in Univ. Let us assume UBase, reducebase{|t|} and expandbase{|t|} are defined as before. This time around, function reduce{|t|} represents values by ignoring choices against Zero and right-associating sums in the following manner (writing I for Int and u for Uint i):
Reduce{[κ]} t reducebase{|Int|} i : u id red {|a|} a . red {|b|} b
expand {|t|} is a generic function that takes a value of the universal data type, and returns a value of type t. It depends on the generic function len{|t|}, which counts the ‘length’ of a product, which is the number of fields (components) in a tuple:
i Inl i Inr i Inl i Inr i Inl (Inl i) Inl (Inr i) Inr i
type Len{[?]} t = Int len{|t :: κ|} len{|Integer|} len{|Double|} len{|Bool|} len{|String|} len{|Unit|} len{|a :*: b|}
:: = = = = = =
Len{[κ]} t 1 1 1 1 0 len{|a|} + len{|b|}
= Univ → t
expand {|t :: κ|} expand {|Int|} [ub ] expand {|Unit|} [ ] expand {|a :*: b|} u | length u ≡ la + lb
:: Expand{[κ]} t = expandbase{|Int|} ub = Unit
| otherwise where (la, lb)
7→ 7→ 7 → 7→ 7→ 7 → 7 → 7 →
UIn UIn UIn UIn UIn UIn UIn UIn
01u 02u 01u 02u 12u 03u 13u 23u
type Arity{[?]} t = Int arity{|t :: κ|} arity{|Int|} arity{|Zero|} arity{|a :+: b|}
:: = = =
Arity{[κ]} t 1 0 arity{|a|} + arity{|b|}
Now we can define reduce{|t|}:
= expand {|a|} (take la u) :*: expand {|b|} (drop la u) = error "expand" = (len{|a|}, len{|b|})
In the last case, we compute the lengths of each factor of the product to determine how many values to project there— remember that a need not be a base type. This information tells us how to split the list between recursive calls. Frank Atanassow and Johan Jeuring, October 30, 2003
I I :+: Zero Zero :+: I I :+: I I :+: I (I :+: I) :+: I (I :+: I) :+: I (I :+: I) :+: I
reduce{|t|} depends on the generic value arity{|t|}, which counts the of number choices in a sum.
Observe that Unit is assigned length zero. Now we can write expand {|t|}: type Expand{[?]} t
:: :: :: :: :: :: :: ::
type Reduce{[?]} t
= t → Univ
reduce{|t :: κ|} reduce{|Int|} i reduce{|Zero|} reduce{|a :+: b|} (Inl x ) where UIn r a u reduce{|a :+: b|} (Inr x )
:: = = = = =
where UIn r a u 7
Reduce{[κ]} t UIn 0 1 (reducebase{|Int|} i) ⊥ UIn r (a + arity{|b|}) u reduce{|a|} x UIn (r + arity{|a|}) (arity{|a|} + a) u = reduce{|b|} x PLAN-X
This treats base types as unary sums, and computes the rank of a value by examining the arities of each summand, effectively ‘flattening’ the sum. Function expand {|t|} is defined as follows:
products: expand {|Int|} (ub : ) expand {|Unit|} expand {|a :*: b|} u | length u > la + lb
type Expand{[?]} t = Univ → t expand {|t :: κ|} :: Expand{[κ]} t expand {|Int|} (UIn 0 1 u) = expandbase{|Int|} i expand {|Zero|} = error "expand" expand {|a :+: b|} (UIn r a u) = | a ≡ aa + ab ∧ r < aa = Inl (expand {|a|} (UIn r (a − ab) u)) | a ≡ aa + ab = Inr (expand {|b|} (UIn (r − aa) (a − aa) u)) | otherwise = error "expand" where (aa, ab) = (arity{|a|}, arity{|b|})
| otherwise where (la, lb) and for sums:
expand {|a :+: b|} (UIn r a u) = | a 6 aa + ab ∧ r < aa = Inl (expand {|a|} (UIn r (a − ab) u)) | a 6 aa + ab = Inr (expand {|b|} (UIn (r − aa) (a − aa) u)) | otherwise = error "expand" where (aa, ab) = (arity{|a|}, arity{|b|})
The logic in the last case checks that the universal value ‘fits’ in the sum type a :+: b, and injects it into the appropriate summand by comparing the value’s rank with the arity of a, being sure to adjust the rank and arity on recursive calls.
These changes implement the following pair of canonical coercions, the first projection of a product and left injection of a sum: a :*: b → a
Sums and products together. It may seem that a difficulty in handling sums and products simultaneously arises in designing the type Univ, as a na¨ıve amalgamation of the sum Univ (call it UnivS) and the product Univ (call it UnivP) permits multiple representations of values identified by the canonical isomorphism relation. However, since the rules of our isomorphism theory do not interact—in particular, we do not account for any sort of distributivity—, a simpler solution exists: we can nest our two representations and add the top layer as a new base type. For example, we can use UnivP in place of UBase in UnivS and add a new constructor to UBase to encapsulate sums.
a → a :+: b Of course, this is only half of the story we would like to hear! Though we could easily implement the dual pair of coercions, we cannot implement them both together except in an ad hoc fashion (and hence refrain from doing so). This is only partly because, in reducing to a universal type, we have thrown away the type information. Even if we knew the types involved, it is not clear, for example, if the coercion a → a :+: a should determine the left or right injection. Fortunately, even this ‘oriented’ form of subtyping proves quite useful. In particular, XML Schema’s so-called ‘extension’ subtyping exactly matches the form of the first projection coercion, as it only allows documents validating against a type t to be used in contexts of type s if t matches a prefix of s: so t is an extension of s. Schema’s other form of subtyping, called ‘restriction’, allows documents validating against type t to be used in contexts of type s if every document validating against t also validates against s: so t is a restriction of s. This can only happen if s, regarded as a grammar, can be reformulated as a disjunction of productions, one of which is t, so it appears our left injection coercion can capture part of this subtyping relation as well. Actually, due to a combination of happenstances, the situation is better than might be expected. First, subtyping in Schema is manifest or nominal, rather than purely structural : consequently, restriction only holds between types assigned a name in the schema. Second, our translation models subtyping by generating a Haskell datatype declaration for the down-closure of each named schema type:
data UnivS = UIn Integer UnivP data UnivP = UNil | UCons UBase UnivP data UBase = UInt Int | USum UnivS We omit the details, as the changes are straightforward.5 Handling subtyping. The reader may already have noticed that the guards in our expansion functions impose some unnecessary limitations. In particular: • when we expand to a product, we require that the length of our universal value equals the number computed by len{|t|}, and • when we expand to a sum, we require that the arity of our universal value equals the number computed by arity{|t|}. If we lift these restrictions, replacing equality by inequality, we can project a prefix of a universal value onto a tuple of smaller length, and inject universal value into a choice of larger arity. The modified definitions are shown below for
data Point data CPoint data LE Point
= = = | data LE CPoint = |
5
Unfortunately, technical limitations in the current implementation of Generic Haskell do not yet make it practical to structure our system in layers this way. Frank Atanassow and Johan Jeuring, October 30, 2003
= expandbase{|Int|} ub = Unit = = expand {|a|} (take la u) :*: expand {|b|} (drop la u) = error "expand" = (len{|a|}, len{|b|})
8
Point... CPoint... EQ Point Point LE CPoint LE CPoint EQ CPoint CPoint ... PLAN-X
stemming from the complexity of our Haskell-XML data binding, UUXML. Our mechanism leverages the power of an existing tool, Generic Haskell, and the established and growing theory of type isomorphisms. Although at present we have a prototype implementation of UUXML, it requires more work before it will be usable. Our technique itself clearly has applications beyond XML itself. For example, when libraries written by distinct developers are used in the same application, they often include different representations of what amounts to the same datatype. When passing data from one library to the other the data must be converted to conform to each library’s internal conventions. Our technique can be used to simplify this conversion task.
Third, we have arranged our translator so that the EQ . . . constructors always appear in the leftmost summand. This means that the injection from the ‘equational’ variant of a translated type to its down-closed variant is always the leftmost injection, and consequently picked out by our expansion mechanism. :: EQ Point EQ CPoint ::
Point → LE Point CPoint → LE CPoint
Since Haskell is, in itself, not so well-equipped at dealing subtyping, when reading an XML document we would rather have the coercion the other way around, that is, we should like to read an LE Point into a Point, but of course this is unsafe. However, when writing a value to a document these coercions save us some work inserting constructors. Of course, since, unlike Schema itself, our coercion mechanism is structural, we can employ this capability in other ways. For instance, if we define the repetition bound ZS as:6
Future work. In fact, the existence of canonical isomorphisms suggests a redundancy in the type systems of functional languages, one which might profitably be captured in a language feature which infers and automatically inserts those isomorphisms for the user. One might conceive of a generic λ-calculus which uses a technique like ours to do normalization at the type level as well as the value level, but using the canonical iso congruence rather than type equality. In the short term, future work might involving extending our technique to infer more type isomorphisms such as commutativity and distributivity. The main reason we have not attempted to do so is that such isomorphisms are canonical, but not uniquely determined by the types involved. Another avenue for improving our technique is to replace the universal type Univ with a set of type-indexed datatypes [10]. This would eliminate the possibility of user’s trying to expand to a type which is not canonically coercible to the one reduced; that is, such errors could be caught statically. Indeed, this was the approach we originally pursued, but Generic Haskell’s support for type-indexed datatypes proved too immature to make it practical at the time of writing.
data ZS b g u = ZS (ZSMaybe (g u)) (Rep g b u) data ZSMaybe a = ZSNothing | ZSJust a when writing a value to a document, we can omit optional elements. The reason is that: Rep g (ZS ZZ) u ∼ = (ZS ZZ) g u ∼ ZSMaybe (g u) :*: Rep g ZZ u = ∼ = ZSMaybe (g u) :*: ZZ u ∼ = ZSMaybe (g u) :*: Unit ∼ = (Unit :+: g u) :*: Unit ∼ = Unit :+: g u and, for every g there is a canonical coercion: g Unit → Unit → Unit :+: g Unit given by a projection followed by an injection. Schema also defines a subtyping relation between primitive types. For example, int is a subtype of integer which is a subtype of decimal. We can easily model this by (adding some more base types and) modifying the functions which convert base types. For example: expandbase{|Decimal|} (UDecimal x ) expandbase{|Decimal|} (UInteger x ) expandbase{|Decimal|} (UInt x ) expandbase{|Integer|} (UInteger x ) expandbase{|Integer|} (UInt x ) expandbase{|Int|} (UInt x )
5
CONCLUSIONS WORK
AND
= = = = = =
Related work. Besides UUXML, we have already mentioned the HaXML [28] and WASH [26] XML data bindings for Haskell. The Model Schema Language semantics [4] is now superseded by newer work [24]. Specialpurpose languages, such as XSLT [27], XDuce [11], Yatl [6], XMλ [17, 23], SXSLT [12] and Xtatic [9], take a different approach to XML problems. In computer science, the use of type isomorphisms seem to have been popularized first by Rittri who demonstrated their value in software retrieval tasks, such as searching a software library for functions matching a query type [21]. Since then the area has ballooned; good places to start on the theory of type isomorphisms is Di Cosmo’s book [8] and the paper by Bruce et al. [5]. More recent work has focused on linear type isomorphisms [2, 25, 22]. In category theory, Mac Lane initiated the study of coherence in a seminal paper [14]; his book [15] treats the case for monoidal categories. Beylin and Dybjer’s use [3] of Mac Lane’s coherence theorem influenced our technique here.
x integer2dec x int2dec x x int2integer x x
RELATED
In this paper, we have described a simple, powerful and very general mechanism that addresses the usability problems 6
We are tentative about using Generic Haskell’s standard Maybe type here because there is some question of whether the order of the summands is standardized and, if so, what the order is. The fact that Haskell’s Maybe type does not define the order is irrelevant because GH necessarily makes a choice about how to convert an algebraic datatype into its structural representation. Frank Atanassow and Johan Jeuring, October 30, 2003
Acknowledgements. The authors are grateful to Andres L¨ oh, Eelco Dolstra and Fermin Reig for their comments on this paper. 9
PLAN-X
REFERENCES
[15] Saunders Mac Lane. Categories for the Working Mathematician, volume 5 of Graduate Texts in Mathematics. Springer-Verlag, New York, 2nd edition, 1997. (1st ed., 1971). [16] Brett McLaughlin. Java & XML data binding. O’Reilly, 2003.
[1] Frank Atanassow, Dave Clarke, and Johan Jeuring. Scripting XML with Generic Haskell. In Proc. 7th Brazilian Symposium on Programming Languages, 2003. See also Utrecht University technical report UUCS-2003. [2] Vincent Balat and Roberto Di Cosmo. A linear logical view of linear type isomorphisms. In CSL, pages 250– 265, 1999.
[17] Erik Meijer and Mark Shields. XMLambda: A functional language for constructing and manipulating XML documents. Available from http://www.cse.ogi.edu/ ~mbs/, 1999.
[3] Ilya Beylin and Peter Dybjer. Extracting a proof of coherence for monoidal categories from a proof of normalization for monoids. In TYPES, pages 47–61, 1995.
[18] Eldon Metz and Allen Brookes. XML data binding. Dr. Dobb’s Journal, pages 26–36, March 2003. [19] Sun Microsystems. Java Architecture for XML Binding (JAXB). http://java.sun.com/xml/jaxb/, 2003.
[4] Allen Brown, Matthew Fuchs, Jonathan Robie, and Philip Wadler. MSL: A model for W3C XML Schema. In Proc. WWW10, May 2001.
[20] Uche Ogbuji. Xml data bindings in python, parts 1 & 2. xml.com, 2003. http://www.xml.com/pub/a/2003/ 06/11/py-xml.html.
[5] Kim B. Bruce, Roberto Di Cosmo, and Giuseppe Longo. Provable isomorphisms of types. Mathematical Structures in Computer Science, 2(2):231–247, 1992. [6] Sophie Cluet and J´erˆ ome Sim´eon. YATL: a functional and declarative language for XML, 2000.
[21] Mikael Rittri. Retrieving library identifiers via equational matching of types. In Conference on Automated Deduction, pages 603–617, 1990.
[7] Jorge Coelho and M´ ario Florido. Type-based XML processing in logic programming. In PADL 2003, pages 273–285, 2003.
[22] Mikael Rittri. Retrieving library functions by unifying types modulo linear isomorphism. Informatique Theorique et Applications, 27(6):523–540, 1993.
[8] Roberto Di Cosmo. Isomorphisms of Types: From lambda-calculus to Information Retrieval and Language Design. Birkh¨ auser, 1995.
[23] Mark Shields and Erik Meijer. Type-indexed rows. In The 28th Annual ACM SIGPLAN - SIGACT Symposium on Principles of Programming Languages, pages 261–275, 2001. Also available from http://www.cse. ogi.edu/~mbs/.
[9] Vladimir Gapeyev and Benjamin C. Pierce. Regular object types. In European Conference on Object-oriented Programming (ECOOP 2003), 2003.
[24] J´erˆ ome Sim´eon and Philip Wadler. The essence of XML. In Proc. POPL 2003, 2003.
[10] Ralf Hinze, Johan Jeuring, and Andres L¨ oh. Typeindexed data types. In Proceedings of the 6th Mathematics of Program Construction Conference, MPC’02, volume 2386 of LNCS, pages 148–174, 2002.
[25] Sergei Soloviev. A complete axiom system for isomorphism of types in closed categories. In A. Voronkov, editor, Proceedings 4th Int. Conf. on Logic Programming and Automated Reasoning, LPAR’93, St. Petersburg, Russia, 13–20 July 1993, volume 698, pages 360–371. Springer-Verlag, Berlin, 1993.
[11] Haruo Hosoya and Benjamin C. Pierce. Xduce: A typed XML processing language. In Third International Workshop on the Web and Databases (WebDB2000), volume 1997 of Lecture Notes in Computer Science, pages 226–244, 1997,2000.
[26] Peter Thiemann. A typed representation for HTML and XML documents in Haskell. Journal of Functional Programming, 12(4&5):435–468, July 2002.
[12] Oleg Kiselyov and Shriram Krishnamurti. SXSLT: manipulation language for XML. In PADL 2003, pages 226–272, 2003.
[27] W3C. XSL Transformations 1.0. Available from http: //www.w3.org/TR/xslt, 1999.
[13] Andres L¨ oh, Dave Clarke, and Johan Jeuring. Dependency-style Generic Haskell. In Proceedings of the International Conference on Functional Programming (ICFP’03), August 2003. to appear.
[28] Malcolm Wallace and Colin Runciman. Haskell and XML: Generic combinators or type-based translation? In International Conference on Functional Programming, pages 148–159, 1999.
[14] Saunders Mac Lane. Natural associativity and commutativity. Rice University Studies, 49:28–46, 1963.
Frank Atanassow and Johan Jeuring, October 30, 2003
10
PLAN-X