-- -- | -- -- -- -- Part I of the series introduced the type-level functional language -- with the notation that resembles lambda-calculus with case -- distinction, fixpoint recursion, etc. Modulo a bit of syntactic tart, -- the language of the type functions even looks almost like the pure -- Haskell. In this message, we show the applications of computable -- types, to ascribe signatures to terms and to drive the selection of -- overloaded functions. We can compute the type of a tree of the depth -- fib(N) or a complex XML type, and instantiate the read function to -- read the trees of only that shape. -- -- A telling example of the power of the approach is the ability to use -- not only (a->) but also (->a) as an unary type function. The former is -- just (->) a. The latter is considered impossible. In our approach, -- (->a) is written almost literally as (flip (->) a) -- -- -- This series of messages has been inspired by Luca Cardelli's 1988 -- manuscript ``Phase Distinctions in Type Theory'' -- -- and by Simon Peyton Jones and Erik Meijer's ``Henk: A Typed -- Intermediate Language'' -- -- which expounds many of the same notions in an excellent tutorial style -- and in modern terminology. -- -- I'm very grateful to Chung-chieh Shan for pointing out these papers to -- me and for thought-provoking discussions. -- {-# LANGUAGE KindSignatures, TypeOperators, EmptyDataDecls, Rank2Types #-} {-# LANGUAGE FlexibleContexts, FlexibleInstances, TypeSynonymInstances #-} {-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-} {-# LANGUAGE UndecidableInstances #-} module Language.TypeFN where import Language.TypeLC -- Load part I of this series (prev message) -- | Our first example comes from Cardelli's paper: defining the type -- Prop(n), of n-ary propositions. That is, -- -- > Prop(2) should be the type Bool -> Bool -> Bool -- > Prop(3) is the type of functions Bool -> Bool -> Bool -> Bool -- -- etc. -- -- Cardelli's paper (p. 10) writes this type as -- -- >let Prop:: AllKK(N:: NatK) Type = -- > recK(F:: AllKK(N:: NatK) Type) -- > funKK(N:: NatK) caseK N of 0K => Bool; succK(M) => Bool -> F(M); -- -- >let and2: Prop(2K) = -- > fun(a: Bool) fun(b: Bool) a & b; -- -- Here 0K and 2K are type-level numerals of the kind NatK; recK is the -- type-level fix-point combinator. The computed type Prop(2) then gives -- the type to the term and2. -- --In our system, this example looks as follows: -- data Prop' instance A (F Prop') f (F (Prop',f)) instance A (F (Prop',f)) Zero Bool instance E (f :< m) r => A (F (Prop',f)) (Su m) (Bool -> r) type Prop = Rec (F Prop') type Prop2 = E (F Prop :< N2) r => r and2 = (\x y -> x && y) `asTypeOf` (undefined:: Prop2) -- | We can compute types by applying type functions, just as we can -- compute values by applying regular functions. Indeed, let us define a -- StrangeProp of the kind NAT -> Type. StrangeProp(n) is the type of -- propositions of arity m, where m is fib(n). We compose together the -- already defined type function Prop2 and the type function Fib in the -- previous message. -- data StrangeProp instance E (F Prop :< (F Fib :< n)) r => A (F StrangeProp) n r oddand4t :: E (F StrangeProp :< (F FSum :< N2 :< N2)) r => r oddand4t = undefined oddand5 = (\x1 x2 x3 x4 -> ((x1 && x2 && x3) &&) . and2 x4) `asTypeOf` oddand4t -- > *DepType> :t oddand4t -- > oddand4t :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -- | We can do better, with the help of higher-order type functions. First -- we declare a few standard Haskell type constants as constants in our -- calculus, with trivial evaluation rules -- instance E Bool Bool instance E Int Int instance E String String instance E (a->b) (a->b) -- We could just as well evaluate under instance E (a,b) (a,b) -- these -- | We introduce the combinator Ntimes: |NTimes f x n| applies f to x n times. -- This is a sort of fold over numerals. -- data Ntimes instance A (F Ntimes) f (F (Ntimes,f)) instance A (F (Ntimes,f)) x (F (Ntimes,f,x)) instance A (F (Ntimes,f,x)) Zero x instance E (F Ntimes :< f :< (f :< x) :< n) r => A (F (Ntimes,f,x)) (Su n) r data ATC1 c instance A (F (ATC1 c)) x (c x) -- | We can then write the type of n-ary propositions Prop(N) in a different way, -- as an N-times application of the type constructor (Bool->) to Bool. -- type PropN' n = E(F Ntimes :< (F (ATC1 ((->) Bool))) :< Bool :< n) r => r and2' = (\x y -> x && y) `asTypeOf` (undefined:: PropN' N2) -- | To generalize, -- data ATC2 (c :: * -> * -> *) instance A (F (ATC2 c)) x (F (ATC1 (c x))) -- currying of type constructors type SPropN' n = E(F Ntimes :< (F (ATC2 (->)) :< Bool) :< Bool :< (F Fib :< n)) r => r test_spn4 = undefined::SPropN' (Su N3) -- > *TypeFN> :t test_spn4 -- > test_spn4 :: Bool -> Bool -> Bool -> Bool -> Bool -> Bool -- | The comparison of ATC1 with ATC2 shows two different ways of defining -- abstractions: as (F x) terms (`lambda-terms' in our calculus) and as -- polymorphic types (Haskell type abstractions). The two ways are -- profoundly related. The more verbose type application notation, via -- (:<), has its benefits. After we introduce another higher-order -- function -- data Flip instance A (F Flip) f (F (Flip,f)) instance A (F (Flip,f)) x (F (Flip,f,x)) instance E (f :< y :< x) r => A (F (Flip,f,x)) y r -- | we make a very little change -- type SSPropN' n = E(F Ntimes :< (F Flip :< (F (ATC2 (->))) :< Bool) :< Bool :< (F Fib :< n)) r => r test_sspn4 = undefined::SSPropN' (Su N3) -- | and obtain quite a different result: -- -- > *TypeFN> :t test_sspn4 -- > test_sspn4 :: ((((Bool -> Bool) -> Bool) -> Bool) -> Bool) -> Bool -- -- In effect, we were able to use not only (a->) but also (->a) as an -- unary type function. Moreover, we achieved the latter by almost -- literally applying the flip function to the arrow type constructor -- (->). -- -- Using the type inspection tools (typecast), we can replace the family -- of functions ATC1, ATC2 with one, kind-polymorphic, polyvariadic -- function ATC. This approach will be explained in further messages. -- -- We can use the computed types to drive overloaded functions such as -- read and show. The specifically instantiated read functions, in -- particular, will check that a (remotely) received serialized value -- matches our expectation. Let's consider the type of trees of the depth -- of at most N. -- data Node v els = Node v [els] deriving (Read, Show) type TreeDN v l n = E(F Ntimes :< (F (ATC1 (Node v))) :< l :< n) r => r instance E (Node v els) (Node v els) read_tree3 s = (read s) `asTypeOf` (undefined:: TreeDN Int String N3) -- > *TypeFN> :t read_tree3 -- > read_tree3 :: String -> Node Int (Node Int (Node Int String)) -- | The ability of computed types to drive the selection of overloaded -- functions has wider implications. We can remove the need for -- functional dependencies, or simulate functional dependencies when they -- cannot apply. For example, one may define a multi-parameter typeclass -- but cannot assert functional dependencies, because not all instances -- satisfy them. Therefore, using the methods of the class may require -- complex and annoying type annotations. Computed types remove the need -- to manually write these annotations; the typechecker can compute them -- itself. This subject is to be explored further. -- -- The last example showed computations of polymorphic types, which, -- unsurprisingly, have the form of type abstractions. Our calculus is -- built around the deep connection between type -- abstraction/instantiation and functional abstraction/application. ttree3_1 = read_tree3 "Node 1 [Node 2 []]" ttree3_2 = read_tree3 "Node 1 [Node 2 [Node 3 [\"ok\"]]]" ttree3_3 = read_tree3 "Node 0 [Node 1 [Node 2 [Node 3 [\"ok\"]]]]" -- > *TypeFN> ttree3_2 -- > Node 1 [Node 2 [Node 3 ["ok"]]] -- > *TypeFN> ttree3_3 -- > *** Exception: Prelude.read: no parse