85 Takarekursiosta Sanoimme luvun 83 foldl -esimerkissämme että foldl :: (a -> b -> a) -> a -> [b] -> a foldl f z [] = z foldl f z (x:xs) = foldl f (f z x) xs olisi pelkkä silmukka Tämä johtuu siitä, että sen rekursiivinen kutsu on erityistä muotoa: takarekursiota (tail recursion) Kutsu on funktion koodissa takapaikalla (in tail position, tail call) jos se on viimeinen askel jonka tämä funktionkoodihaara ottaa Tässä rekursiivinen foldl-kutsu on takapaikalla: Kun siitä rekursiokutsusta palataan, niin sen jälkeen tämä foldl-kutsu ei enää tee mitään muuta kuin vain palauttaa sieltä saamansa arvon omalle kutsujalleen Silloin kuin takapaikalla kutsutaan funktiota itseään, niin kyseessä on takarekursiivinen kutsu kuten siis tässä Takapaikalla olevat kutsut ovat mielenkiintoisia koska ohjelmointikielen toteutus voi optimoida niitä (tail call optimization, TCO): Toteutus ylläpitää tyypillisesti pinoa jonka avulla tiedetään, mihin pitää palata jatkamaan suoritusta, kun tämän nykyisen aliohjelman suoritus päättyy Tavallinen kutsu lisää ensin tähän pinoon nämä riittävät paluutiedot ennen kuin siirtyy kutsuttavaan aliohjelmaan, jotta se aikanaan osaa palata takaisin tänne mistä sitä kutsuttiin Siis tavallinen kutsu kasvattaa tätä pinoa Takapaikalla oleva kutsu voidaankin sen sijaan toteuttaa kasvattamatta tätä pinoa: Takapaikalla kutsuttava aliohjelma voikin palata suoraan sinne, johon tämä aliohjelma itse palaisi joka tapauksessa heti tämän viimeisen aliohjelmakutsunsa jälkeen lukemalla suoraan sen paluutiedot Koska funktionaalisissa kielissä kuten ei ole erillisiä toistorakenteita kuten whilesilmukkaa, vaan toistokin on rekursiota, niin niissä TCO on välttämätöntä muuten while-simukan funktionaalinen toteutus täyttäisi hiljalleen koko muistin pinon kasvaessa kierros kierrokselta Aiemmassa luvun 72 esimerkissämme kutsuimme funktiota laskin takarekursiivisesti TCO ja muistin ahkeruus takaavat, että se voisi jatkaa mielivaltaisen pitkään täyttämättä koneen muistia Näimme yhtälössä (16) TCO:n vaikutuksen silloin, kun laskentaa tarkastellaan sieventämisenä: Lausekkeessa oli koko ajan vain yksi foldl jonka parametrit muuttuivat eli laskenta pyöri foldl-silmukassa 130
Verrataan sitä aidosti rekursiiviseen foldr :: (a -> b -> b) -> b -> [a] -> b foldr f z [] = z foldr f z (x:xs) = f x (foldr f z xs) jolla laskenta etenisikin foldr (+) 0 [x 1, x 2, x 3,,x n ] = βx 1 + (foldr (+) 0 [x 2, x 3,,x n ]) = βx 1 + (x 2 +foldr (+) 0 [x 3,,x n ]) = βx 1 + (x 2 + (x 3 + ( + (x n + (foldr (+) 0 []))))) = βx 1 + (x 2 + (x 3 + ( + (x n + 0)))) Tässä foldr-laskennassa seuraava redex menee yhä syvemmälle ja syvemmälle tuloslausekkeessa Tämä polku seuraavaan redeksiin (tai paremminkin: polku sieltä takaisin) on se, jota ylläpidetään pinossa Yhtälön (16) foldl-laskennassa seuraava redex pysyi sen sijaan aina lausekkeen alussa Kun vielä tehostimme tämän laiskan foldl-laskennan ahkeraksi foldl -laskennaksi, niin saimme haluamamme vakiomuistitilassa pyörivän silmukan Tällaisen foldr-laskennan muistintarve ei riipu funktion f laiskuudesta tai ahkeruudesta, vaan siitä että f joutuu odottamaan foldr-rekursiokutsun tulosta Toisaalta laiskassa foldr-laskennassa f voikin jossakin vaiheessa päättää, ettei se tarvitsekaan foldr-rekursiokutsun tulosta, ja siten lopettaa syötelistan läpikäynnin jo ennen sen päättymistä Tähän foldl-laskenta taas ei pysty, vaan sen on aina käytävä syötelistansa loppuun saakka Laiskassa funktionaalisessa ohjelmoinnissa käytetäänkin tavallisesti funktiota foldr elleivät ajan- tai muistinkäyttö vaadi funktion foldl tai foldl käyttöä Eräs usein toimiva tapa saada aidosti rekursiivisesta koodista takarekursiivista on lisätä siihen sopiva kerääjäparametri Aiemmin luvussa 412 lähdimme optimoimaan funktiota, jossa oli kaksi aitoa rekursiokutsua: Ensin syötteenä saadun hakupuun vasen ja oikea alipuu käsiteltiin rekursiivisesti Sitten näiden rekursiokutsujen antamat osatulokset vielä yhdistettiin operaatiolla (++) koko tulokseksi Lisäämällä kerääjäparametrin a saimme optimoitua sen sellaiseen muotoon, jossa oikea alipuu käsiteltiin yhä aidolla rekursiolla, mutta vasen takarekursiolla Siis silmukaksi joka etenee hakupuun vasenta haaraa pitkin ja käsittelee mennessään sen oikeat haarat rekursiivisesti 131
Samassa yhteydessä huomautimme myös, että foldl voidaan nähdä funktion foldr kerääjäparametriversiona silloin kun f on liitännäinen ja z sen neutraalialkio Yhtälössä (16) taas nähtiin, että tällainen kerääjäparametri kannattaa usein laskea ahkerasti, jotta saavutettaisiin oikeasti se haluttu muistinsäästö Siten funktiosta foldl saatiin foldl 86 Tyypinpäättelyn periaatteista Haskell sisältää siis ns Hindley-Milner-tyypinpäättelyn, johon on lisätty tyyppiluokat Tutustutaan nyt sen perusperiaatteisiin yleisellä tasolla Tutustutaan myös tyypinpäättelyn ja loogisen päättelyn välisiin yhteyksiin hyvin yleisellä tasolla Otetaan Preludesta esimerkiksemme listan pituuden laskeva funktio length [] = 0 length (_:l) = 1 + length l Haskell-ohjelmoija voisi päätellä sen tyypin intuitiivisesti vaikkapa seuraavasti: Tämä length saa yhden parametrin, eli se on tyyppiä funktio parametrinsa tyypiltä a tuloksensa tyypille b jotka selviävät tarkemmin myöhemmin Koska Numero 0 on yksi sen mahdollinen tulos, niin tämän tyypin b pitää olla jokin Numerotyyppi eli Num b Koska tyhjä lista [] on yksi mahdollinen arvo sen parametrille, niin tämän tyypin a pitää olla muotoa [c] Koska length ei välitä syötelistansa alkioista, sillä alkion kohdalla on hahmossa _, niin tämä alkiotyyppi c saa olla mitä tahansa eli se saa olla monimuotoinen Siis kaiken kaikkiaan tyypiksi saadaan length :: (Num b) => [c] -> b Prelude antaa sille hieman täsmällisemmän tyypin length :: [c] -> Int jossa b = Int koska se riittää yleensä listan pituudelle on nopeampi käsitellä kuin mielivalintainen Numero Vakiokirjastossa on myös tämä yleisempi funktio nimellä DataListgenericLength Samoin sieltä löytyy myös DataListgenericTake jne 132
Tehdään sitten vastaava päättely hieman tarkemmin siten kuin Haskell sen voisi suorittaa Otetaan käyttöön logiikasta (tuttu?) notaatio oletukset lausekkeen osien tyypeistä johtopäätös koko lausekkeen tyypistä niille säännöille, joita tyypinpäättelyssä käytetään Olemme jo nähneet tällaisia sääntöjä epäformaalisti eri lausekkeiden yhteydessä Esimerkiksi lausekkeessa case valinta of hahmo 1 -> tulos 1 hahmo k -> tulos k vaadittiin, että 1 valinta lausekkeen ja kaikkien hahmojen pitää olla keskenään samaa tyyppiä 2 valinta lausekkeiden pitää olla keskenään samaa tyyppiä, ja niiden yhteinen tyyppi on myös koko case-lausekkeen tyyppi Tämä voidaan ilmaista sääntönä h 1 :: t h k :: t v, h 1,,h k :: t jossa t on ehdon 1 ja u ehdon 2 tyyppi w 1 :: u w k :: u case v of {h 1 ->w 1 ; ;h k ->w k } :: u (17) Merkintä h i :: t w i :: u tarkoittaa että jos tehdään lisäoletus h i :: t niin silloin siitä voidaan todistaa johtopäätös w i :: u eli että näiden kolmen pisteen kohdalle voidaan laatia jokin todistus Vasemmanpuoleiset oletukset taas takaavat, että nämä lisäoletukset ovat voimassa joten nämä johtopäätöksetkin ovat voimassa Esimerkkimme alkaa tyypitettävän funktion sisäisestä esitysmuodosta ilman syntaktista sokeria: length = \ p -> case p of 133
[] -> 0 _:xs -> ((+) 1) (length xs) Voidaan aloittaa funktioiden säännöllä Tällaista sääntöä voidaan käyttää x :: t e :: u (\ x -> e) :: t -> u (18) edeten säännössä alhaalta ylös eli johtopäätöksestä oletuksiin koristellen funktiota säännön mukaisilla tyyppitarkenteilla :: Tyyppi Haskell salliikin tällaisten tyyppitarkenteiden kirjoittamisen varsin moneen kohtaan lähdekoodia Niillä ohjelmoija voi määrätä tyypin tarkemmaksi kuin se, jonka Haskell päättelee ei tietenkään väljemmäksi, sehän vaarantaisi tyyppiturvallisuuden! Käytimme sitä luvun 524 Isqrt-tyyppiluokkaesimerkissämme takaamaan, että laitetason neliöjuuri sqrt lasketaan tarkemmilla liukuluvuilla Double Näin saadaan length :: a -> b = \ (p :: a) -> (case p of [] -> 0 _:xs -> (((+) 1) (length xs))) :: b jossa a, b, c, ovat vielä tällä hetkellä tuntemattomia, eli uusia tyyppimuuttujia, eli sellaisia joita ei vielä ole käytetty Sitten voimme siirtää tämän b case-lausekkeen sisään vastaavalla säännöllä (17): length :: a -> b = \ (p :: a) -> case (p :: a) of [] :: a -> 0 :: b (_:xs) :: a -> (((+) 1) (length xs)) :: b 134
Tämän casen valinta lausekkeen p tyypiksi on jo edellisessä vaiheessa sovittu a Laajennetaan tämä sopimus myös sen hahmoihin Tiedämme, että 0 on Numerovakio: jonka perusteella length :: (Num b) => a -> b = \ (p :: a) -> case (p :: a) of [] :: a -> 0 :: b (_:xs) :: a -> (((+) 1) (length xs)) :: b 0 :: (Num t) => t joka tuo mukanaan lisärajoitteen Num b eli tyypin b pitää olla Numeerinen Tämä lisärajoite liittyy siis joka tyyppiin, jossa tyyppimuuttuja b esiintyy, mutta nyt merkitään se vain ensimmäiseen sellaiseen tyyppiin eikä toisteta sitä enää muualla Tarkemmin sanoen tapahtuu seuraavaa: Vakion 0 sisäinen sokeroimaton muoto onkin (frominteger :: (Num b) => Integer -> b) (0 :: Integer) eli Integer-vakio 0 ja sen konversio mihin tahansa Numerotyyppiin Kun siihen sovelletaan funktion kutsun tyypityssääntöä sen tyypiksi saadaan se väitetty (Num b) => b f :: t -> u x :: t f x :: u (19) Tiedämme myös, että tyhjä lista [] on vakio jonka alkiotyyppi t on monimuotoinen [] :: [t] Sen perusteella tyyppi a on siis listatyyppi [c] jossa emme tunne alkiotyyppiä c: 135
length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_:xs) :: [c] -> (((+) 1) (length xs)) :: b Listahahmoilla on tyypityssääntö jonka perusteella length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_ :: c) : (xs :: [c]) -> (((+) 1) (length xs)) :: b y :: c z :: [c] y:z :: [c] Nyt on jäljellä enää viimeisen rivin käsittely Se koostuu funktionkutsuista, ja ne siis käsitellään tyypityssäännöllä (19) Etenemme ulkoa sisään koska tiedämme koko lausekkeen tyypin olevan b, ja haluamme viedä tämä tiedon sen osiin Ulommaisen kutsun käsittely tuottaa joten uusi tyyppi e onkin vanha tyyppi [c] d onkin vanha tyyppi b length :: e -> d xs :: e ((+) 1) :: d -> b (length xs) :: d (((+) 1) (length xs)) :: b aiempien tyypitysvaiheiden perusteella Siten jäljelle jää pääteltäväksi enää jonka oletuksista (+) :: f -> (b -> b) 1 :: f ((+) 1) :: b -> b vasen antaa metodin (+) tyypin nojalla että uusi tyyppi f onkin sama kuin vanha tyyppi b oikea voidaan käsitellä kuten vakio 0 136
Yhteenvetona siis length :: (Num b) => [c] -> b = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: b (_ :: c) : (xs :: [c]) -> ((((+) :: b -> (b -> b)) (1 :: b)) ((length :: [c] -> b) (xs :: [c]))) :: b Lopuksi tehdään vielä yleistysaskel (generalization): Koska tyypitys on nyt saatettu onnistuneesti loppuun, niin jätetään jäljelle jääneet tyyppimuuttujat b ja c monimuotoisiksi kunhan muistetaan säilyttää mukana myös se lisärajoite, että Num b Jos käyttäisimme Haskell-laajennuksena olevaa tyyppikvanttoria niin voisimme esittää tämän yleistysaskeleen sen tuomisena koko tyypin eteen: length :: forall b c (Num b) => [c] -> b Parametrinen monimuotoisuus tarkoittaakin että mikä tahansa tyyppi käy, koska tyypityksen ei tarvinnut olettaa siitä tämän enempää Prelude siis antaa tälle funktiolle tarkemman tyypin length :: [c] -> Int joka tarkentaa tätä pääteltyä tulosta siten, että b = Int: length :: [c] -> Int = \ (p :: [c]) -> case (p :: [c]) of [] :: [c] -> 0 :: Int (_ :: c) : (xs :: [c]) -> ((((+) :: Int -> (Int -> Int)) (1 :: Int)) ((length :: [c] -> Int) (xs :: [c]))) :: Int Lisärajoite saa muodon Num Int joka on totta ja voidaan poistaa: on olemassa vastaava instance Num Int Samoin aiemmin monimuotoinen metodin (+) kutsu on nyt täsmentynyt kutsumaan juuri sitä versiota, joka löytyy juuri tästä instanssista 137
Esimerkissämme ei ollut paikallisia määrittelyjä Lauseke let määrittelyt in runko tyypitetään seuraavasti: 1 Ensin tyypitetään nämä paikalliset määrittelyt kuten edellä Jos sen kuluessa todetaan, että kaksi tyyppimuuttujaa (vaikkapa x ja y) ovat itse asiassa sama tyyppi, ja niistä yksi (vaikkapa x) on kotoisin tämän let-lausekkeen ulkopuolelta toinen (vaikkapa y) syntyi vasta näitä paikallisia määrittelyjä tyypittäessä niin silloin jälkimmäinen korvataan edellisellä eikä päinvastoin (eli y korvataan muuttujalla x eikä päinvastoin) eli uudempi vanhemmalla 2 Sitten yleistetään monimuotoisiksi vain ne uudet tyyppimuuttujat, jotka syntyivät tyypitysaskeleen 1 aikana mutta joita ei korvattu vanhemmilla 3 Lopuksi tyypitetään runko käyttäen näitä paikallisia ja yleistettyjä määrittelyjä Näin saatu rungon tyyppi on samalla koko let-lausekkeen tyyppi Myös while-lauseke tyypitetään näin Tällainen tyypinpäättely on ns PSPACE-täydellistä (PSPACE-complete) eli intuitiivisesti yksi vaikeimmista laskentaongelmista, joka voidaan ratkaista järkevällä määrällä muistia mutta johon voi silti kulua järjettömän paljon aikaa Kaikki kurssilla LAP esitellyt NP-täydelliset laskentaongelmat kuuluvat myös tähän luokkaan PSPACE, ja yleisesti uskotaan että PSPACE sisältää paljon muutakin kuin ne Onneksi tällainen tyypinpäättely on osoittautunut olevan nopeaa ohjelmakoodin pituuteen verrattuna ja hidasta vain sen sisennyssyvyyteen eli leveyteen verrattuna ja onneksi ohjelmat ovat tavallisesti hyvin pitkiä mutteivät kovinkaan leveitä 87 Ohjelmoinnin ja logiikan suhteesta Edellä luvussa 86 on kuvattu Haskellin tyypinpäättelyä samaan tapaan kuin tehtäisiin päättelyä logiikassa Niillä onkin läheinen yhteys; tarkastellaan sitäkin hieman Tarkastellaan sellaisia Haskell-lausekkeita, joissa ei käytetä paikallisia let- eikä where-määrittelyjä ei käytetä arvoa undefined ainoana perustyyppinä on vain () muista monikoista käytetään monikoista vain pareja 138
algebrallisista tyypeistä käytetään vain tyyppiä data Either a b = Left a Right b ja tarkastellaan niiden tyypittämistä Tämä yhteys on nähty alun perin λ-lausekkeilla, mutta pysymme nyt niiden sijaan Haskell-lausekkeissä Parien tyypityssääntö on x :: s y :: t (x,y) :: (s,t) (20) ja parityypeille case-sääntö (17) saa muodon v :: (s,t) x :: s y :: t x :: s, y :: t w :: u case v of {(x,y)->w} :: u (21) mutta tavallisesti käytetään Preludestakin löytyviä funktioita fst v = case v of {(x,_)->x} snd v = case v of {(_,y)->y} Either-tyyppikonstruktorilla on kaksi sääntöä, yksi sen kummallekin arvokonstruktorille: x :: s Left x :: Either s t (22) y :: t Right y :: Either s t (23) Tämän tyyppikonstruktorin case-sääntö (17) saa muodon v :: Either s t x :: s y :: t x :: s w L :: u y :: t w R :: u case v of {Left x->w L ;Right y->w R } :: u (24) mutta tavallisesti käytetään Preludestakin löytyvää funktiota either :: (s -> u) -> (t -> u) -> Either s t -> u either f g v = case v of {Left x->f x;right y->g y} Unohdetaan hetkeksi tyypityssäännöistämme osat jos lauseke on tyyppiä eli osat lauseke :: Mitä jää? (20): Jos on todistettu sekä s että t niin voidaan todistaa myös (s,t) Tämähän on loogisen konjunktion sääntö kunhan vain kirjoitetaan(s,t) muodossa s t 139
Ohjelmoijan intuitio onkin että parissa on yksi kenttä tyyppiä s ja toinen tyyppiä t Tästä näkökulmasta fst esittää päättelysäännön s t s eli jos on todistettu konjunktio, niin on todistus myös sen ensimmäiselle konjunktille Vastaavasti snd on toisen konjunktin t päättelysääntö Näistä case-säännöistä jätetään lukematta myös niiden hahmojen tyypitykset, ja luetaankin vain lausekkeiden tyypitykset Tyyppi () on siis tyhjä konjunktio eli tosi (18): Jos oletuksella t voitiin todistaa u niin voidaan todistaa myös t -> u Tämähän on loogisen implikaation sääntö kunhan vain kirjoitetaan t -> u muodossa t u Funktioihin ei voi soveltaa case-lausekkeita mutta sen sijaan niitä voi kutsua: Kutsun tyypityssääntö (19) voidaan lukea jos on todistettu sekä t -> u että t niin voidaan todistaa myös u Tämähän on puolestaan päättelysääntö modus ponens eli implikaation käyttö : t u t u (25) (22): Jos on todistettu s niin voidaan todistaa myös Either s t Tämähän on loogisen disjunktion yksi sääntö kunhan vain kirjoitetaan Either s t muodossa s t Vastaavasti (23) on se toinen sääntö, joka todistaakin saman disjunktion jos t onkin todistettu Ohjelmoijan intuitio onkin että tässä tietueessa on joko kenttä tyyppiä s tai kenttä tyyppiä t, ja lisäksi tieto kumpi näistä kentistä siinä on Vastaava case-lauseke (24) on puolestaan päättelysääntö s t s u t u u eli jos on todistettu että s tai t ja että kummastakin seuraa u niin sitten voidaan todistaa myös u eli funktio either Siten tyypityssääntömme Tyyppi osat muodostavatkin propositionaalisen eli lauselogiikan (propositional logic) Tämä logiikka ei kuitenkaan olekaan se tuttu klassinen (classical) joka puhuu totuudesta, vaan konstruktiivinen (constructive) joka puhuukin todistuvuudesta Konstruktiivista logiikkaa nimitetään myös intuitionistiseksi (intuitionistic) Siis tyyppi onkin konstruktiivisen lauselogiikan kaava Väite Marsissa joko on elämää tai siellä ei ole elämää on 140
tosi klassisesti koska sen totuusarvo ei riipu Marsin todellisuudesta avoin konstruktiivisesti koska meillä ei ole vielä todistusta kummallekaan sen osaväitteelle Siis kolmannen poissuljetun laki φ φ on tautologia(na pätevä) klassisessa logiikassa mutta ei konstruktiivisessa Konstruktiivinen logiikka antaakin omalle implikaatiolleen Brouwer-Heyting- Kolmogorov-tulkinnan: Implikaatio t u pätee konstruktiivisesti, jos on olemassa jokin tapa muuntaa mikä tahansa todistus T kaavalle t todistukseksi U kaavalle u konstruoida U todistuksesta T Brouwer kehitti matematiikan perusteiden filosofiaa 1900-luvun alussa Heyting taas kehitti silloin sellaisen algebran, joka on konstruktiivisen logiikan taustalla samaan tapaan kuin binäärialgebra on klassisen logiikan taustalla Kolmogorov oli monipuolinen matemaatikko: tätä logiikkaa, todennäköisyyslaskennan aksiomatisointi, kompleksisuuden mitta, Klassisessa logiikassa taas implikaatio oli sama kuin mutta ei siis konstruktiivisessa φ ψ φ ψ Lausekkeet tyyppiä t ja u voidaan puolestaan nähdä esitystapoina näille todistuksille T ja U: Funktiot fst, snd ja either edustavat päättelysääntöjä, samoin funktion määrittely ja sen kutsu Siten lauseke onkin todistus tyyppinsä ilmaisemalle konstruktiivisen lauselogiikan kaavaille ja tyypintarkistus onko T :: t? todistuksen T tarkistamista ja halutun tyyppisen lausekkeen ohjelmointi onkin tällaisen todistuksen laatimista! Silloin BHK-tulkinnan vaatimaksi tavaksi puolestaan käy mikä tahansa lauseke, jonka tyyppi on F :: t u koska silloin lauseke F T antaa sopivan tuloksen U: Koska lausekkeissamme ei ole rekursiota, niin onnistunut tyypitys takaa normalisoinnin pysähtymisen Lausekkeissamme ei ole myöskään mitään, joka johtaisi suoritusaikaiseen virheeseen Siten funktion arvon laskenta eli ohjelman suoritus onkin tällaisen todistuksen sieventämistä kohden normaalimuotoaan, jossa sen sisältämät päättelyaskeleet kuten erityisesti (25) onkin oikaistu 141
Yhteenvetona edellä (hyvin epätarkasti ) luonnosteltu Curryn-Howardin vastaavuus (the Curry-Howard correspondence, the proofs-as-programs correspondence) on: käsite ohjelmoinnissa tyyppi jokin sen tyyppinen lauseke sen tyyppisen lausekkeen arvon laskenta eli suorittaminen eli sieventäminen halutun tyyppisen lausekkeen ohjelmointi näin ohjelmoidun lausekkeen tyypin tarkistus parametrinen monimuotoisuus sama käsite logiikassa kaava jokin sitä tyyppiä vastaavan kaavan todistus vastaavan todistuksen yksinkertaistaminen todistuksen laatiminen sen tyyppiä vastaavalle kaavalle näin laaditun todistuksen tarkistaminen yleistys kaikilla kaavoilla pätee, että Tämä vastaavuus inspiroi ohjelmoinnin ja ohjelmointikielten teoriaa kehittämään yhä ilmaisuvoimaisempia tyyppijärjestelmiä jotta tyypeillä voitaisiin ilmaista yhä enemmän ohjelmien spesifikaatioiden sisältämästä informaatiosta menetelmiä, joilla formaaleista loogisista todistuksista voisi generoida niihin liittyvän ohjelmakoodin (puoli)automaattisesti Silloin ohjelmoijasta tulisikin loogikko, joka todistaa konstruktiivisesti, että tyypeillä/kaavoilla esitetty spesifikaatio on todellakin mahdollista toteuttaa ja tietokone generoisi automaattisesti hänen todistuksestaan sitä vastaavan eli spesifikaation mukaisen ohjelmatoteutuksen Käytännössä tällainen äärimmäisen formaali korrektiuteen keskittyvä ohjelmointitapa rajoittuisi varmaankin sellaisiin ohjelmistoihin ja ohjelmistojen ydinosiin, joiden luotettavuus on aivan välttämätöntä Ehkäpä jonain päivänä 142