{-- -- TalTech ITI0212 -- -- Functional Programming -- -- week 10, 2022-03-30 -- -- Data, Codata, and Totality --} import Data.List import Data.Colist import Data.Stream -- Idris distinguishes between partial and total functions -- First we will talk about totality for data types, before -- introducing the notion of codata and a corresponding refinement of -- totality. {- Totality for data types -} -- A total function on data types is one which is both *covering* and -- *terminating* where: -- Covering: there must be patterns which cover all possible values of -- input. This is easy for Idris to check mechanically, there just -- needs to be one pattern for each type constructor --not_covering : Nat -> Nat --not_covering (S k) = k -- Terminating: the function must terminate within a finite time -- Not possible to decide mechanically: the famous halting problem -- Instead, Idris uses a syntactic conservative approximation to -- termination. Syntactic means: Idris tries to determine termination -- by examining the code at a syntactic level. Conservative -- approximation means: this can detect many terminating functions -- mechanically (though there will be some terminating functions that -- it cannot detect as terminating). -- The syntactic conservative approximation is that arguments to -- recursive calls be proper subterms of inductive arguments. -- total (covering, no recursive call) pred' : Nat -> Nat pred' 0 = 0 pred' (S k) = k -- total (covering, recursive calls happen on subterms) fib : Nat -> Nat fib Z = 0 fib 1 = 1 fib (S (S n)) = fib n + fib (S n) -- total (covering, recursive call happens on subterm) iterate : Nat -> (a -> a) -> a -> a iterate 0 f = id iterate (S n) f = iterate n f . f -- not total, argument does not get smaller forever : a -> a forever x = forever x -- not recognized as total because (id k) is -- not syntactically a subterm of (S k) id' : Nat -> Nat id' 0 = 0 id' (S k) = S (id' (id k)) -- not total infinity' : Nat infinity' = S infinity' -- infinity' -> S infinity' -> S (S infinity') -> ... {- Codata: potentially infinite data -} -- Servers, REPLs, operating systems, etc. are quite different from -- calculators, compilers, ... etc. -- The former do not produce data like most programs, but rather -- consume (potentially) infinite streams of data. Inductive data -- types can't have potentially infinite values: instead we represent -- infinite data using so-called codata types. -- A term of type Inf a is potentially infinite value -- We can get terms of type Inf a using 'Delay' -- Type of potentially infinite computations producing things of type a -- Delay 'value-constructor' for Inf 'type-constructor': -- data Inf : Type -> Type where -- Delay : (value : ty) -> Inf ty -- get a result from a 'Delay'ed computation -- Force : (computation : Inf ty) -> ty -- Delay prevents eager evaluation of function/constructor arguments -- data Inf : Type -> Type where -- Delay : (value : ty) -> Inf ty data CoNat : Type where Zero : CoNat Succ : Inf CoNat -> CoNat one : CoNat one = Succ (Delay Zero) -- idris can insert Delays for us so we can write -- one = Succ Zero infinity : CoNat infinity = Succ infinity {- Totality for codata -} -- Recursive functions for coinductive types -- are total if they are covering and productive -- Productive: it will evaluate to a (possibly Delay-d) result in finite time -- Also undecidable, so we syntactic conservative approximation: -- Each recursive call must be 'guarded' by a constructor of a coinductive type -- (and thus by an implicit Delay) toCoNat : Nat -> CoNat toCoNat 0 = Zero toCoNat (S k) = Succ (toCoNat k) pred : CoNat -> CoNat pred Zero = Zero pred (Succ x) = x -- not total uncoN : CoNat -> Nat uncoN Zero = Z uncoN (Succ n) = S (uncoN n) -- uncoN inf = uncoN (Succ inf) = S (uncoN inf) infinity'' : CoNat infinity'' = Succ $ id infinity'' {- Colist: finite or infinite sequences -} -- data Colist' : Type -> Type where -- Nil : Colist' a -- (::) : a -> Inf (Colist' a) -> Colist' a zeros : Colist Nat zeros = 0 :: zeros -- Force : Inf a -> a take' : Nat -> Colist a -> List a take' 0 x = [] take' (S k) [] = [] take' (S k) (x :: xs) = x :: take' k xs mapColist : (a -> b) -> Colist a -> Colist b mapColist f [] = [] mapColist f (x :: xs) = f x :: mapColist f xs nats' : Colist Nat nats' = 0 :: mapColist S nats' {- Streams: infinite sequences -} data Stream' : Type -> Type where (::) : a -> Inf (Stream' a) -> Stream' a ones : Stream Nat ones = 1 :: ones implementation Num a => Num (Stream a) where (+) (x :: xs) (y :: ys) = x + y :: (xs + ys) (*) (x :: xs) (y :: ys) = x * y :: (xs * ys) fromInteger n = fromInteger n :: fromInteger n natStream : Stream Nat natStream = 0 :: map S natStream natStream' : Stream Nat natStream' = nats_from 0 where nats_from : Nat -> Stream Nat nats_from n = n :: nats_from (S n) {- Decomposing functions into production and consumption -} sqrtApproxs : (guess : Double) -> Integer -> Stream Double sqrtApproxs guess n = let approx = 0.5 * (guess + (cast n)/guess) in approx :: sqrtApproxs approx n stopAtEpsilon : (epsilon : Double) -> Stream Double -> Double stopAtEpsilon epsilon (x :: xs) = if (head xs - x < epsilon) then head xs else stopAtEpsilon epsilon xs approxSqrt : (guess : Double) -> (prec : Double) -> Integer -> Double approxSqrt guess prec = (stopAtEpsilon prec) . (sqrtApproxs guess)