EFECTIVIDAD DEL CONTROL INTERNO
2.6. DESCRIPCIÓN DE LAS ACTIVIDADES
2.6.3. Diseño de la herramienta
The larger syntactic changes to Haskell as it becomes Dependent Haskell are sketched above. In addition to these changes, Haskell’s typing rules naturally become much more involved. Though a declarative specification remains out of reach, Chapter 6 describes (and Appendix D details) the algorithm Bake, which is used to detect type-correct Dependent Haskell programs. It is important future work to develop a more declarative specification of Dependent Haskell.
This section comments on several topics that affect the design of Dependent Haskell.
4.4.1
Type
:
Type
Dependent Haskell includes theType:Typeaxiom, avoiding the infinite hierarchy of sorts [57, 80] that appear in other dependently-typed languages. This choice is made solely to simplify the language. Other languages avoid the Type : Type axiom in order to remain consistent as a logic. However, to have logical consistency, a language must be total. Haskell already has many sources of partiality, so there is little risk in adding one more.
Despite the questionable reputation of the Type:Type axiom, languages with this feature have been proved type-safe for some time. Cardelli [12] gives a thorough early history of the axiom and presents a type-safe language withType:Type. Given the inherent partiality of Haskell, the inclusion of this axiom has little effect on the theory.
4.4.2
Inferring
Π
The discussion of quantifiers in this chapter begs a question: which quantifier is chosen when the user has not written any? The answer: →. Despite all of the advances to the type system that come with Dependent Haskell, the non-dependent, relevant, visible, and unmatchable function type,→, remains the bedrock. In absence of other information, this is the quantifier that will be used.
However, as determined by the type inference process (Chapter 6), an inferred type might still have a Π in it. For example, if I declare
replicate’ =replicate
without giving a type signature to replicate’, it should naturally get the same type (which includes aΠ) as replicate. Indeed this is what is delivered by Bake, Dependent
On the other hand, the generalized type of the expression λf g x → f (g x)
is ∀ a b c. (b → c) → (a → b) → (a → c), the traditional type for function composition, not the much more elaborate type (see Section 6.1) for a dependently typed composition function. The more exotic types are introduced only when written in by the user.
4.4.3
Roles and dependent types
Integrating dependent types with Haskell’s role mechanism [11] is a challenge, as explored in some depth in my prior, unpublished work [27]. Instead of addressing this issue head-on, I am deferring the resolution until we can find a better solution than what was proposed in that prior work. That approach, unworthy of being repeated here, is far too ornate and hard to predict. Instead, I make a simplifying assumption that all coercions used in types have a nominal role.35 This choice restricts the way Haskell
newtypes can work with dependent types if the coerce function has been used. A violation of this restriction (yet to be nailed down, exactly) can be detected after type- checking and does not affect the larger type system. It is my hope that, once the rest of Dependent Haskell is implemented, a solution to this thorny problem will present itself. A leading, unexplored candidate is to have two types of casts: representational and nominal. Currently, all casts are representational; possibly, tracking representational casts separately from nominal casts will allow a smoother integration of roles and dependent types than does the ornate approach in my prior work.
4.4.4
Impredicativity, or lack thereof
Despite a published paper [97] and continued attempts at cracking this nut, GHC lacks support for impredicativity.36 Here, I use the following definitions in my meaning of impredicativity, which has admittedly drifted somewhat from its philosophical origins:
Definition (Simple types). A simple type has no constraint, quantification, or de- pendency.
Definition (Impredicativity). A program is impredicative if it requires a non-simple type to be substituted for a type variable.
Impredicativity is challenging to implement while retaining predictable type infer- ence, essentially because it is impossible to know where to infer invisible arguments— invisible arguments can be hidden behind a type variable in an impredicative type system.
Dependent Haskell does not change this state of affairs in any way. In Dependent Haskell, just like in today’s Haskell, impredicativity is simply not allowed.
35If you are not familiar with roles, do not fret. Instead, safely skip the rest of this subsection. 36There does exist an extension
ImpredicativeTypes. However, it is unmaintained, deprecated, and quite broken.
There is a tantalizing future direction here, however: are the restrictions around impredicativity due to invisible binders only? Perhaps. Up until now, it has been impossible to have a dependent or irrelevant binder without that binder also being invisible. (To wit,∀ is the invisible, dependent, irrelevant binder of today’s Haskell.) One of the tasks of enhancing Haskell with dependent types is picking apart the relationship among all of the qualities of quantifiers [56]. It is conceivable that the reason impredicativity hinders the predictability of type inference has to do only with visibility, allowing arbitrary instantiations of type variables with complex types, as long as they have no invisible binders. Such an idea requires close study before implementing, but by pursuing this idea, we may be able to relax the impredicativity restriction substantially.
4.4.5
Running proofs
Haskell is a partial language. It has a multitude of ways of introducing a computa- tion that does not reduce to a value: ⊥/error, general recursion, incomplete pattern matches, non-strictly-positive datatypes, baked-in type representations [75], and possi- bly Girard’s paradox [36, 48], among others. This is in sharp contrast to many other dependently typed language, which are total. (An important exception is Cayenne. See Section 8.3.)
In a total language, if you have a function pf that results in a proof thata ∼ b, you never need to run the function. (Here, I’m ignoring the possibility of multiple, different proofs of equality [91].) By the totality of that language, you are assured that pf will always terminate, and thus running pf yields no information.
On the other hand, in a partial language like Haskell, it is always possible that pf
diverges or errors. We are thus required to run pf to make sure that it terminates. This is disappointing, as the only point of running pf is to prove a type equality, and types are supposed to be erased. However, the Haskell functionpf has two possible outcomes: an uninformative (at runtime) proof of type equality, or divergence. There seems to be no easy, sound way around this restriction, which will unfortunately have a real effect on the runtimes of dependently typed Haskell programs.37
Despite not having an easy, sound workaround, GHC already comes with an easy,
unsound workaround: rewrite rules [73]. A rewrite rule (written with aRULES pragma) instructs GHC to exchange one fragment of a program in its intermediate language with another, by pattern matching on the program structure. For example, a user can write a rule to change map id to id. To the case in point, a user could write a rule that changes pf... to unsafeCoerce Refl. Such a rule would eliminate the possibility of a runtime cost to the proof. By writing this rule, the user is effectively asserting that the proof always terminates.
37Note that running a term likepf is theonly negative consequence of Haskell’s partiality. If, say,
Agda always ran its proofs, it could be partial, too! This loses logical consistency—and may surprise users expecting something that looks like a proof to actually be a proof—but the language would remain type safe.
4.4.6
Import and export lists
Recall thesafeTail example from Section 4.3.2. As discussed in that section, forsafeTail
to compile, it is necessary to reduce ’pred (’Succ n’) to n’. This reduction requires knowledge of the details of the implementation of pred. However, if we imagine that
pred is defined in another module, it is conceivable that the author of pred wishes to keep the precise implementation of pred private—after all, it might change in future versions of the module. Naturally, hiding the implementation ofpred would prevent an importing module from writing safeTail, but that should be the library author’s prerogative.
Another way of examining this problem is to recognize that the definition of pred
encompasses two distinct pieces of information:pred’s type andpred’s body. A module author should have the option of exporting the type without the body.
This finer control is done by a small generalization of the syntax in import and export lists. If a user includes pred in an import/export list, only the name pred and its type are involved. On the other hand, writing pred(. .) (with a literal (. .) in the source code) in the import/export list also includespred’s implementation. This echoes the current syntax of using, say, Bool to export only the Bool symbol while Bool(. .)
exports Bool with all of its constructors.
4.4.7
Type-checking is undecidable
In order to type-check a Dependent Haskell program, it is sometimes necesary to eval- uate expressions used in types. Of course, these expressions might be non-terminating in Haskell. Accordingly, type-checking Dependent Haskell is undecidable.
This fact, however, is not worrisome. Indeed, GHC’s type-checker has had the potential to loop for some time. Assuming that the solver’s own algorithm terminates, type-checking will loop only when the user has written a type-level program that loops. Programmers are not surprised when they write an ordinary term-level program that loops at runtime; they should be similarly not surprised when they write a type-level program that loops at compile time. In order to provide a better user experience, GHC counts reduction steps and halts with an error message if the count gets too high; users can disable this check or increase the limit via a compiler flag.