Previous Up Next

Peatükk 9 Andmestruktuurid puhtas λ-arvutuses

Eelnevalt nägime, kuidas arvutada konstantidega λ-arvutuses, kus konstantideks võtsime tõeväärtused, täisarvud, paarid ja listid. Nüüd vaatame, kuidas defineerida tõeväärtused, naturaalarvud, paarid ja listid kasutades puhast λ-arvutust. See tähendab, et teoreetilises lähenemises võime konstante ja δ-reduktsioone ignoreerida.

Definitsioonideks valime kombinaatorid: ilma vabade muutujateta termid, mis on normaalkujul. Nii käituvad meie definitsioonid sarnaselt konstantidele — ei tekita juurde vabu muutujaid ega redutseeru selliselt, et me neid ära ei tunne. Tegelikult on võimalik kõik abstraktsioonid kirjeldada S, K ja I kombinaatoritega.

I := λ x. x          
K := λ x y. x          
S := λ f g x. f x (g x)          

Tõeväärtuste spetsifikatsiooniks võtame tõeväärtuste funktsioonide δ-reduktsioonid, leiame konstantidele vastavad λ-termid ja näitame, et δ-reduktsiooni asemel saame teha β-reduktsioone ja saada sama tulemuse.

9.1 Tõeväärtuste implementatsioon

Tõeväärtuste operatsioonid on spetsifitseeritud järgnevalt.

not  true→δ false          
not  false→δ true          
cond  true  e1 e2→δ e1          
cond  false  e1 e2→δ e2          

Sobivate definitsioonide leidmiseks kasutame järgnevat strateegiat: väärtuseks võtame vastava väärtuse mustrisobituse operatsiooni. Tõeväärtuse true mustrisobitus ehk tingimuslause võtab kaks parameetrit, e1 ja e2, ning tagastab e1. Seega võtame true := λ x y.  x. Sarnase argumentatsiooniga võtame false := λ x y.  y. Muud operatsioonid saame programmeerida kasutades teadmist, et tõeväärtused on nendele vastavad tingimuslaused.

not:= λ t. t false  true          
cond:= λ t.  t          

Saame näidata, et δ-reduktsioonile vastab mitmesammuline β-reduktsioon.

not  true=(λ t. t false  true) true
 →βtrue  false  true
 =(λ x. λ y. x) false  true
 →β(λ y. false) true
 →βfalse
not  false=(λ t. t false  true) false
 →βfalse  false  true
 =(λ x. λ y. y) false  true
 →β(λ y. y) true
 →βtrue
cond  true  e1 e2=(λ t. t) true  e1 e2
 →βtrue  e1 e2
 =(λ x. λ y. x) e1 e2
 →β(λ y. e1) e2
 →βe1
cond  false  e1 e2=(λ t. t) false  e1 e2
 →βfalse  e1 e2
 =(λ x. λ y. y) e1 e2
 →β(λ y. y) e2
 →βe2

9.2 Paaride implementatsioon

Paaride operatsioonid on spetsifitseeritud järgnevalt.

fst  (pair  e1 e2) →δ e1           
snd  (pair  e1 e2) →δ e2           

Definitsioonide leidmise strateegia on sama mis tõeväärtuste puhul — leiame mustrisobitusele vastava termi. Idrises saab paaride mustrisobitust teostada avaldisega ⁠match MkPair e_1 e_2 with MkPair x y => p. Seega λ-arvutuses võime võtta näiteks pair = λ e1 e2 p. p e1 e2. Ehk siis paari komponendid antakse ainukesele harule edasi eraldi parameetritena. Siis fst ja snd saame impmeneteerida kasutades mustrisobitus järgnevalt.

fst:= λ p. p true          
snd:= λ p. p false          

Nüüd saame näidata definitsioonide vastavust spetsifikatsioonile.

fst  (pair  e1 e2)=(λ p. p true) (pair  e1 e2)
 →β(pair  e1 e2) true
 =(λ e1 e2 p. p e1 e2) e1 e2 true
 ↠βtrue  e1 e2
 =(λ x y. x) e1 e2
 ↠βe1
snd  (pair  e1 e2)=(λ p. p false) (pair  e1 e2)
 →β(pair  e1 e2) false
 =(λ e1 e2 p. p e1 e2) e1 e2 false
 ↠βfalse  e1 e2
 =(λ x y. y) e1 e2
 ↠βe2

9.3 Naturaalarvud

Natutaalarvude kohta nõuame, et succ , pred , iszero , add ja mul arvutaks vastavalt järgmise arvu, eelmise arvu, arvu võrdumise nulliga, liitmise ja korrutamise.

Esmalt püüame ehitada nii, et nulli esitab identsusfunktsioon λ x. x ja nullist suuremad arve paarid, mille esimene komponent on false ja teine on ühe võrra väiksem number. Nii on arvude struktuur sarnane Idrise struktuurile aga lisatud on false komponendid. Sellist konstruktsiooni nimetatakse standardnumbriteks.

⌜ 0 ⌝ := λ x. x          
⌜ n+1 ⌝ := pair  false  ⌜ n ⌝          
succ := λ n. pair  false  n          
pred := snd          
iszero := fst          

Saame näidata, et succ , pred ja iszero töötavad korrektselt. Näeme, et pred töötab vaid nullist suuremate arvude korral.

succ  ⌜ n ⌝=(λ n. pair  false  n) ⌜ n ⌝
 →βpair  false  ⌜ n ⌝
 =⌜ n+1 ⌝
pred  ⌜ n+1 ⌝=(λ p. p false) (pair  false  ⌜ n ⌝ )
 →βpair  false  ⌜ n ⌝ false
 =(λ e1 e2 p. p e1 e2) false  ⌜ n ⌝ false
 ↠βfalse  false  ⌜ n ⌝
 =(λ x y.  y) false  ⌜ n ⌝
 ↠β⌜ n ⌝
pred  ⌜ 0 ⌝=(λ p. p false) (λ x. x)
 →β(λ x. x) false
 →βfalse
iszero  ⌜ 0 ⌝=(λ p. p true) (λ x. x)
 →β(λ x. x) true
 →βtrue
iszero  ⌜ n+1 ⌝=(λ p. p true) (pair  false  ⌜ n ⌝)
 →βpair  false  ⌜ n ⌝ true
 =(λ e1 e2 p. p e1 e2) false  ⌜ n ⌝ true
 ↠βtrue  false  ⌜ n ⌝
 →βfalse

Kui makrode defineerimisel oleks rekursioon lubatud, saaks standardnumbrite liitmise defineerida kui add := λ x y. cond  (iszero  x) y (add  (pred  x)(succ  y)). Hiljem, peatükis 9.5, näeme ühte võimalust, kuidas rekursiooniga hakkama saada. Enne aga vaatame ühte alternatiivset lähenemist — Churchi numbreid.

Numbrite defineerimiseks kasutame funktsiooni termide astendamise notatsiooni. Termide E, E∈ Λ jaoks En E′ rakendab funktsiooni E n-korda, alustades argumendist E′. Ehk E0 E′ = E′, E1 E′ = E E′, E2 E′ = E (E E′) , E3 E′ = E (E (E E′)) jne. Nüüd saame defineerida Churchi numbrid.

n
:= λ f x. fn x          
succ := λ n. λ f x. f (n f x)          
iszero := λ n. n (λ x. false) true          

Churchi numbrite intuitsioon on, et arv n tähistab mingi valitud operatsiooni n-kordset tegemist — arv vastab sellele, mitu iteratsiooni tuleb teha. Arvule ühe lisamine tähendab ühe iteratsiooni lisamist. Nulliga võrdumist saab kontrollida nii, et algseisuks võetakse true ja tsüklis muudetakse see false -ks; nulli iteratsiooni puhul jääb seisundiks true ja rohkemate iteratsioonide puhul on tulemus false . Vaata formaalseid β-reduktsioone.

succ  
n
=(λ n. λ f x. f (n f x)) (λ f x. fn x)
 →βλ f x. f ((λ f x. fn x) f x)
 ↠βλ f x. f (fn x)
 =λ f x. fn+1 x
 =
n+1
iszero  
0
=(λ n. n (λ x. false) true) (λ f x. x)
 →β(λ f x. x) (λ x. false) true
 ↠βtrue
iszero  
n+1
=(λ n. n (λ x. false) true) (λ f x. f (fn x))
 →β(λ f x. f (fn x)) (λ x. false) true
 ↠β(λ x. false) ((λ x. false)n true)
 →βfalse

Liitmise, korrutamise ja isegi astendamise saame defineerida järgnevalt. Pane tähele, et astendamise definitsiooni saaks kirjutada veel lühemalt: exp = λ m n. n m.

add := λ m n. λ f x. m f (n f x)          
mul := λ m n. λ f x. m (n f) x          
exp := λ m n. λ f x. n m f x          

Vaatame, kuidas term add  m n β-redutseerub naturaalarvude m ja n summaks. Intuitiivselt, kõigepealt tehakse n iteratsiooni ja seejärel veel m iteratsiooni. Korrutamise korrektsuse verifitseerimiseks peame veenduma, et (λ x. fn x )m x on tõesti sama kui fmn x. Ehk, intuitiivselt m korda tehakse n iteratsiooni.

add  
m
 
n
=
(λ m n. λ f x. m f (n f x))  
m
 
n
 ↠β
λ f x. 
m
 f (
n
 f x)
 =λ f x. (λ f x. fm x) f ((λ f x. fn x) f x)
 ↠βλ f x. fm (fn x)
 =
m+n
mul  
m
 
n
=
(λ m n. λ f x. m (n f) x)  
m
 
n
 ↠β
λ f x. 
m
 (
n
 f) x
 =
λ f x. (λ f x. fm x ) (
n
 f) x
 ↠β
λ f x. (
n
 f)m x
 =λ f x. ((λ f x. fn x ) f)m x
 →βλ f x. (λ x. fn x )m x
 ↠βλ f x. fmn x
 =
mn

Churchi numbrite puhul on keeruliseks ülesandeks hoopis ühe lahutamine. Intuitiivselt, kui meil on arv n ja me saame teha n iteratsiooni aga soovime teha n−1 iteratsiooni. Selle probleemile on kaks standardset lahendust. Esiteks vaatame lahendust, kus meil intuitiivselt on iteratsioonis lisamuutuja — tõeväärtus, mis ütleb, kas esimese rakendusega või mitte. Vastavalt abimuutujale rakendame funktsiooni f või tagastame kohe argumendi. Vajame abifunktsiooni abifunktsiooni, kus kehtib

prefn f (true,x)↠(false,x)
prefn f (false,x)↠(false,f x)
(prefn f)n (false,x)↠(false,fn x)

Seega kehtiks ka

(prefn f)1+n (true,x)↠(prefn f)n (false,x)↠(false,fn x)

Sellist spetsifikatsiooni rahuldab järgmine definitsioon.

prefn := λ f p. (false,(cond  (fst  p) (snd  p) (f (snd  p))))           
prefn f (true,x)=(λ f p. (false,(cond  (fst  p) (snd  p) (f (snd  p))))) f (true,x)
 ↠β(false,(cond  (fst  (true,x)) (snd  (true,x)) (f (snd  (true,x)))))
 ↠β(false,(cond  true  (snd  (true,x)) (f (snd  (true,x)))))
 ↠β(false,snd  (true,x))
 ↠β(false, x)
prefn f (false,x)=(λ f p. (false,(cond  (fst  p) (snd  p) (f (snd  p))))) f (false,x)
 ↠β(false,(cond  (fst  (false,x)) (snd  (false,x)) (f (snd  (false,x)))))
 ↠β(false,(cond  false  (snd  (false,x)) (f (snd  (false,x)))))
 ↠β(false, f (snd  (false,x)))
 ↠β(false, f x)

Reduktsiooni (prefn f)n (false,x) ↠ (false,fn x) tõestame induktsiooniga üle naturaalarvu n. Vaatame baasjuhtu n=0 ja induktsiooni sammu n=n′+1.

(prefn f)0 (false,x)=(false,x)
 =(false, f0 x)
(prefn f)n′+1 (false,x)=(prefn f)n′ (prefn f (false,x))
 ↠β(prefn f)n′ (false, f x)
 ↠β(false, fn′ (f x))
 =(false, fn′+1 x)

Siis saame defineerida eelmise arvu leidmise operaatori pred .

pred := λ n. λ f x. snd  (n (prefn f) (true,x))           
pred  
0
=
(λ n. λ f x. snd  (n (prefn f) (true,x))) 
0
 ↠β
λ f x. snd  (
0
 (prefn f) (true,x))
 =λ f x. snd  ((λ f x. f0 x) (prefn f) (true,x))
 ↠βλ f x. snd  (true,x)
 ↠βλ f x.  x
 =
0
pred  
(n+1)
=
(λ n. λ f x. snd  (n (prefn f) (true,x))) 
(n+1)
 ↠β
λ f x. snd  (
(n+1)
 (prefn f) (true,x))
 =λ f x. snd  ((λ f x. fn+1 x) (prefn f) (true,x))
 ↠βλ f x. snd  ((prefn f)n+1 (true,x))
 =λ f x. snd  ((prefn f)n (prefn f (true,x)))
 ↠βλ f x. snd  ((prefn f)n (false,x))
 ↠βλ f x. snd  (false,fn x))
 ↠βλ f x. fn x
 =
n

Seega oleme andnud ühe naturaalarvude implementatsiooni kasutades paari ja tõeväärtust. Teine variant on implementeerida viivitus naturaalarvude paariga, mille mõlemad komponendid on alguses nullid. Iga iteratsioon kopeerib paari teise komponendi esimeseks ja suurendab teiset komponenti ühe võrra — nii on paari esimene komponent alati peale esimest iteratsiooni ühe võrra väiksem. Peale iteratsioone tagastatakse paari esimene komponent.

f:= λ p. pair  (snd  p) (succ  (snd  p))         
pred
:= λ n. fst  (n f (pair  
0
 
0
))
         

Esmalt tõestamie induktsiooniga, et f-i n-kordsel rakendamisel paarile pair  m m+1 on tulemuseks (pair  m+n m+n+1). Induktsiooni baas kehtib triviaalselt kuna f0 (pair  m m+1) = pair  m m+1. Induktsiooni sammu kehtivust tõestab järgnev reduktsioon.

fn+1 
 (pair  
m
 
m+1
) =
         
 
= (λ p. pair  (snd  p) (succ  (snd  p))) (fn (pair  
m
 
m+1
))
         
 
→β pair  (snd  (fn (pair  
m
 
m+1
))) (succ  (snd  (fn (pair  
m
 
m+1
))))
         
 
→β pair  (snd  (pair  
m+n
 
m+n+1
)) (succ  (snd (pair  
m+n
 
m+n+1
)))
         
 
↠β pair  
m+n+1
 (succ  
m+n+1
)
         
 
↠β pair  
m+n+1
 
m+n+1+1
         

Nüüd saame hõlpsasti näidata, et pred  0 redutseerub väärtuseks 0 ja pred  n+1 redutseerub väärtuseks n.

pred  
0
=
(λ n. fst  (n f (pair  
0
 
0
))) 
0
 →β
fst  (
0
 f (pair  
0
 
0
))
 →β
fst  (pair  
0
 
0
)
 ↠β
0
pred  
n+1
=
(λ n. fst  (n f (pair  
0
 
0
))) 
n+1
 ↠β
fst  (
n+1
 f (pair  
0
 
0
))
 ↠β
fst  (fn (f (pair  
0
 
0
)))
 ↠β
fst  (fn (pair  
0
 
1
))
 ↠β
fst  (pair  
n
 
n+1
)
 ↠β
n

9.4 Listid

Listide kohta on jägnevad δ-reduktsioonireegild.

null nil→δ true          
null (cons e1 e2)→δ false          
hd (cons e1 e2)→δ e1          
tl nil→δ nil          
tl (cons e1 e2)→δ e2          

Sarnaselt naturaalarvudele, on siin võimalik kaks lähenemist. Esiteks võime lähtuda listi mingist fold-funktsioonist ja implementeerida null, hd ja tl kasutades seda fold-i. Teine võimalus oleks kasutada paare ja tõeväärtueseid — teades ette, et fold-i puudumisel on paljude funktsioonide implementeerimiseks vaja rekursioon.

Vaatame esmalt põgusalt listide implementatsiooni, mis on tuletatud listide foldr funktsioonist.

nil:= λ f b. b          
cons:= λ x l. λ f b. f x (l f b)          

Nüüd saame implementeerida konstandid null ja hd.

null:= λ l. l (λ y z. false) true          
hd:= λ l. l (λ y z. y) I          
null nil=(λ l. l (λ y z. false) true) nil
 →βnil (λ y z. false) true
 =(λ f b. b) (λ y z. false) true
 ↠βtrue
null (cons x l)=(λ l. l (λ y z. false) true) (cons x l)
 →βcons x l (λ y z. false) true
 =(λ x l. λ f b. f x (l f b)) x l (λ y z. false) true
 ↠β (λ y z. false) x (l (λ y z. false) true)
 ↠βfalse
hd (cons x l)=(λ l. l (λ y z. y) I) (cons x l)
 →βcons x l (λ y z. y) I
 =(λ x l. λ f b. f x (l f b)) x l (λ y z. y) I
 ↠β(λ y z. y) x (l (λ y z. y) I)
 ↠βx

Listi saba leidmine kasutades fold funktsioone on võimalik aga parajalt tülikas. Definitsioonid tulevad pikad ja korrektsuse näitamine võtab palju samme. Seetõttu piirdume siin Idrise koodiga, mis arvutavad listi saba kasutades ⁠foldr või ⁠foldl.

1tl1 : List a -> List a 2tl1 l = snd (foldl f (True,[]) l) where 3 f : (Bool, List a) -> a -> (Bool, List a) 4 f (True ,r) x = (False, r) 5 f (False,r) x = (False, r++[x]) 6 7tl2 : List a -> List a 8tl2 l = snd (foldr f (pred (length l), []) l) where 9 f : a -> (Nat, List a) -> (Nat, List a) 10 f x (0, r) = (0, r) 11 f x (S n, r) = (n, x::r)

Teine võimalus listifunktsioone defineerida on läbi paaride ja tõeväärtuste.

nil:= λ z. z          
cons:= λ x y. pair false  (pair  x y)          
null:= λ z. z true          
hd:= λ z. fst  (snd  z)         
tl:= λ z. snd  (snd  z )          

Nüüd saame näidata, et meie definitsioonid vastavad spetsifikatsioonile kasutades β-reduktsiooni.

null nil=(λ z. z true) nil
 →βnil true
 =(λ z. z) true
 ↠βtrue
null (cons x l)=(λ z. z true ) (cons x l)
 →βcons x l true
 =(λ x y. pair false  (pair  x y)) x l true
 ↠βpair false  (pair  x l) true
 ↠βtrue false  (pair  x l)
 ↠βfalse
hd (cons x l)=(λ z. fst  (snd  z)) (cons x l)
 →βfst  (snd  (cons x l))
 =fst  (snd  ((λ x y. pair false  (pair  x y)) x l))
 ↠βfst  (snd  (pair false  (pair  x l)))
 ↠βfst  (pair  x l)
 ↠βx
tl (cons x l)=(λ z. snd  (snd  z)) (cons x l)
 →βsnd  (snd  (cons x l))
 =snd  (snd  ((λ x y. pair false  (pair  x y)) x l))
 ↠βsnd  (snd  (pair false  (pair  x l)))
 ↠βsnd  (pair  x l)
 ↠βl

Sellised definitsioonid ei võimalda meil aga defineerida rekursiivset foldr funktsiooni. Järgmisena vaatamegi, kuidas toetada rekursiivset käitumist.

9.5 Püsipunktid

Termi M nimetatakse püsipunktikombinaatoriks kui kehtib

∀ F.  M F  =β  F (M F).

Tuntuim püsipunktikombinaator on Haskell Curry “paradoksaalne” kombinaator

Y := λf. (λx. f (x x)) (λx. f (x x)).

Saame näidata, et Y on püsipunktikombinaator.

Y e
→β
(λx. e (x x)) (λx. e (x x))
         
  →β e ((λx. e (x x)) (λx. e (x x)))         
 
←βe
(Y e)
         

Pane tähele, et tõestuse viimase sammuna tuleb β-ekspansioon, mitte β-reduktsioon. Seetõttu on mõnevõrra raskendatud Y e-ks ekspandeerutava termi äratundmine suurema termi sees — kui meil oleks mingi konkreetne term e. Seetõttu võib olla kasulik kasutada hoopis “tugevat” püsipunkti kombinaatorit Θ.

Θ := (λ x y. y (x x y)) (λ x y. y (x x y))

Kombinaatori Θ kohta saame tõestada tugevama väite, et Θ e  →β  e (Θ e).

Θ e
→β 
(λx y. y (x x y)) (λx y. y (x x y))
e
         
 
→β 
(λy. y ((λx y. y (x x y)) (λx y. y (x x y)) y)) e
         
  →β  e ((λx y. y (x x y)) (λx y. y (x x y)) e)         
   =   e (Θ e)          

Püsipunktikombinaatoreid saab kasutada rekursiivsete funktsioonide defineerimiseks. Võtame näiteks standardnumbrite (naturaalarvude) liitmise. Rekursiivse väljakutse jaoks lisame definitsiooni esimese parameetri ja rakendame funktsiooni püsipunktikombinaatorile.

add := Y (λf x y. cond (iszero x) y (f (pred x)(succ y)))

Tõestame näiteks ka liitmise korrektsust, s.t, et add m n redutseerub avaldiseks m+n. Tõestuseks kasutame induktsiooni ja alustame baasjuhust m=0, kus kasutame add-i definitsiooni järel püsipunktikombinaatori võrdust Y f =β f (Y f), teadmist iszero 0 ↠β true ja cond true e1 e2 ↠β e1.

add
0
n
= Y (λf x y. cond (iszero x) y (f (pred x)(succ y)))
0
n
         
 
=β λf x y. cond (iszero x) y (f (pred x) (succ y))) add
0
n
         
 
↠β cond (iszero
0
)
n
(add (pred
0
) (succ
n
))
         
 
↠β cond true
n
(add (pred
0
) (succ
n
))
         
 
↠β
n
         

Jätkame induktsiooni sammu m=m′+1 tõestusega, kus kasutame add-i definitsiooni järel püsipunktikombinaatori võrdust Y f =β f (Y f), teadmist iszero m+1 ↠β false , teadmist pred m+1 ↠β m, teadmist succ n ↠β n+1, teadmist cond false e1 e2 ↠β e2 ning induktsiooni hüpoteesi ∀ k, add m k ↠β m+k.

add
(m′+1)
n
= Y (λf x y. cond (iszero x) y (f (pred x)(succ y)))
(m′+1)
n
         
 
=β λf x y. cond (iszero x) y (f (pred x) (succ y)) add
(m′+1)
n
         
 
↠β cond (iszero
(m′+1)
) y (add (pred
(m′+1)
) (succ
n
))
         
 
↠β add (pred
(m′+1)
) (succ
n
)
         
 
↠β add
m′
(n+1)
         
 
↠β
(m′+n+1)
=
((m′+1)+n)
         

Sarnaselt liitmisoperatsioonile add saame kõik rekursiivsed definitsioonid kirjutada mitterekursiivselt, kasutades mõnda püsipunktioperaatorit. Üldine muster on, et funktsioonile tuleb lisada esimene parameeter ja sellega asendada kõik rekursiivsed väljakutsed. Nii saame funktsiooni, mis töötab soovitud viisil kuni rekursiivse väljakutseni. Kuidas aga anda funktsioonile see esimene argument? Seda teeb püsipunktioperaator. Ehk kui soovime defineerida funktsiooni, mis käitub nagu võrdus f = λx y z. … (f a b c) … siis piisab meil see defineerida mitterekursiivse makrona f := Y (λf’ x y z. … (f’ a b c) …).

9.6 Ülesanded

Ülesanne 1  Väärtusta cond (cond true false true ) fst snd (pair false true ).
Ülesanne 2  Väärtusta  mul  0 3 ,  exp 0 3  ja  exp 2 0 .
Ülesanne 3  Defineeri listi pikkuse arvutamine length kasutades püsipunktikombinaatorit. Väärtusta  length (pair  0 (pair  1 nil)) .

Previous Up Next