Paradygmaty programowania/Wykład 8: U podstaw programowania funkcyjnego — rachunek lambda: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Wkm (dyskusja | edycje)
Wkm (dyskusja | edycje)
Linia 465: Linia 465:


<center><math>\aligned
<center><math>\aligned
\cal{Y} M & = & ( \lambda y. (\lambda x. y(xx))(\lambda x. y(xx)))M \qquad \text{z definicji} \cal{Y} \\
{\cal Y} M & = & ( \lambda y. (\lambda x. y(xx))(\lambda x. y(xx)))M &
& =_\beta & (\lambda x. M(xx))\underbrace{(\lambda x. M(xx))}_B \qquad y := M\\
\mbox{~~~~~~| z definicji } {\cal Y} \\
& =_\beta & M(\underbrace{(\lambda x. M(xx))}_B \underbrace{(\lambda x. M(xx))}_B ) \qquad x := B\\
& =_\beta & (\lambda x. M(xx))\underbrace{(\lambda x. M(xx))}_B & \mbox{~~~~~~| } y := M\\
& =_\beta & M( \cal{Y} M ) \qquad \cal{Y} M = (\lambda x. M(xx))(\lambda x. M(xx))\\
& =_\beta & M(\underbrace{(\lambda x. M(xx))}_B \underbrace{(\lambda x. M(xx))}_B ) &
\mbox{~~~~~~| } x := B\\
& =_\beta & M( {\cal Y} M ) &
\mbox{~~~~~~| } {\cal Y} M = (\lambda x. M(xx))(\lambda x. M(xx))\\
\endaligned
\endaligned
</math></center>
</math></center>

Wersja z 08:31, 26 wrz 2006


Wstęp

W latach trzydziestych XX wieku Alonzo Church i Haskell Curry, pracując niezależnie, opracowali notację dla funkcji — rachunek lambda. Spojrzenie na funkcje w rachunku lambda jest nieco inne od klasycznego, teoriomnogościowego rozumienia funkcji jako zbioru par argument-wartość. W rachunku lambda funkcje rozumiane są jako przepisy, aby podkreślić ich obliczalność. Rachunek lambda ma dwa zasadnicze warianty: nietypowany, zwany po prostu rachunkiem lambda, oraz typowany. Najpierw przedstawimy nieco obszerniej ten pierwszy; później powiemy krótko o rachunku z typami.

Podstawowe definicje i własności

Napisy w rachunku lambda nazywamy termami. Zbiór Λ termów rachunku lambda konstruuje się indukcyjnie jako zbiór słów nad alfabetem Var{(,),λ}, gdzie Var jest przeliczalnym zbiorem ziennych. Do zbioru Λ należą tylko termy skonstruowane w poniższy sposób:

  • Dowolna zmienna ze zbioru Var jest termem rachunku lambda, czyli jeżeli xVar, to xΛ.
  • Jeżeli weźmiemy dowolny term M ze zbioru Λ, oraz dowolną zmienną x ze zbioru Var, to term postaci (λx.M) jest termem rachunku lambda, czyli jeżeli MΛ i xVar, to (λx.M)Λ. Powyższy sposób tworzenia termu nazywamy abstrakcją.
  • Inna metoda to metoda aplikacji, czyli jeżeli termy MΛ i NΛ, to (MN)Λ.

Nawiasy są często pomijane według zasady łączności lewostronnej: na przykład MNPQ(((MN)P)Q). Wieloargumentowe funkcje są reprezentowane przez funkcje jednoargumentowe, których argumentami są funkcje. Zatem to, co zwykle jest zapisywane w postaci F(N,M), w rachunku lambda zapisuje się ((FN)M)FNM. Powtórzenia symbolu λ pomijane są według zasady łączności prawostronnej, tzn. λxyz.M(λx.(λy.(λz.M))).

W termie rachunku lambda zmienna może być wolna albo związana. Wolnym wystapieniem zmiennej x nazywamy tylko takie wystąpienie tej zmiennej, które nie jest „w zasięgu” żadnego λx.

Zbiór zmiennych wolnych termu M, oznaczany przez FV(M), zdefiniowany jest, w zależności od budowy termu M, w następujący sposób:

  • Jeżeli M jest pojedynczą zmienną, tzn. Mx, to zmienna ta jest wolna, a zatem FV(M)={x}.
  • Jeżeli M jest postaci λx.M, to λx wiąże wszystkie wolne wystąpienia zmiennej x w termie M, a zatem FV(M)=FV(M){x}.
  • Jeżeli M jest postaci PQ, to zmiennymi wolnymi termu M są wszystkie zmienne wolne występujące w termach PQ, czyli FV(M)=FV(P)FV(Q).

Zmienna jest zmienną wolną termu M, jeżeli należy do zbioru FV(M),zmienną związaną w przeciwnym wypadku.

Term M jest domknięty wtedy i tylko wtedy, gdy FV(M)=.

Przykład

  • W termie M=y(λx.x) zmienna y jest zmienną wolną, a x zmienną związaną. W termie λy.M nie ma zmiennych wolnych, jest to więc term domknięty.
  • W termie M=x(λx.x) tylko pierwsze wystąpienie zmiennej x jest wolne, chociaż FV(M)={x}. Oczywiście term M nie jest termem domkniętym.

Na termy w rachunku lambda patrzymy jako na przepisy, jak z zadanej wartości argumentu otrzymać wartość funkcji; zatem termy to procedury, których parametry specyfikowane są poprzez lambda abstrakcję:


Parser nie mógł rozpoznać (błąd składni): {\displaystyle \lambda \underbrace{x_1... x_n}_{\mbox{\tiny parametry}}. \underbrace{M}_{\mbox{\tiny ciało procedury}}}


Alfa-konwersja i beta-redukcja, czyli obliczanie

Termy rachunku lambda możemy przekształcać, korzystając z α-konwersji i β-redukcji.

α-konwersja to po prostu zamiana nazw zmiennych związanych. Zdarza się, że na przykład poprzez stosowanie „podprocedur” lub parametrów pochodzących z wykonania innych procedur dochodzi do konfliktu nazw zmiennych. Aby zapewnić poprawne wykonanie, stosujemy α-konwersję, która pozwala zapewnić unikalność nazw.


λx.Mαλy.M[x/y],


gdzie y∉FV(M), a M[x/y] oznacza wynik postawienia zmiennej y za każde wolne wystąpienie zmiennej x w termie M.

Będziemy używać oznaczenia T1=αT2, aby powiedzieć, że termy T1T2 są równe modulo α-konwersja, tzn. że mogą być zredukowane do tego samego termu dzięki zastosowaniu skończonej liczby α-konwersji.

Przykład Niech x, y, z oznaczają różne zmienne. Wówczas


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned \lambda x.x & =_\alpha & \lambda y.y\\ \lambda x.xz & =_\alpha & \lambda y.yz\\ \lambda x.xy & \neq_{\alpha} & \lambda x.xz. \endaligned}


β-redukcja — obliczenia wykonywane przez procedurę symulowane są w rachunku lambda poprzez proces zwany β-redukcją. Aplikujemy procedurę λx.M do argumentu N:


(λx.M)NβM[x/N]


w taki sposób, że każda zmienna wolna termu N pozostaje wolna po podstawieniu. Jest to oczywiście możliwe do osiągnięcia po ewentualnym wcześniejszym zastosowaniu α-konwersji. Na przykład:


(λxy.xy)yλy.yy


jest nieprawidłowym użyciem β-konwersji, gdyż dochodzi do konfliktu oznaczeń i do utraty pierwotnego znaczenia termu-procedury. Jednakże można temu zaradzić, stosując α-konwersję:


(λxy.xy)y=α(λxz.xz)y=βλz.yz.


Używamy oznaczenia T1=βT2, aby powiedzieć, że termy T1T2 są równe modulo β-konwersja, tzn. że mogą być zredukowane do tego samego termu dzięki zastosowaniu skończonej liczby β-konwersji.

Aplikację PQ nazywamy β-redeksem, jeżeli term P jest w postaci λ-abstrakcji. Term M jest w postaci normalnej, jeżeli nie zawiera β-redeksów, tzn. żaden podterm termu M nie jest β-redeksem.

Własność Churcha-Rossera

Poniższe twierdzenie zwane jest własnością Churcha-Rossera lub własnością karo:

Jeżeli term M poprzez pewne ciągi β-redukcji redukuje się do termów N1N2, to istnieje term M taki, że zarówno N1, jak i N2 można zredukować do M (poprzez ciągi β-redukcji):


Paradygmaty programowania/Moduł8
Paradygmaty programowania/Moduł8


Powyższa własność mówi, że postać normalna (o ile istnieje) jest unikalna z dokładnością do α-konwersji.

Term T jest normalizowalny, jeżeli można go zredukować do postaci normalnej, a jest silnie normalizowalny, jeżeli każda droga redukcji prowadzi do postaci normalnej. Beztypowy rachunek lambda nie ma żadnej z tych własności. Wprowadźmy oznaczenie: Δ=λx.xx. Najprostszym przykładem termu nie posiadającego postaci normalnej jest ΔΔ:


ΔΔ=(λx.xx)Δ=βΔΔ.


Przykładem termu, w którym nie każda droga redukcji prowadzi do postaci normalnej, jest:


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned \overbrace{(\lambda x.y) (\underbrace{\Delta\Delta}_{(1)})}^{(2)} & =_\beta & (\lambda x.y)(\Delta\Delta) & =_\beta & ... & \mbox{redukując (1)}\\ & =_\beta & y & & & \mbox{redukując (2)} \endaligned }


Istnieją strategie redukcji, które zawsze doprowadzają do postaci normalnej, oczywiście gdy postać normalna istnieje. Taką strategią jest na przykład metoda redukowania zawsze najbardziej lewego redeksu.

Lambda-definiowalność funkcji na liczbach naturalnych

Do tej pory nie przedstawiliśmy żadnych termów oznaczających liczby naturalne czy funkcje operujące na liczbach naturalnych, wydaje się więc rzeczą dziwną mówienie o lambda reprezentowalności tych funkcji. Okazuje się jednak, że w rachunku lambda nie potrzeba żadnych dodatkowych oznaczeń: pojęcie liczby i funkcji daje się wprowadzić na bazie do tej pory przyjętych definicji.

Popatrzmy na liczbę naturalną n jak na procedurę, która n-krotnie stosuje następnik do zera. Oczywiste jest, że taka procedura w sposób jednoznaczny określa liczbę, i na odwrót. Mając dany przepis wiemy, jaką liczbę naturalną liczy, a mając liczbę naturalną doskonale wiemy, który przepis zastosować.

  • Term λsx. x reprezentuje liczbę naturalną 0.
  • Term Parser nie mógł rozpoznać (błąd składni): {\displaystyle \lambda sx.\ \underbrace{s( s( ... (s}_{\mbox{\tiny n razy}}x) ...))} reprezentuje liczbę naturalną n.

Term λsx. x to po prostu algorytm: „weź następnik s, weź zero x i zwróć zero”; natomiast term Parser nie mógł rozpoznać (błąd składni): {\displaystyle \lambda sx.\ \underbrace{s( s( ... (s}_{\mbox{\tiny n razy}}x) ...))} to algorytm: „weź następnik s, weź zero x, zastosuj n-krotnie następnik do zera i zwróć wynik”. Zatem liczba naturalna w naszym rozumieniu to procedura, która ma dwa parametry: s (następnik) oraz x (zero).

Oczywiście powyższe termy podane są z dokładnością do α-konwersji. Np. term λty.t(ty) reprezentuje 2, gdyż λty.t(ty)=αλsx.s(sx).

Mówimy, że term E rachunku lambda reprezentuje funkcję na liczbach naturalnych e:k wtedy i tylko wtedy, gdy En1_...nk_=αβe(n1,...,nk)_, gdzie n_ oznacza reprezentację liczby n w rachunku lambda.


Operacje na liczbach

Znając reprezentację liczb naturalnych w rachunku lambda, możemy zdefiniować podstawowe operacje na liczbach.

Term

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned A & = & \lambda mnsx. (ms)(nsx) \endaligned}

reprezentuje dodawanie. Jest to procedura, która oczekuje na podanie dwóch parametrów: m i n, a w wyniku daje procedurę liczącą liczbę naturalną (będącą sumą liczb m i n). Jak wygląda algorytm otrzymany w wyniku działania procedury A? Jest to algorytm, który m-krotnie stosuje następnik do n.


Przykład Zobaczmy, jak działa term A na liczbach 1 i 2:

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned A\underline{1}\ \underline{2} & = & \{[\lambda m.\ (\lambda n.\ (\lambda sx.\ (ms)(nsx))]\underline{1}\} \underline{2} & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & [\lambda n.\ (\lambda sx.\ (\underline{2}s)(nsx)]\underline{1} & \mbox{~~~~~~| } m := 2\\ & =_\beta & \lambda sx.\ (\underline{2} s)(\underline{1} sx) & \mbox{~~~~~~| } n := 1\\ & = & \lambda sx.\ (\underline{2} s)\{[(\lambda t.\ (\lambda y.\ ty))s]x\} & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & \lambda sx.\ (\underline{2} s)[(\lambda y.\ sy)x] & \mbox{~~~~~~| } t := s\\ & =_\beta & \lambda sx.\ (\underline{2} s)(sx) & \mbox{~~~~~~| } y := x\\ & = & \lambda sx.\ \{\underbrace{[ \lambda t.\ (\lambda y. t(ty))]}_{\mbox{\tiny procedura licząca 2}} s\} \underbrace{(sx)}_{\mbox{\tiny „zero”}} & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & \lambda sx.\ (\lambda y.\ s(sy))(sx) & \mbox{~~~~~~| } t := s\\ & =_\beta & \lambda sx.\ s(s(sx)) & \mbox{~~~~~~| } y := sx\\ & = & \underline{3} \endaligned}

Term

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned M & = & \lambda mnsx. n(ms)x \endaligned}

reprezentuje mnożenie. Procedura mnożenia oczekuje na podanie dwóch argumentów m i n, a w wyniku daje procedurę, która liczy liczbę naturalną w sposób następujący: m-krotnie dodaje liczbę n, zaczynając od zera. Liczba naturalna obliczona w taki sposób to mn.

Przykład

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned M\underline{2}\ \underline{2} & = & \{[\lambda m.\ (\lambda n.\ (\lambda sx.\ n(ms)x))]\underline{2}\} \underline{2} & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & [\lambda n.\ (\lambda sx.\ n(\underline{2}s)x)]\underline{2} & \mbox{~~~~~~| } m := 2\\ & =_\beta & \lambda sx.\ \underline{2} (\underline{2} s)x & \mbox{~~~~~~| } n := 2\\ & = & \lambda sx.\ \underline{2} [ \underbrace{(\lambda t.\ (\lambda y.\ t(ty)))}_{\mbox{\tiny procedura licząca 2}}s]x & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & \lambda sx.\ \underline{2} [\lambda y.\ s(sy)] x & \mbox{~~~~~~| } t := s\\ & = & \lambda sx.\ \underbrace{[ \lambda t.\ (\lambda z. t(tz))]}_{\mbox{\tiny procedura licząca 2}} \underbrace{[\lambda y. s(sy)]}_{\mbox{\tiny dodawanie 2}} x & \mbox{~~~~~~| dopisując nawiasy i } \lambda\\ & =_\beta & \lambda sx.\ (\lambda z.\ [\lambda y. s(sy)] ([\lambda y. s(sy)]z))x & \mbox{~~~~~~| } t := [\lambda y. s(sy)]\\ & =_\beta & \lambda sx.\ [\lambda y. s(sy)] ([\lambda y. s(sy)]z) & \mbox{~~~~~~| } z := x\\ & =_\beta & \lambda sx.\ s(s[s(sx)]) & \mbox{~~~~~~| } y := s(sx)\\ & = & \underline{4} \endaligned}

Term

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned C & = & \lambda mnksx. m(\lambda y.nsx)(ksx) \endaligned}

reprezentuje funkcję ifthenelse. Term C oczekuje na podanie trzech argumentów i tak jak w poprzednich przykładach zwraca procedurę obliczającą liczbę naturalną. W przypadku, gdy pierwszy parametr m jest zerem, procedura wynikowa liczy liczbę k, w przeciwnym przypadku -- liczbę n.

Przykład Zobaczmy dwa przykłady działania termu C -- pierwszy, gdy m=0, oraz drugi, gdy m0.

Przykład pierwszy

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned \mbox{} C \underline{0}\ \underline{n}\ \underline{k} & =_\beta & \lambda sx.\ \underline{0} (\lambda y.\ \underline{n} sx) (\underline{k} sx)\\ & = & \lambda sx.\ \underbrace{(\lambda tz. z)}_{\underline{0}} (\lambda y.\ \underline{n} sx)(\underline{k}sx) \\ & =_\beta & \lambda sx.\ \underline{k} sx \\ & =_\beta & \underline{k} \endaligned}

Przykład drugi

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned C \underline{m}\ \underline{n}\ \underline{k} & =_\beta & \lambda sx. \underline{m} (\lambda y. \underline{n} sx) (\underline{k} sx) \\ & = & \lambda sx.\ [\lambda tz.\ \underbrace{t(... (t}_{\tiny m\neq 0 razy}y) ...)] (\lambda y.\ \underline{n} sx)(\underline{k} sx) \\ & =_\beta & \lambda sx. [\lambda y.\ \underline{n} sx] (... ([\lambda y.\ \underline{n} sx](\underline{k} sx)) ...)\\ & =_\beta & \lambda sx. \underline{n} sx \endaligned}

Przekazanie funkcji jako parametru

Musimy częściowo zapomnieć o naszych intuicjach, gdy spojrzymy na term reprezentujący funkcję wykładniczą. Co prawda jest to term, który jak wszystkie poprzednie oczekuje na podanie (dwóch) parametrów,  w wyniku daje procedurę liczącą liczbę naturalną, ale trudno tu dopatrzeć się analogii do tych języków programowania, które przekazują parametry tylko przez wartość.

Term


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned E & = & \lambda mn. (mn) \endaligned}


reprezentuje funkcję e(m,n)=nm i ma pewną „niezwykłą” własność: w wyniku zwraca algorytm, który „zero” jednego z parametrów traktuje jako następnik. Oto przykład:


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned E\underline{2}\ \underline{2} & = & [\lambda mn.\ (mn)] \underline{2}\ \underline{2}\\ & =_\beta & \underline{2}\ \underline{2} & \mbox{~~~~~~| } m := \underline{2},\ n := \underline{2}\\ & = & \underbrace{[\lambda sx.\ s(sx)]}_{\underline{2}}\underline{2} &\\ & =_\beta & \lambda x.\ \underline{2} (\underline{2} x) & \mbox{~~~~~~| } s := \underline{2}\\ & =_\alpha & \lambda s.\ \underline{2} (\underline{2} s) & \mbox{~~~~~~| } x := s\\ & = & \lambda s. [\lambda tx.\ t(tx)](\underline{2} s)\\ & =_\beta & \lambda sx.\ (\underline{2} s)((\underline{2} s)x) & \mbox{~~~~~~| } t := \underline{2} s\\ & = & \lambda sx.\ (\underbrace{[\lambda ty. t(ty)]}_{\underline{2}}s) ((\underbrace{[\lambda pz. p(pz)]}_{\underline{2}}s)x)\\ & =_\beta & \lambda sx.\ [\lambda y.\ s(sy)](s(sx)) & \mbox{~~~~~~| } t := s,\ p := s,\ z := x\\ & =_\beta & \lambda sx.\ s(s[s(sx)]) & \mbox{~~~~~~| } y := s(sx)\\ & = & \underline{4} \endaligned }


Ciekawym termem jest też term reprezentujący poprzednik:


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned P & = & \lambda n. (n (\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0})\!\!>) <\!\!\underline{0}, \underline{0}\!\!>) \underline{1}, \endaligned}


gdzie A oznacza wspominany już wcześniej term dodawania, z jest parą uporządkowaną, której pierwsza składowa to z0_, natomiast druga to z1_. Jaką procedurę opisuje term P? Mówi on tyle: „weź liczbę naturalną n, a następnie n razy wykonaj algorytm weź parę z i utwórz nową parę <1+z0,z0>, startując od pary <0_,0_>; jako wynik zwróć drugą składową ostatnio otrzymanej pary”. Uwaga: term rerprezentujący parę uporządkowaną zapisujemy tu w nawiasach kątowych, by odróżnić je od nawiasów grupujących fragmenty termów.

Wygląda to następująco:

Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned <0, 0> & \mbox{~}\\ <1, 0> & \mbox{~~~~~~krok 1}\\ <2, 1> & \mbox{~~~~~~{krok 2}}\\ ... \\ <n, n-1> & \mbox{~~~~~~krok } n \endaligned }


Zatem term P definiuje funkcję p(n)=(f(n)(0,0))2, gdzie f(x,y)=<x+1,x>.

Przykład Zobaczmy teraz, jak działa term opisujący powyższą procedurę. Policzmy:


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned P\underline{2} & = & \{\lambda n. (n (\lambda z. <\!\!A\underline{1} (z\underline{0}), (z\underline{0}) \!\!>) <\!\!\underline{0}, \underline{0}\!\!>)\underline{1}\}\underline{2}\\ & =_\beta & (\underline{2} (\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0}) \!\!> ) <\!\!\underline{0}, \underline{0}\!\!> )\underline{1}\\ & = & ((\underbrace{\lambda sx.\ s(sx)}_{\mbox{\tiny procedura licząca 2}}) (\underbrace{\lambda z. <\!\!A\underline{1}(z\underline{0}), (z\underline{0}) \!\!>}_{\mbox{\tiny następnik}}) \underbrace{<\!\!\underline{0}, \underline{0}\!\!>}_{\mbox{\tiny zero}}) \underline{1}\\ & =_\beta & ((\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0}) \!\!> ) [(\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0}) \!\!> ) <\!\! \underline{0}, \underline{0} \!\!>])\underline{1}\\ & =_\beta & ((\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0}) \!\!> ) <\!\! A\underline{1} \underline{0}, \underline{0} \!\!> )\underline{1}\\ & =_\beta & ((\lambda z. <\!\! A\underline{1} (z\underline{0}), (z\underline{0}) \!\!> ) <\!\! \underline{1}, \underline{0} \!\!> )\underline{1}\\ & =_\beta & ( <\!\! A\underline{1} \underline{1}, \underline{1}\!\!> ) \underline{1}\\ & =_\beta & ( <\!\! \underline{2}, \underline{1}\!\!> ) \underline{1}\\ & =_\beta & \underline{1} \endaligned }

Operator punktu stałego

Podaliśmy tutaj tylko przykłady funkcji reprezentowalnych w rachunku lambda. Okazuje się, że beztypowy rachunek lambda to model obliczeń równoważny funkcjom rekurencyjnym (obliczalnym). Oznacza to, że każdą funkcję obliczalną można reprezentować w rachunku lambda oraz że każdy term rachunku lambda reprezentuje pewną funkcję obliczalną.

Aby móc dowolną funkcję rekurencyjną reprezentować w rachunku lambda, konieczne jest wprowadzenie operatora punktu stałego 𝒴. Operator punktu stałego to term, który dla dowolnego termu M daje jego punkt stały, tzn.


𝒴M=βM(𝒴M).


Zauważmy, że term


𝒴=λy.(λx.y(xx))(λx.y(xx))


jest oczekiwanym operatorem. Zobaczmy, jak term 𝒴 zachowuje się zaaplikowany do dowolnego termu M:


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned {\cal Y} M & = & ( \lambda y. (\lambda x. y(xx))(\lambda x. y(xx)))M & \mbox{~~~~~~| z definicji } {\cal Y} \\ & =_\beta & (\lambda x. M(xx))\underbrace{(\lambda x. M(xx))}_B & \mbox{~~~~~~| } y := M\\ & =_\beta & M(\underbrace{(\lambda x. M(xx))}_B \underbrace{(\lambda x. M(xx))}_B ) & \mbox{~~~~~~| } x := B\\ & =_\beta & M( {\cal Y} M ) & \mbox{~~~~~~| } {\cal Y} M = (\lambda x. M(xx))(\lambda x. M(xx))\\ \endaligned }

Typowany rachunek lambda

Znaczenie typowanego rachunku lambda dla informatyki bierze się z analogii do struktury typów w typowanych językach programowania. W rachunku tym każda zmienna ma przypisany typ, tzn. zakresem jej zmienności jest zbiór opisywany przez ten typ. Typ wyrażenia konstruuje się indukcyjnie z typów jego składowych. Term może być stworzony poprzez aplikację funkcji do argumentu jedynie wtedy, gdy typy funkcji i argumentu są zgodne, tzn. argument należy do dziedziny funkcji.

Rachunek lambda z typami prostymi wprowadzony został przez Churcha i Curry'ego. Jednakże istnieją istotne różnice pomiędzy zaproponowanymi przez nich systemami typów. W systemie Curry'ego termy typowanego rachunku lambda są po prostu wyróżnione spośród termów beztypowego rachunku poprzez relację ustalającą typy. Tak więc term λx.x jest typowalny i należy do wyróżnionego zbioru termów, a term λx.xx typowalny nie jest i do zbioru nie należy. W systemie Churcha reguły nadające typy zostały wbudowane w reguły tworzenia termów. Nie będziemy omawiać żadnego z systemów konkretnie, a jedynie przedstawimy intuicje związane z systemem Churcha nadawania typów.

Zakładamy, że mamy nieskończony, przeliczalny zbiór zmiennych (różnych od zmiennych ze zbioru Var). Typ to wyrażenie zdefiniowane indukcyjnie:

  • Każda zmienna jest typem, zwanym typem atomowym.
  • Jeżeli στ są typami, to (στ) jest typem, zwanym typem złożonym.

Typy atomowe oznaczamy najczęściej α, β, γ, a typy ogólnie σ, τ, ... Nawiasy są często pomijane zgodnie z zasadą łączności prawostronnej, tzn.

γστ(γ(στ)).


Fakt, że term M jest typu τ (ma przypisany typ τ), zapisujemy jako Mτ lub poprzez indeks dolny, na przykład λx.Mτ. Typ, który nie może zostać przypisany żadnemu termowi, nazywamy typem pustym.

Termom nadajemy typy w następujący sposób:

  • Jeżeli term M jest pojedynczą zmienną typu τ, to M jest typu τ, czyli Mτ.
  • Jeżeli term M=λx:τ.N:σ, to M(τσ). Innymi słowy, jeżeli tworzymy program, który oczekuje na parametr typu τ, a zwraca wynik typu σ, to całość jest typu τσ.
  • Jeżeli term M=PQ, gdzie P(τσ), a Qτ, to Mσ. Innymi słowy, jeżeli aplikujemy „funkcję” P typu τσ do argumentu Q typu τ, to wynik jest typu σ.

Pytanie, czy term jest danego typu, jest problemem rozstrzygalnym, tzn. istnieje algorytm, który dla zadanego termu M oraz typu τ odpowiada, czy M jest typu τ. Rozstrzygalny jest także problem, czy danemu termowi przypisać można typ zgodnie z zasadami opisanymi powyżej.

Przykład


Parser nie mógł rozpoznać (nieznana funkcja „\aligned”): {\displaystyle \aligned \mbox{\bf Term:} & & \mbox{\bf Typ:}\\ \hline \lambda x : \alpha.x & & \alpha\to \alpha\\ \lambda x : \alpha\to \beta. x & & (\alpha\to \beta)\to (\alpha\to \beta)\\ \lambda x : \alpha. \lambda y : \beta. x & & \alpha\to (\beta\to \alpha)\\ \lambda x : \alpha\to \alpha. \lambda y : \alpha. x & & (\alpha\to \alpha)\to (\alpha\to (\alpha\to \alpha))\\ \lambda x : \alpha. \lambda y : \beta. y & & \alpha\to (\beta\to \beta)\\ \lambda x : \alpha\to \beta. \lambda y : \gamma\to \alpha. \lambda z : \gamma. x(yz) & & (\alpha\to \beta)\to((\gamma\to \alpha)\to (\gamma\to \beta))\\ \lambda x.xx & & \mbox{term nie jest typowalny} \\ \lambda xyz.xy(xz) & & \mbox{term nie jest typowalny} \\ \mbox{typ pusty} & & (\alpha\to \alpha) \to \beta\\ \mbox{typ pusty} & & ((\alpha\to \beta)\to \alpha)\to \alpha \endaligned }


Jeżeli τ1,,τn, τ są typami, to zapis τ1,...,τnτ oznacza typ τ1(τ2...(τnτ)...). Zatem każdy typ może być przedstawiony w postaci τ1,...,τnα, gdzie n0, a α jest typem atomowym.