-- Monada za verjetnostno računanje. -- -- Izračun predstavlja končno verjetnostno porazdelitev nad poljubnim tipom. -- Porazdelitev elemtov tipa a predstavimo s seznamom parov (izid, verjetnost): -- seznam [(x_1, p_1), ..., (x_n, p_n)] pomeni, da izid x_i nastopi z -- verjetnostjo p_i. Vsota verjetnosti je največ 1; če je strogo manjša, -- manjkajoča verjetnost ustreza neuspelim ali zavrženim izračunom. module Verjetnost where -- Iz standardne knjižnice uvozimo nekaj funkcij za delo s seznami, -- ki jih bomo potrebovali v funkciji `zdruzi`. import Data.List (sortBy, groupBy) import Data.Ord (comparing) import Data.Function (on) -- Definicija tipa za verjetnostno porazdelitev. -- -- Ključna beseda `newtype` v Haskellu definira nov tip, ki ima natanko -- en konstruktor in eno polje, in pri tem ne uvede dodatne ovojnice v -- pomnilniku. Tip `Verjetnost a` je v bistvu `[(a, Float)]`, -- vendar ga Haskell obravnava kot ločen tip. Tako ga lahko opremimo z -- lastnimi instancami razredov tipov (Functor, Applicative, Monad). -- -- Zapis `{ izidi :: [(a, Float)] }` poleg konstruktorja `Verjetnost` -- definira še konstruktor -- izidi :: Verjetnost a -> [(a, Float)] -- s katerim iz vrednosti tipa `Verjetnost a` dobimo nazaj seznam parov. newtype Verjetnost a = Verjetnost { izidi :: [(a, Float)] } -- Instanca razreda `Show` določa, kako naj se vrednost tipa -- `Verjetnost a` izpiše v GHCi. Vsak izid postavimo v svojo vrstico, -- ločeno s tabulatorjem od njegove verjetnosti. instance Show a => Show (Verjetnost a) where show (Verjetnost xs) = concat [show x ++ ":\t" ++ show p ++ "\n" | (x, p) <- xs] -- Da bi tip `Verjetnost` postal monada v smislu Haskella, moramo podati -- tri instance: `Functor`, `Applicative` in `Monad`. Razred `Monad` -- namreč podeduje od `Applicative`, ta pa od `Functor`. -- Functor: na vsakem izidu uporabimo čisto funkcijo `f`, verjetnost pa -- ostane nespremenjena. Tako npr. `fmap (+10) kocka` pretvori izide -- 1..6 v 11..16, vsak še vedno z verjetnostjo 1/6. instance Functor Verjetnost where fmap f (Verjetnost xs) = Verjetnost [(f x, p) | (x, p) <- xs] -- Applicative: -- * `pure x` je porazdelitev, ki z verjetnostjo 1 vrne izid `x`. -- To je čisti izračun, ki vedno vrne isto vrednost. -- * `<*>` izvede dva neodvisna izračuna in zmnoži njuni verjetnosti, -- saj je verjetnost neodvisnih dogodkov produkt njihovih verjetnosti. instance Applicative Verjetnost where pure x = Verjetnost [(x, 1)] (Verjetnost fs) <*> (Verjetnost xs) = Verjetnost [(f x, p * q) | (f, p) <- fs, (x, q) <- xs] -- Monad: ključna operacija `>>=` (beri "veži") sestavi dva izračuna -- v zaporedje. Za vsak izid `x` z verjetnostjo `p` v prvem izračunu -- poženemo nadaljnji izračun `f x`, ki vrne svojo porazdelitev. Vsako -- verjetnost v tej drugi porazdelitvi pomnožimo s `p`. Tako dobimo -- skupno verjetnost zaporedja dveh dogodkov. instance Monad Verjetnost where return = pure (Verjetnost xs) >>= f = Verjetnost [(y, p * q) | (x, p) <- xs, (y, q) <- izidi (f x)] -- Pomožna funkcija `zdruzi`: sešteje verjetnosti enakih izidov in -- vrne urejen seznam različnih izidov. -- -- OPOMBA: matematično bi to združevanje sodilo neposredno v operacije -- `>>=` in `<*>`, saj je sumiranje verjetnosti enakih izidov del same -- definicije sestavljanja porazdelitev. V Haskellu pa tega ne moremo -- doseči, ker razreda `Monad` in `Applicative` ne dovolita dodatnih -- omejitev na tipovne parametre: `>>=` mora delovati za poljuben tip -- izidov `a`, ne le za tiste z `Eq a` ali `Ord a`. Brez primerjanja -- pa ne moremo prepoznati enakih izidov. Združevanje zato naknadno -- opravimo z `zdruzi`, ki potrebuje omejitev `Ord a`. zdruzi :: Ord a => Verjetnost a -> Verjetnost a zdruzi (Verjetnost xs) = Verjetnost [ (k, sum (map snd gs)) | gs@((k, _) : _) <- groupBy ((==) `on` fst) (sortBy (comparing fst) xs) ] -- Pomožna funkcija `pogoj`: zavrže veje izračuna, ki ne ustrezajo pogoju. -- * Če pogoj velja, vrnemo enotni izid `()` z verjetnostjo 1; -- `>>=` nato z verjetnostjo 1 nadaljuje s preostankom izračuna. -- * Če pogoj ne velja, vrnemo prazno porazdelitev. Ker `>>=` množi -- verjetnosti, se s tem tista veja izračuna "ubije" in njena -- verjetnost je izgubljena. -- Uporabljamo jo v `do` blokih za filtriranje izidov. preveri :: Bool -> Verjetnost () preveri True = Verjetnost [((), 1)] preveri False = Verjetnost [] -- Primer monade. Met poštene šestostrane kocke: vsak izmed izidov -- 1, 2, ..., 6 nastopi z verjetnostjo 1/6. kocka :: Verjetnost Int kocka = Verjetnost [(n, 1/6) | n <- [1..6]] -- Primer 1: porazdelitev vsote pri metu dveh kock. -- -- V `do` zapisu vrstica `x <- kocka` pomeni "vrzi kocko in njen izid -- poimenuj `x`"; podobno za `y`. Ker je `>>=` desugarizacija `do`, -- se v ozadju za vsako izbiro `x` razveji vseh šest možnih izbir za -- `y`. Skupaj dobimo 36 enako verjetnih parov (x, y), vsak z -- verjetnostjo (1/6)*(1/6) = 1/36. Z `return (x + y)` vsak par -- pretvorimo v njegovo vsoto. -- -- Naključne vsote se ponovijo: 7 dobimo kot 1+6, 2+5, 3+4, 4+3, 5+2, -- 6+1, torej z verjetnostjo 6/36 = 1/6. Brez `zdruzi` bi izpis imel -- 36 vrstic, s klicem `zdruzi` pa 11 vrstic za različne vsote 2..12. vsotaDveh :: Verjetnost Int vsotaDveh = zdruzi $ do x <- kocka y <- kocka return (x + y) -- Primer 2: porazdelitev vsote dveh metov, pri čemer met zavržemo, -- če je vsota soda. -- -- Polovica vseh 36 parov ima sodo vsoto, zato vsota verjetnosti -- preostalih izidov ni 1, temveč 1/2. Dobimo torej "pod-porazdelitev", -- v kateri manjkajoča verjetnost ustreza zavrženim metom. lihaVsotaDveh :: Verjetnost Int lihaVsotaDveh = zdruzi $ do x <- kocka y <- kocka preveri (odd (x + y)) return (x + y)