Paradygmaty programowania/Wykład 8: U podstaw programowania funkcyjnego — rachunek lambda

Z Studia Informatyczne
Wersja z dnia 18:01, 31 lip 2006 autorstwa Pitab (dyskusja | edycje)
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)
Przejdź do nawigacjiPrzejdź do wyszukiwania

==Wprowadzenie== Omawiamy podstawy z dawna obiecywanego rachunku lambda. Choć rzecz wygląda niepozornie, w istocie kryją się za nią ważne własności programowania funkcyjnego i --- pośrednio --- imperatywnego. Chodzi tu np. o uściślenie fundamentalnego pojęcia funkcji i sposobu jej obliczania oraz o system typów. Rachunek lambda dobrze też sobie radzi z funkcjami wyższego rzędu, co w językach programowania wciąż nie jest powszechne.

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ΛxVar, to (λx.M)Λ. Powyższy sposób tworzenia termu nazywamy abstrakcją.

  • Inna metoda to metoda aplikacji, czyli jeżeli termy MΛ

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}}}

α-konwersja i β-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ść Church-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):

(70,70) (28,0){M} (10,30){(1,-1){20}} (26,11){*} (18,6){β} (60,30){(-1,-1){20}} (36,11){*} (43,6){β}

(-3,32){N1} (60,32){N2}

(30,60){(-1,-1){20}} (6,41){*} (13,37){β} (40,60){(1,-1){20}} (58,41){*} (49,36){β} (30,62){M}

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 „\beginarray”): {\displaystyle \beginarray{lclcl|l} \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)} \endarray }

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 } nParser nie mógł rozpoznać (błąd składni): {\displaystyle 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 } nParser nie mógł rozpoznać (błąd składni): {\displaystyle 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: mn, a w wyniku daje procedurę liczącą liczbę naturalną (będącą sumą liczb m, 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 „\beginarray”): {\displaystyle \beginarray{lll|l} A\underline{1}\ \underline{2} & = & \{[\lambda m.\ (\lambda n.\ (\lambda sx.\ (ms)(nsx))]\underline{1}\} \underline{2} & \mbox{dopisując nawiasy i } Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & [\lambda n.\ (\lambda sx.\ (\underline{2}s)(nsx)]\underline{1} & m := 2\\ & =_\beta & \lambda sx.\ (\underline{2} s)(\underline{1} sx) & n := 1\\ & = & \lambda sx.\ (\underline{2} s)\{[(\lambda t.\ (\lambda y.\ ty))s]x\} & \mbox{dopisując nawiasy i~} Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & \lambda sx.\ (\underline{2} s)[(\lambda y.\ sy)x] & t := s\\ & =_\beta & \lambda sx.\ (\underline{2} s)(sx) & 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 } Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & \lambda sx.\ (\lambda y.\ s(sy))(sx) & t := s\\ & =_\beta & \lambda sx.\ s(s(sx)) & y := sx\\ & = & \underline{3} \endarray }

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 mn, 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 „\beginarray”): {\displaystyle \beginarray{lll|l} M\underline{2}\ \underline{2} & = & \{[\lambda m.\ (\lambda n.\ (\lambda sx.\ n(ms)x))]\underline{2}\} \underline{2} & \mbox{dopisując nawiasy i } Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & [\lambda n.\ (\lambda sx.\ n(\underline{2}s)x)]\underline{2} & m := 2\\ & =_\beta & \lambda sx.\ \underline{2} (\underline{2} s)x & 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~} Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & \lambda sx.\ \underline{2} [\lambda y.\ s(sy)] x & 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 } Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & \lambda sx.\ (\lambda z.\ [\lambda y. s(sy)] ([\lambda y. s(sy)]z))x & t := [\lambda y. s(sy)]\\ & =_\beta & \lambda sx.\ [\lambda y. s(sy)] ([\lambda y. s(sy)]z) & z := x\\ & =_\beta & \lambda sx.\ s(s[s(sx)]) & y := s(sx)\\ & = & \underline{4} \endarray }

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.

Parser nie mógł rozpoznać (nieznana funkcja „\beginarray”): {\displaystyle \beginarray[t]{lll} \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} \endarray }
Parser nie mógł rozpoznać (nieznana funkcja „\beginarray”): {\displaystyle \beginarray[t]{lll} 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}_{\mbox{\tiny } m 0Parser nie mógł rozpoznać (błąd składni): {\displaystyle 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 \endarray }

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, a 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 „\beginarray”): {\displaystyle \beginarray{lll|l} E\underline{2}\ \underline{2} & = & [\lambda mn.\ (mn)] \underline{2}\ \underline{2}\\ & =_\beta & \underline{2}\ \underline{2} & m := \underline{2},\ n := \underline{2}\\ & = & [\lambda sx.\ s(sx)]\underline{2} & s := \underline{2}\\ & =_\beta & \lambda x.\ \underline{2} (\underline{2} x) & x := s\\ & =_\alpha & \lambda s.\ \underline{2} (\underline{2} s)\\ & = & \lambda s. [\lambda tx.\ t(tx)](\underline{2} s)\\ & =_\beta & \lambda sx.\ (\underline{2} s)((\underline{2} s)x) & 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)) & t := s,\ p := s,\ z := x\\ & =_\beta & \lambda sx.\ s(s[s(sx)]) & y := s(sx)\\ & = & \underline{4} \endarray }

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:

(100,110) (30,10){<n, n1>} (25,30){(0,-1){15}} (-10,20){krok n}

(35,40){...}

(30,50){<2, 1>} (25,70){(0,-1){15}} (-10,60){krok 2}

(30,70){<1, 0>} (25,90){(0,-1){15}} (-10,80){krok 1}

(30,90){<0, 0>}

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 „\beginarray”): {\displaystyle \beginarray{lll} 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} \endarray }

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 „\beginarray”): {\displaystyle \beginarray{lcl|l} {\cal Y} M & = & (\lambda y. (\lambda x. y(xx))(\lambda x. y(xx)))M & \mbox{z definicji } { Y}Parser nie mógł rozpoznać (błąd składni): {\displaystyle }\\ & =_\beta & (\lambda x. M(xx))\underbrace{(\lambda x. M(xx))}_B & y := M\\ & =_\beta & M(\underbrace{(\lambda x. M(xx))}_B \underbrace{(\lambda x. M(xx))}_B ) & x := B\\ & =_\beta & M({\cal Y} M) & {\cal Y} M= (\lambda x. M(xx))(\lambda x. M(xx)) \endarray }

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. 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 „\beginarray”): {\displaystyle \beginarray{l|cl} \mbox{\rm\bf Term:} & \mbox{}\hspace*{1ex} & \mbox{\rm\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 \endarray }

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.