Specifikacija, implementacija, abstrakcija
Vsebina
10. Specifikacija, implementacija, abstrakcija¶
10.1. Specifikacija & implementacija¶
Specifikacija (angl. specification) S
je zaheva, ki opisuje, kakšen
izdelek želimo.
Implementacija (angl. implementation) I
je izdelek. Implementacija I
zadošča specifikaciji S
, če ustreza zahtevam iz S
.
V programiranju je implementacija programska koda. Specifikacije podajamo na različne načine in jih pogosto razvijemo postopoma:
pogovor s stranko in analiza potreb
dokumentacija, ki jo razume stranka
tehnična dokumentacija za programerje
Brez specifikacije ne vemo, kaj je treba naprogramirati. Danes si bomo ogledali, kako v programskih jezikih poskrbimo za zapis specifikacij in kako programski jezik preveri, ali dana koda (implementacija) zadošča dani specifikaciji.
Omenimo še povezavo z algebro. V algebri poznamo algebraične strukture, na primer vektorske prostore, grupe, monoide, kolobarje, Boolove algebre, … Definicija takih struktur poteka v dveh korakih:
signatura pove, kakšne množice, konstante in operacije imamo
aksiomi povedo, kakšnim zakonam morajo zadoščati operacije
Primer
Matematično strukturo grupa opišemo takole:
signatura:
množica
G
operacija
·
:G × G → G
operacija
⁻¹
:G → G
konstanta
e
:G
aksiomi:
x · (y · z) = (x · y) · z x · e = x e · x = x x · x⁻¹ = e x⁻¹ · x = e
Primer
Matematično strukturo usmerjen graf opišemo takole:
signatura:
množica
V
(vozlišča)množica
E
(povezave)operacija
src : E → V
(začetno vozlišče povezave)operacija
trg : E → V
(končno vozlišče povezave)
aksiomi: ni aksiomov
Zakaj vse to razlagamo? Ker programski jeziki ponavadi omogočajo zapis signature v programskem jeziku, ne pa tudi aksiomov, saj jih prevajalnik ne more preveriti.
10.2. Vmesniki¶
Specifikaciji včasih rečemo tudi vmesnik (angl. interface), ker jo lahko razumemo kot opis, ki pove, kako se uporablja neko programsko kodo. Na primer, avtor programske knjižnice običajno objavi API (Application Programming Interface), ki ni nič drugega kot specifikacija, ki pove, kako deluje knjižnica.
Torej imamo (vsaj) dve uporabi specifikacij:
zahtevek za programsko kodo (specifikacija)
protokol za uporabo programske kode (vmesnik)
10.2.1. Vmesniki v Javi¶
V Javi je specifikacija S
podana z vmesnikom
public interface S {
...
}
v katerem lahko naštejemo metode. Tipe, ki nastopajo v specifikaciji, podamo kot generične razrede. Na primer, vmesnik za grupo bi zapisali takole:
public interface Group<G> {
public G getUnit();
public G multiply(G x, G y);
public G inverse(G x);
}
Vmesnik za usmerjeni graf:
public interface Graph<V, E> {
public V src(E e);
public V trg(E e);
}
Seveda v praksi nihče ne piše takih vmesnikov, tu samo razmišljamo o zvezi med matematičnimi signaturami in vmesniki v programskih jezikih. Kasneje bomo videli bolj uporabne vmesnike, ki opisujejo abstraktne podatkovne strukture.
10.2.2. Vmesniki v OCamlu¶
V OCamlu lahko podamo poljubno signaturo (tipe in vrednost), ne moremo pa zapisati aksiomov, ki jim zadoščajo. Takole zapišemo signaturo za grupo:
module type GROUP =
sig
type g
val mul : g * g -> g
val inv : g -> g
val e : g
end
In takole za usmerjeni graf:
module type DIRECTED_GRAPH =
sig
type v
type e
val src : e -> v
val trg : e -> v
end
10.3. Implementacija¶
Programski jeziki seveda omogočajo implementacijo, kar ni nič drugega kot pisanje kode, ki ustreza dani specifikaciji. Zanimivo pa je vprašanje, kako so posamezne enote implementacije organizirane v različnih jezikih.
10.3.1. Implementacija v Javi¶
V Javi implementiramo vmesnik I
tako, da definiramo razred C
, ki mu zadošča:
public class C implements I {
...
}
Razred lahko hkrati zadošča večim vmesnikom. (Opomba: podrazredi so mehanizem, ki se ne uporablja za specifikacijo.)
10.3.2. Implementacija v OCamlu¶
Implementacija v OCamlu se imenuje modul (angl. module). Modul je skupek definicij tipov in vrednosti, lahko pa vsebuje tudi še nadaljnje podmodule.
Nekaj primerov (nepraktičnih) implementacij grup podajmo tu, kasneje pa bomo videli bolj uporabne primere:
(* Najprej definiramo signature. *)
module type GROUP =
sig
type g
val mul : g * g -> g
val inv : g -> g
val e : g
end
module type DIRECTED_GRAPH =
sig
type v
type e
val src : e -> v
val trg : e -> v
end
(* Signaturo implementiramo z modulom ali strukturo.
Dana signatura ima lahko več implementacij. *)
module Z3 : GROUP =
struct
type g = Zero | One | Two
let e = Zero
let plus = function
| (Zero, y) -> y
| (x, Zero) -> x
| (One, One) -> Two
| (One, Two) -> Zero
| (Two, One) -> Zero
| (Two, Two) -> One
let mul = plus
let inv = function
| Zero -> Zero
| One -> Two
| Two -> One
end
module Z3' : GROUP =
struct
type g = int
let mul (x, y) = (x + y) mod 3
let inv x = (3 - x) mod 3
let e = 0
end
module K4 : DIRECTED_GRAPH =
struct
type v = V0 | V1 | V2 | V3
type e = E0 | E1 | E2 | E3 | E4 | E5
let src = function
| E0 -> V0
| E1 -> V1
| E2 -> V2
| E3 -> V3
| E4 -> V0
| E5 -> V1
let trg = function
| E0 -> V1
| E1 -> V2
| E2 -> V3
| E3 -> V0
| E4 -> V2
| E5 -> V3
end
module Cycle3 : DIRECTED_GRAPH =
struct
type v = int (* uporabimo 0, 1, 2 *)
type e = int (* uporabimo 0, 1, 2 *)
let src e = e
let trg e = (e + 1) mod 3
(* Graf z vozlišči 0, 1, 2 in povezavami 0, 1, 2:
src trg
0 : 0 --> 1
1 : 1 --> 2
2 : 2 --> 0
*)
end
(* Takole pa naredimo modul, ki je parametriziran s
strukturo. Kasneje bomo videli bolj uporabne primere. *)
module Cycle (S : sig val n : int end) : DIRECTED_GRAPH =
struct
type v = int
type e = int
let src k = k
let trg k = (k + 1) mod S.n
end
module C5 = Cycle(struct let n = 5 end)
module C15 = Cycle(struct let n = 15 end)
10.4. Abstrakcija¶
Ko gradimo večje programske sisteme, so ti sestavljeni iz enot, ki jih povezujemo med seboj. Za vsako enoto je lahko zadolžena ločena ekipa programerjev. Programerji opišejo programske enote z vmesniki, da vedo, kaj kdo počne in kako uporabljati kodo ostalih ekip.
A to je le del zgodbe. Denimo, da prva ekipa razvija programsko enoto E
, ki
zadošča vmesniku S
in da druga ekipa uporablja enoto E
pri izdelavi svoje
programske enote. Dobra programska praksa pravi, da se druga ekipa ne sme
zanašati na podrobnosti implementacije E
, ampak samo na to, kar je zapisano v
specifikaciji S
. Na primer, če E
vsebuje pomožno funkcijo f
, ki je S
ne
omenja, potem je druga ekipa ne sme uporabljati, saj je f
namenjena notranji
uporabi E
. Prva ekipa lahko f
spremeni ali zbriše, saj f
ni del
specifikacije S
.
Če sledimo načelu, da mora programski jezik neposredno podpirati aktivnosti
programerjev, potem bi želeli skriti podrobnosti implementacije E
tako, da
bi lahko programerji druge ekipe imeli dostop samo do tistih delov E
, ki so
našteti v S
.
Kadar skrijemo podrobnosti implementacije, pravimo, da je implementacija abstraktna.
Programski jeziki omogočajo abstrakcijo v večji ali manjši meri:
Java nadzoruje dostopnost do komponent z določili
private
,public
inprotected
Python nima nikakršne abstrakcije
OCaml omogoča abstrakcijo z določilom
M : S
, kjer jeM
module inS
signatura. S tem skrijemo vsebino modulaM
, razen tistih komponent, ki so naštete vS
.
10.5. Generično programiranje¶
Z izrazom generično programiranje razumemo kodo, ki jo lahko uporabimo večkrat na različne načine. Na primer, če napišemo knjižnico za 3D grafiko, bi jo želeli uporabljati na več različnih grafičnih karticah. Ali bomo za vsako grafično kartico napisali novo različico knjižnice? Ne! Želimo generično implementacijo, ki bo preko ustreznega vmesnika dostopala do grafične kartice. Proizvajalci grafičnih kartic bodo implementirali gonilnike, ki bodo zadoščali temu vmesniku.
10.5.1. Generično progarmiranje v Javi¶
Java podpira generično programiranje. Ko definiramo razred, je ta lahko odvisen od kakega drugega razreda:
public class Knjiznica3D<Driver extends GraphicsDriver> {
...
}
10.5.2. Generično programiranje v OCamlu¶
V OCamlu je generično programiranje omogočeno s funktorji (angl. functor) (opomba: v matematiki poznamo funktorje v teoriji kategorij, ki nimajo nič skupnega s funktorji v OCamlu).
Funktor je preslikava iz struktur v strukture in je bolj splošen kot generični razredi v Javi (ker lahko struktura vsebuje podstrukture in definicije večih tipov, razred pa ne more vsebovati definicij podrazredov).
Funktor F
, ki sprejme strukturo A
, ki zadošča signaturi S
, in vrne
strukturo B
zapišemo takole:
module F(A : S) =
struct
⟨definicija strukture B⟩
end
Zgoraj smo videli primer preprostega funktorja Cycle
, ki sprejme strukturo s številom n
in vrne usmerjeni cikel na
n
vozliščih. Bolj uporaben primer sledi.
10.6. Primer: prioritetne vrste¶
Prioritetna vrsta je podatkovna struktura, v katero dodajamo elemente, ven pa jih jemljemo glede na njihovo prioriteto. Zapišimo specifikacijo:
Signatura:
podatkovni tip
element
operacija
priority : element → int
podatkovni tip
queue
konstanta
empty : queue
operacija
put : element → queue → queue
operacija
get : queue → element option * queue
Aksiomov ne bomo pisali, ker bi morali v tem primeru spoznati bolj zahtevne jezike za specifikacijo, ki presegajo okvir te lekcije. Neformalno pa lahko opišemo zahteve za prioritetno vrsto:
element
je tip elementov, ki jih hranimo v vrstipriority x
vrne prioriteto elementax
, ki je celo število. Manjše število pomeni »prej na vrsti«queue
je tip prioritetnih vrstempty
je prazna prioritetna vrsta, ki ne vsebuje elementovput x q
vstavi elementx
v vrstoq
glede na njegovo prioriteto in vrne tako dobljeno vrstoget q
vrne(Some x, q')
kjer jex
element izq
z najnižjo prioriteto inq'
vrstaq
brezx
Operacijaget
vrne(None, q)
, če jeq
prazna vrsta.
10.6.1. Implementacija v OCamlu¶
Oglejmo si implementacijo v OCamlu.
module type PRIORITY_QUEUE =
sig
type element
val priority : element -> int
type queue
val empty : queue
val put : element -> queue -> queue
val get : queue -> element option * queue
end
module MyFirstQueue : PRIORITY_QUEUE with type element = int * int =
struct
type element = int * int
let priority (a, b) = a
type queue = element list
let empty = []
let rec put x = function
| [] -> [x]
| y :: ys ->
if priority x <= priority y then
x :: y :: ys
else
y :: put x ys
let get = function
| [] -> (None, [])
| x :: xs -> (Some x, xs)
end
module type PRIORITY =
sig
type t
val priority : t -> int
end
(* Implementacija prioritetne vrste s seznami. To je funktor, ki
sprejme tip elemetov in prioritetno funkcijo. *)
module ListQueue (M : PRIORITY) : PRIORITY_QUEUE with type element = M.t
=
struct
type element = M.t
let priority = M.priority
type queue = element list
let fortytwo = 42
let empty = []
let rec put x = function
| [] -> [x]
| y :: ys ->
if priority x <= priority y then
x :: y :: ys
else
y :: put x ys
let get = function
| [] -> (None, [])
| x :: xs -> (Some x, xs)
end
(* Naredimo prioritetno vrsto nizov, prioriteta je dolžina niza. *)
module A =
ListQueue(
struct
type t = string
let priority = String.length
end)
(* Preizkus. *)
let example1 =
A.get (A.put "kiwi" (A.put "jabolko" (A.put "banana" A.empty)))
(* Naredimo prioritetno vrsto nizov, prioriteta je dolžina niza. *)
module A' =
ListQueue(
struct
type t = string
let priority s = - String.length s
end)
(* Preizkus. *)
let example1' =
A'.get (A'.put "kiwi" (A'.put "jabolko" (A'.put "banana" A'.empty)))
(* Naredimo prioritetno vrsto parov števil. *)
module B =
ListQueue(
struct
type t = int * int
let priority (a,b) = a
end
)
module IntQueue =
ListQueue(
struct
type t = int
let priority k = k
end
)
(* Učinkovita implementacija z levičarskimi kopicami,
glej https://en.wikipedia.org/wiki/Leftist_tree.
Implementacija je abstraktna, ker uporabimo :,
vendar dodamo določilo, da je tip element enak tipu t.
*)
module LeftistHeapQueue (M : PRIORITY)
: PRIORITY_QUEUE with type element = M.t =
struct
type element = M.t
let priority = M.priority
type queue = Leaf | Node of int * element * queue * queue
let rank = function
| Leaf -> 0
| Node (r, _, _, _) -> r
let node (x, a, b) =
if rank a < rank b then
Node (1 + rank a, x, b, a)
else
Node (1 + rank b, x, a, b)
let rec meld a b =
match (a, b) with
| (_, Leaf) -> a
| (Leaf, _) -> b
| (Node (_, ka, la, ra), Node (_, kb, lb, rb)) ->
if priority ka < priority kb then
node (ka, la, meld ra b)
else
node (kb, lb, meld a rb)
let singleton x = Node (1, x, Leaf, Leaf)
let empty = Leaf
let put x q = meld q (singleton x)
let get = function
| Leaf -> (None, Leaf)
| Node (_, y, l, r) -> (Some y, meld l r)
end
module D = LeftistHeapQueue(
struct
type t = int * int
let priority (x, y) = x + y
end)
let example2 =
let rec loop q = function
| 0 -> D.put (0, 0) q
| k -> loop (D.put ((47 * k * k + 13) mod 1000, k) q) (k - 1)
in
loop D.empty 300000
let rec to_list q =
match D.get q with
| (None, _) -> []
| (Some x, q) -> x :: to_list q
10.6.2. Implementacija v Javi¶
Oglejmo si še implementacijo v Javi. V tem jeziku je bolj naravno narediti vrste kot objekte, ki se spreminjajo. Torej spremenimo specifikacijo.
Signatura:
podatkovni tip
Element
metoda
priority : element → int
podatkovni tip
queue
operacija
empty : unit → queue
operacija
is_empty : queue → bool
operacija
put : element → queue → unit
operacija
get : queue → element option
Zahteve so podobne kot prej, le da operacije empty
, put
in get
delujejo
nekoliko drugače:
empty ()
vrne nov primerek (objekt) prazne vrsteput x q
vstavix
v vrstiq
in s tem spremeniq
get q
vrne prvix
v vrstiq
in s tem spremeniq
Zgornjo specifikacijo predelamo v dva vmesnika. Prvi je vmesnik za razrede, ki imajo metodo priority
:
public interface Priority {
public int priority();
}
Vmesnik PriorityQueue
pa podaja specifikacijo za prioritetno vrsto:
public interface PriorityQueue<Element extends Priority> {
public PriorityQueue<Element> emptyQueue();
public boolean isEmpty();
public void put(Element x);
public Element get();
}
Še primer implementacije prioritetnih vrst s seznami:
import java.util.LinkedList;
public class ListQueue<Element extends Priority> implements PriorityQueue<Element> {
private LinkedList<Element> elements;
public ListQueue() {
elements = new LinkedList<Element>();
}
@Override
public boolean isEmpty() {
return elements.isEmpty();
}
@Override
public void put(Element x) {
int i = 0;
for (Element y : elements) {
if (x.priority() < y.priority()) {
break;
} else {
i += 1;
}
}
elements.add(i, x);
}
@Override
public Element get() {
return elements.removeFirst();
}
@Override
public PriorityQueue<Element> emptyQueue() {
return new ListQueue<Element>();
}
}
10.7. Primer: množice¶
Na vajah boste na dva načina implementirali končne množice, opisane z naslednjo signaturo: signaturo.
type order = Less | Equal | Greater
module type SET =
sig
type element
val cmp : element -> element -> order
type set
val empty : set
val member : element -> set -> bool
val add : element -> set -> set
val remove : element -> set -> set
end