Do sedaj smo spoznali ukazno in deklarativno programiranje:
Pri ukaznem programiranju na izvajanje programa gledamo kot na zaporedje akcij, ki spreminjajo stanje sistema (vrednosti spremenljivk).
Pri deklarativnem programiranju je program izraz, ki se preračuna v končno vrednost.
Logično programiranje izhaja iz ideje, da je izvajanje programa iskanje dokaza. Da bomo to razumeli, najprej ponovimo nekaj osnov logike.
V logiki prvega reda lahko zapišemo formule sestavljene iz konstant ⊥
, ⊤
, veznikov ∧
, ∨
, ⇒
, ¬
in kvantifikatorjev ∀
(za vsak), ∃
(obstaja). Take formule so lahko precej zapletene in niso primerne za logično programiranje, zato se omejimo na tako imenovane Hornove formule, ki so oblike
∀ x₁, …, xᵢ . (Φ₁ ∧ Φ₂ ∧ ⋯ ∧ Φⱼ ⇒ Ψ)
Tu so Φ₁
, …, Φⱼ
in Ψ
osnovne formule, se pravi vsaka od njih je oblike
p(t₁, …, tᵢ)
kjer je p
relacijski simbol in t₁
, …, tᵢ
termi. Nadalje je term izraz, ki ga lahko sestavimo iz konstant, funkcijskih simbolov in spremenljivk.
Poseben primer Hornove formule je dejstvo, ki ga dobimo pri j = 0
:
∀ x₁, …, xᵢ . Ψ
Drugi primer je formula brez kvantifikatorjev (v kateri ni spremenljivk, samo konstante), ki ga dobimo pri i = 0
:
Φ₁ ∧ Φ₂ ∧ ⋯ ∧ Φⱼ ⇒ Ψ
Poglejmo si nekaj primerov.
Hornova formula
∀ a . (pes(a) ⇒ zival(a))
pove, da so psi živali: “za vsak a
, če je a
pes, potem je a
žival”.
Hornova formula
∀ x y z . (otrok(x, y) ∧ otrok(y, z) ∧ zenska(z) ⇒ babica(x, z))
pravi: “za vse (osebe) x
, y
, z
, če je x
otrok od y
in y
otrok od z
in je z
ženska, potem je z
babica od x
”.
Formula
∀ x y z . otrok(x, z) ∧ otrok(y, z) ∧ zenska(x) ∧ zenska(y) ⇒ sestra(x, y)
ne pomeni “x
in y
sta sestri” ampak “x
in y
sta sestri ali polsestri, ali pa sta enaka”.
S Hornovimi formulami lahko izrazimo tudi matematična dejstva. Peanova aksioma za seštevanje se glasita
∀ n . n + 0 = n
∀ k m . k + succ(m) = succ (k + m)
Pravzaprav v Prologu ni funkcij, težko definiramo vsoto kot funkcijo! Definirati moramo relacijo, ki predstavlja funkcijo:
Pravimo, da relacija
R
predstavlja funkcijof
, če jef(x) = y ⇔ R(x, y)
.
Za funkcijo seštevanja: namesto operacije +
bomo definirali relacijo vsota
, da bo veljalo:
x + y = z ⇔ vsota(x, y, z)
S Hornovima formulama ju zapišemo takole, pri čemer vsota(x,y,z)
beremo “vsota x
in y
je z
”:
∀ n . vsota(n, zero, n)
∀ k m n . vsota(k, m, n) ⇒ vsota(k, succ(m), succ(n))
Prva formula očitno ustreza prvemu aksiomu, druga pa je ekvivalentna
∀ m n k . k + m = n ⇒ k + succ(m) = succ(n)
kar je ekvivalentno drugemu aksiomu (premisli zakaj!).
S Hornovimi formulami zapiši Peanova aksioma za množenje:
∀ n . n · 0 = 0
∀ k m . k · succ(m) = k + k · m
Uporabi relacijo vsota
iz prejšnjega primera, ter relacijo zmnozek(x,y,z)
, ki ga beremo “zmnožek x
in y
je z
”.
Nekaterih dejstev s Hornovimi formulami ne moremo izraziti, na primer negacije ne eksistenčnih formul ∃ x . ⋯
.
Denimo, da imamo Hornove formule in želimo vedeti, ali iz njih sledi dana izjava. Kako bi sistematično poiskali dokaz?
Najprej poglejmo primer brez kvantifikatorjev. Ali iz Hornovih formul
X ∧ Y ⇒ C
A ∧ B ⇒ C
X ⇒ B
A ⇒ B
A
sledi C
?
Iskanja dokaza se lotimo sistematično. Katera od formul bi lahko pripeljala do dokaza izjave C
? Prva ali druga. Poskusimo obe:
če uporabimo X ∧ Y ⇒ C
, C
prevedemo na podnalogi X
in Y
. A tu se zatakne, ker o X
in Y
ne vemo nič pametnega.
če uporabimo A ∧ B ⇒ C
, C
prevedemo na podnalogi A
in B
:
A
: to velja zaradi 5. formuleB
: uporabimo lahko 3. ali 4. formulo. Tretja ne deluje, četrta pa dokazovanje prevede na podnalogo A
, ki velja.Ali iz
∀ x y . otrok(x, y) ⇒ mlajsi(x, y)
otrok(miha, mojca)
sledi mlajsi(miha, mojca)
? Če v prvi formuli vzamemo x = miha
in y = mojca
, lahko nalogo prevedemo na otrok(miha, mojca)
. To pa velja zaradi druge formule.
Ali iz
∀ x . sodo(x) ⇒ liho(succ(x))
∀ y . liho(y) ⇒ sodo(succ(y))
sodo(zero)
sledi sodo(succ(succ(zero)))
? Tokrat zapišimo bolj sistematično postopek iskanja:
sodo(succ(succ(zero)))
y
vstavimo succ(zero)
in dobimo nalogoliho(succ(zero))
x
vstavimo zero
in dobimo nalogosodo(zero)
V splošnem bomo morali rešiti nalogo združevanja: poišči take vrednosti spremenljivk, da sta dani formuli enaki*. Podobno nalogo smo že reševali, ko smo obravnavali parametrični polimorfizem, kjer smo izenačevali tipe.
V logičnem programiranju je program podan s
H₁
, …, Hᵢ
inG
Pravila so Hornove formule. Poizvedba je formula oblike
∃ y₁, …, yⱼ . p(y₁, …, yⱼ)
Zanima nas, ali poizvedba sledi iz pravil. Poizvedbo G
predelamo na poizvedbe in iščemo v globino, takole:
V seznamu H₁
, …, Hᵢ
poišči prvo formulo, ki je oblike
∀ x₁, …, xᵢ . Φ₁(x₁, …, xᵢ) ∧ ⋯ ∧ φᵣ(x₁, …, xᵢ) ⇒ Ψ(x₁, …, xᵢ),
katere sklep Ψ
je združljiv z G
. To pomeni da lahko za y₁
, …, yⱼ
vstavimo neke vrednosti u₁
, …, uⱼ
in za x₁
, …, xᵢ
neke vrednosti v₁
, …,vᵢ
, da sta formuli
p(u₁, …, uⱼ)
in
Ψ(v₁, …, vᵢ)
enaki.
Opomba: možno je, da izbira vrednosti u₁
, …, uⱼ
in v₁
, …, vᵢ
ni enolična. V tem primeru izberemo najbolj splošne vrednosti. To naredimo s postopkom združevanja (angl. unification), ki smo ga spoznali pri parametričnem polimorfizmu.
Poizvedbo smo predelali na poizvedbe Φ₁(v₁, …, vᵢ)
, …, Φᵣ(v₁, …, vᵢ)
, ki jih rešujemo po vrsti rekurzivno. (Če se v teh poizvedbah pojavljajo spremenljivke, jih obravnavamo, kot da smo jih kvantificirali z ∃
.)
Poglejmo si še enkrat primer, ko imamo Hornove formule
sodo(zero)
∀ x . sodo(x) ⇒ liho(succ(x))
∀ y . liho(y) ⇒ sodo(succ(y))
in poizvedbo
∃ z . liho(z)
Ali lahko združimo sodo(zero)
in liho(z)
? Ne.
Ali lahko združimo liho(succ(x))
in liho(z)
? Poskusimo s postopkom združevanja: Enačbo
liho(succ(x)) = liho(z)
prevedemo na enačbo
succ(x) = z
Dobili smo rešitev za z
in ni več enačb, torej je x
poljuben. Torej uporabimo drugo pravilo, ki prevede nalogo na
∃ x . sodo(x)
Ali lahko to združimo s prvo formulo? Poskusimo rešiti
sodo(zero) = sodo(x)
Rešitev je x = zero
. Ker je prva formula dejstvo, ni nove podnaloge.
Rešitev se glasi: x = zero
, z = succ(x)
. Končna rešitev je torej
z = succ(zero)
Dokazali smo, da res obstaja liho število, namreč succ(zero)
.
Če v prejšnjem primeru zamenjamo vrstni red pravil,
∀ x . sodo(x) ⇒ liho(succ(x))
∀ y . liho(y) ⇒ sodo(succ(y))
sodo(zero)
potem poizvedba
∃ z . liho(z)
privede do neskončne zanke (ker iščemo v globino in vedno uporabimo prvo pravilo, ki deluje). Namreč, z uporabo prvega pravila dobimo poizvedbo
∃ x . sodo(x)
nato z uporabo drugega pravila (x = succ(y)
)
∃ y . liho(y)
nato z uporabo prvega pravila
∃ u . sodo(u)
in tako naprej. Tretje pravilo nikoli ne pride na vrsto!
Prolog je programski jezik, v katerem logično programiramo. Ima nekoliko nenavadno sintakso:
A ∧ B
pišemo A, B
A ∨ B
pišemo A; B
A ⇒ B
pišemo B :- A
(pozor, zamenjal se je vrstni red, B ⇐ A
!)∀ x …
in ∃ x …
ne pišemo, ampak kvantificirane spremenljivke pisemo z velikimi črkamiNa koncu vsake formule zapišemo piko.
Predelajmo primer iz prejšnjega razdelka v Prolog. Najprej v datoteko even_odd.pl
spravimo pravila (pri čemer pravila za sodo
zložimo skupaj, da se ne pritožuje):
sodo(zero).
sodo(succ(Y)) :- liho(Y).
liho(succ(X)) :- sodo(X).
Datoteko naložimo v interkativno zanko. Ta nam omogoča, da vpišemo poizvedbo in dobimo odgovor:
?- liho(Z).
Z = succ(zero) ;
Z = succ(succ(succ(zero))) ;
Z = succ(succ(succ(succ(succ(zero))))) .
Ko nam prolog poda oddgovor, lahko z znakom ;
zahtevamo, da išče še naprej. Z znakom .
zaključimo iskanje.
Ali se prolog res spusti v neskončno zanko, če zamenjamo vrsti red pravil za sodo
?
Na svoj računalnik si namesti SWI Prolog in poženi zgornji program.
Kako bi v Prologu naredili sezname? V Ocamlu smo jih definirali kot induktivni tip:
type 'a list = Nil | Cons of 'a * 'a list
Na primer, Cons(a, Cons(b, Cons(c, Nil)))
je seznam z elementi a
, b
in c
.
V Prologu ni tipov, lahko pa uporabljamo poljubne konstante in konstruktorje, le z malimi črkami jih je treba pisati. Torej lahko sezname še vedno predstavljamo z nil
in cons
.
Seznamov ni treba v naprej definirati, se pravi, ni treba razlagati, kaj sta nil
in cons
. Prolog ju obravnava kot simbola, s katerimi lahko tvorimo izraze. Seznam [a; b; c]
zapišemo cons (a, cons (b, cons (c, nil)))
.
Opomba: Prolog ima tudi vgrajene sezname, glej spodaj.
elem
Da bomo dobili občutek za moč logičnega programiranja, definirajmo nekaj funkcij za delo s seznami.
Naš prvi program je relacija, ki ugotovi, ali je dani X
pripada danemu seznamu L
:
elem(X, cons(X, _)).
elem(X, cons(_, L)) :- elem(X, L).
Poiskusimo (glej datoteko list.pl
:
?- elem(a, cons(b, cons(a, cons(c, cons(d, cons(a, nil)))))).
true ;
true ;
false.
Zakaj smo dvakrat dobili true
in nato false
?
Vprašamo lahko tudi, kateri so elementi danega seznama:
?- elem(X, cons(a, cons(b, cons(a, cons(c, nil))))).
X = a ;
X = b ;
X = a ;
X = c ;
false.
In celo, kateri seznami vsebujejo dani element!
?- elem(a, L).
L = cons(a, _3234) ;
L = cons(_3232, cons(a, _3240)) ;
L = cons(_3232, cons(_3238, cons(a, _3246))) ;
L = cons(_3232, cons(_3238, cons(_3244, cons(a, _3252)))) ;
L = cons(_3232, cons(_3238, cons(_3244, cons(_3250, cons(a, _3258))))) ;
L = cons(_3232, cons(_3238, cons(_3244, cons(_3250, cons(_3256, cons(a, _3264)))))) .
Prolog je ustvaril pomožne spremenljivke _XYZW
, s katerimi označi poljubne terme.
join
Funkcijo, ki stakne seznama predstavimo s trimestno relacijo join
:
join(X, Y, Z)
pomeni, da jeZ
enak stiku seznamovX
inY
.
Zapišimo pravila zanjo:
join(nil, Y, Y).
join(cons(A, X), Y, cons(A, Z)) :- join(X, Y, Z).
To je podobno funkciji, ki bi jo definirali v OCamlu:
let rec join x y=
match (x, y) with
| (Nil, y) -> y
| (Cons (a, x), y) ->
let z = join x y in
Cons (a, z)
Takole izračunamo stik seznamov cons(a, cons(b, nil))
in cons(x, cons(y, cons(z, nil)))
:
?- join(cons(a, cons(b, nil)), cons(x, cons(y, cons(z, nil))), Z).
Z = cons(a, cons(b, cons(x, cons(y, cons(z, nil))))).
Prolog že ima vgrajene sezname:
[e₁, e₂, …, eᵢ]
je seznam elementov e₁
, e₂
, …, eᵢ
.[e | ℓ]
je seznam z glavo e
in repom ℓ
[e₁, e₂, …, eᵢ | ℓ]
je seznam, ki se začne z elementi e₁
, e₂
, …, eᵢ
in ima rep ℓ
.Za delo s seznami je na voljo knjižnica lists
, ki jo naložimo z ukazom
:- use_module(library(lists)).
Ta že vsebuje relaciji member
(ki smo jo zgoraj imenovali elem
) in append
(ki smo jo zgoraj imenovali join
). Preizkusimo:
?- append([a,b,c], [d,e,f], Z).
Z = [a, b, c, d, e, f].
Lahko pa tudi vprašamo, kako razbiti seznam [a,b,c,d,e,f]
na dva podeznama:
?- append(X, Y, [a,b,c,d,e,f]).
X = [],
Y = [a, b, c, d, e, f] ;
X = [a],
Y = [b, c, d, e, f] ;
X = [a, b],
Y = [c, d, e, f] ;
X = [a, b, c],
Y = [d, e, f] ;
X = [a, b, c, d],
Y = [e, f] ;
X = [a, b, c, d, e],
Y = [f] ;
X = [a, b, c, d, e, f],
Y = [] ;
false.
Včasih v Prologu potrebujemo enakost in neenakost. Enakost pišemo s = t
in neenakost s \= t
.
Če bo čas, si bomo ogledali, kako v Prologu implementiramo tolmač za preprost ukazni programski jezik, glej comm.pl
.