Definicije tipov

Poleg bogatega nabora vgrajenih tipov si tipe v OCamlu lahko definiramo tudi sami.

Okrajšave tipov

Najenostavnejši način za definicijo tipov so okrajšave obstoječih tipov. Na primer, tip za \(\mathbb{R}^3\) si lahko definiramo kot:

type r3 = float * float * float
type r3 = float * float * float

Tako kot na primer vgrajeni tip list lahko tudi naši tipi vsebujejo parametre:

type 'a zaporedje = int -> 'a

type ('k, 'v) slovar = ('k * 'v) list
type 'a zaporedje = int -> 'a
type ('k, 'v) slovar = ('k * 'v) list

Če tip sprejme več parametrov (na primer slovar ima tako tip ključev kot tip vrednosti), jih lahko naštejemo v oklepajih.

Zapisni tipi

Recimo, da si definiramo kompleksna števila s pari realnih:

type kompleksno = float * float
type kompleksno = float * float

Kako bi izračunali absolutno vrednost kompleksnega števila? Ena možnost je:

let abs (x, y) = sqrt (x ** 2. +. y ** 2.)
val abs : float * float -> float = <fun>

Toda če smo v mislih imeli polarni zapis, je pravilna definicija:

let abs (r, _fi) = r
val abs : 'a * 'b -> 'a = <fun>

Zmešnjavi se lahko izognemo, če obe komponenti poimenujemo. V OCamlu to storimo z zapisnimi tipi, ki jih podamo tako, da naštejemo imena polj ter njihove tipe:

type kartezicno = {re : float; im : float}
type polarno = {radij : float; kot : float}
type kartezicno = { re : float; im : float; }
type polarno = { radij : float; kot : float; }

Vrednosti tipov pišemo podobno, le da jih podamo z =:

let i = {re = 0.0; im = 1.0}
val i : kartezicno = {re = 0.; im = 1.}

Do komponent dostopamo z zapis.ime_polja:

let abs z = sqrt (z.re ** 2. +. z.im ** 2.)
val abs : kartezicno -> float = <fun>
let abs' z = z.radij
val abs' : polarno -> float = <fun>

Kljub temu, da zapise pišemo podobno kot Pythonove slovarje, gre za popolnoma različni strukturi. Zapisi so v resnici kartezični produkti, le da so komponente poimenovane, imena polj pa niso vrednosti, ki bi si jih lahko podajali naokoli.

Vsote

Najzanimivejši tip, ki ga lahko definiramo, pa so vsote. Podamo jih tako, da naštejemo možne variante, od katerih je vsaka podana s svojim konstruktorjem. Če se želimo omejiti na fiksno množico velikosti oblačil, lahko na primer napišemo enostavno vsoto s petimi variantami:

type velikost = XS | S | M | L | XL
type velikost = XS | S | M | L | XL

Tedaj bo tip imel natanko pet možnih vrednosti in OCaml nas bo opozoril, če poskusimo uporabiti nenavedeno varianto:

[XS; XS; M; S; XL; L; L]
- : velikost list = [XS; XS; M; S; XL; L; L]
[XS; XS; L; M; M; XM]
File "[13]", line 1, characters 18-20:
1 | [XS; XS; L; M; M; XM]
                      ^^
Error: This variant expression is expected to have type velikost
       The constructor XM does not belong to type velikost

Vsaka izmed naštetih konstruktorjev lahko sprejme tudi argumente vnaprej določenega tipa:

type geometrijski_objekt =
  | Tocka
  | Krog of float
  | Pravokotnik of float * float
type geometrijski_objekt =
    Tocka
  | Krog of float
  | Pravokotnik of float * float
[Tocka; Pravokotnik (1., 2.); Tocka; Krog 3.]
- : geometrijski_objekt list = [Tocka; Pravokotnik (1., 2.); Tocka; Krog 3.]

Tako kot vsote naštejemo po kosih, lahko prek match ali function po kosih tudi definiramo funkcije na njih.

let povrsina obj =
  match obj with
  | Tocka -> 0.
  | Krog r -> 3.14 *. r ** 2.
  | Pravokotnik (v, s) -> v *. s
val povrsina : geometrijski_objekt -> float = <fun>
let obseg =
  function
  | Tocka -> 0.
  | Krog r -> 2. *. 3.14 *. r
  | Pravokotnik (v, s) -> 2. *. (v +. s)
val obseg : geometrijski_objekt -> float = <fun>

Tip option

Včasih imamo opravka s funkcijami, ki jih ne moremo povsod dobro definirati. Na primer, glava seznama je definirana samo za sestavljeni seznam. Če želimo, lahko naštejemo le to možnost, vendar bomo potem dobili napako ob izvajanju.

let slaba_glava (x :: _) = x
File "[18]", line 1, characters 16-28:
1 | let slaba_glava (x :: _) = x
                    ^^^^^^^^^^^^
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val slaba_glava : 'a list -> 'a = <fun>
slaba_glava [1; 2; 3]
- : int = 1
slaba_glava []
Exception: Match_failure ("[18]", 1, 16).
Raised at slaba_glava in file "[18]", line 1, characters 16-28
Called from Toploop.load_lambda in file "toplevel/toploop.ml", line 212, characters 17-27

Varnejši način je, da uporabimo tip 'a option, ki predstavlja morebitno vrednost tipa 'a:

type 'a option = None | Some of 'a

Nato bi lahko glavo seznama definirali tako, da vedno vrne vrednost:

let glava = function
  | [] -> None
  | x :: _ -> Some x
val glava : 'a list -> 'a option = <fun>
glava [1; 2; 3]
- : int option = Some 1
glava []
- : 'a option = None

Če uporabimo tip option, nas tipi prisilijo, da obravnavamo robne primere. Na primer, prej bi lahko napisali:

let ali_je_slaba_glava_velika xs = slaba_glava xs > 100
val ali_je_slaba_glava_velika : int list -> bool = <fun>

Če uporabimo varnejši način, pa rezultata ne moremo neposredno primerjati s 100:

let ali_je_glava_velika xs = glava xs > 100
File "[25]", line 1, characters 40-43:
1 | let ali_je_glava_velika xs = glava xs > 100
                                            ^^^
Error: This expression has type int but an expression was expected of type
         'a option

Tipi nas prisilijo, da obravnavamo vse primere in se odločimo, kaj naredimo, če podatka ni na voljo. Lahko bi se na primer odločili, da seznam brez glave nima velike glave:

let ali_je_glava_velika xs =
  match glava xs with
  | None -> false
  | Some x -> x > 100
val ali_je_glava_velika : int list -> bool = <fun>

Lahko bi se odločili tudi drugače, na primer da spet vrnemo morebitni odgovor, v vsakem primeru pa ne bomo pozabili na noben primer:

let ali_je_glava_velika xs =
  match glava xs with
  | None -> None
  | Some x -> Some (x > 100)
val ali_je_glava_velika : int list -> bool option = <fun>

Vsote so lahko definirane tudi rekurzivno. Takim tipom pravimo tudi induktivni ali algebrajski tipi. Najenostavnejši primer induktivnega tipa so naravna števila. Predstavimo jih z vsoto z dvema konstruktorjema Nic in Naslednik, pri čemer slednji sprejme en argument, ki je zopet naravno število.

type naravno = Nic | Naslednik of naravno
type naravno = Nic | Naslednik of naravno

Vsoto naravnih števil podamo z običajno rekurzivno definicijo:

let rec vsota m n =
  match m with
  | Nic -> n
  | Naslednik m' -> Naslednik (vsota m' n)
val vsota : naravno -> naravno -> naravno = <fun>

Še en že poznan primer induktivnega tipa so seznami. Vsak seznam je bodisi prazen, bodisi sestavljen iz glave in repa:

type 'a list =
  | Prazen
  | Sestavljen of 'a * 'a list
type 'a list = Prazen | Sestavljen of 'a * 'a list

Sedaj tudi vidimo, zakaj :: lahko uporabljamo v vzorcih - ni namreč običajna funkcija za sestavljanje seznamov, temveč konstruktor tipa seznamov.

Induktivne tipe se pogosto uporablja za predstavitev izrazov določenega formalnega jezika. Na primer, aritmetične izraze gradimo iz števil ter aritmetičnih operacij. Take izraze bi lahko predstavili s tipom:

type izraz =
  | Stevilo of int
  | Plus of izraz * izraz
  | Minus of izraz
  | Krat of izraz * izraz
type izraz =
    Stevilo of int
  | Plus of izraz * izraz
  | Minus of izraz
  | Krat of izraz * izraz

Na primer, izrazu \(-(5 \times (2 + 7))\) bi ustrezala vrednost

let i = Minus (
  Krat (Stevilo 5, Plus (Stevilo 2, Stevilo 7))
)
val i : izraz = Minus (Krat (Stevilo 5, Plus (Stevilo 2, Stevilo 7)))

Za vajo lahko napišete rekurzivno funkcijo izracunaj : izraz -> int, ki dani izraz prevori v njegovo vrednost. Na primer, za zgornji izraz bi funkcija vrnila -45.

Še en induktivni tip, ki ga bomo podrobneje spoznali v kratkem, pa so dvojiška drevesa. Dvojiško drevo je bodisi prazno bodisi ima koren, v katerem je shranjena vrednost, ter dva otroka, ki sta zopet drevesi, na primer (pri čemer praznih dreves ne kažemo):

Tip dvojiških dreves podamo s tipom

type 'a drevo =
  | Prazno
  | Sestavljeno of 'a drevo * 'a * 'a drevo
type 'a drevo = Prazno | Sestavljeno of 'a drevo * 'a * 'a drevo