Veriun verĂ«n e kaluar mora pjesĂ« nĂ« â njĂ« program pĂ«r studentĂ«t nga kompaninĂ« Google. Ădo vit, organizatorĂ«t pĂ«rzgjidhin disa projekte Open Source, duke pĂ«rfshirĂ« nga organizata tĂ« njohura si dhe . PĂ«r tĂ« punuar mbi kĂ«to projekte, Google fton studentĂ« nga e gjithĂ« bota.Â
Si pjesĂ«marrĂ«s nĂ« Google Summer of Code 2019, unĂ« realizova njĂ« projekt brenda bibliotekĂ«s nĂ« organizatĂ«n , e cila merret me zhvillimin e gjuhĂ«s Haskell â njĂ« nga gjuhĂ«t mĂ« tĂ« njohura tĂ« programimit funksional. Alga Ă«shtĂ« njĂ« bibliotekĂ« qĂ« ofron tĂ« pĂ«rfaqĂ«simit pĂ«r grafĂ«t nĂ« Haskell. Ajo pĂ«rdoret, pĂ«r shembull, nĂ« â bibliotekĂ«n e kompanisĂ« Github, e cila ndĂ«rton pemĂ« semantike, grafĂ« thirrjesh dhe varĂ«sish nga kodi dhe Ă«shtĂ« nĂ« gjendje t'i krahasojĂ« ato. Projekti im pĂ«rfshinte shtimin e njĂ« pĂ«rfaqĂ«simi tĂ« sigurt pĂ«r grafĂ«t dy-pjesĂ«sh dhe algoritme pĂ«r kĂ«tĂ« pĂ«rfaqĂ«sim.Â
NĂ« kĂ«tĂ« postim do tĂ« flas pĂ«r implementimin tim tĂ« algoritmit pĂ«r verifikimin e grafit pĂ«r dy-pjesshmĂ«ri nĂ« Haskell. MegjithĂ«se algoritmi Ă«shtĂ« njĂ« nga mĂ« bazikĂ«t, implementimi i tij tĂ«rheqĂ«s nĂ« stilin funksional mĂ« mori disa iteracione dhe kĂ«rkoi njĂ« punĂ« tĂ« konsiderueshme. Si rezultat, vendosa pĂ«r njĂ« implementim me transformatorĂ« monad.Â

Për vetë
Më quajnë Vasily Alfyrov, jam student i vitit të katërt në Universitetin e Peterburgut. Më parë në blog kam shkruar dhe . Tani po bëj një praktikë në në Norvegji, ku merrem me qasjet ndaj problemit të . Fushat e mia të interesit përfshijnë algoritmet e parametrizuara dhe programimin funksional.
Rreth implementimit të algoritmit
Parathënie
StudentĂ«t qĂ« marrin pjesĂ« nĂ« program, kĂ«shillohen me ngulm tĂ« mbajnĂ« njĂ« blog. MĂ« kanĂ« ofruar njĂ« platformĂ« pĂ«r blogun . Ky artikull Ă«shtĂ« njĂ« pĂ«rkthim , i shkruar prej meje pĂ«r aty nĂ« korrik nĂ« anglisht, me njĂ« parathĂ«nie tĂ« vogĂ«l.Â
Pull Request me kodin që po flitet, mund ta gjeni .
Për rezultatet e punës sime mund të lexoni (në anglisht) .
Postimi supozon njohjen e lexuesit me konceptet bazë në programimin funksional, edhe pse do të përpiqem të rikujtoj të gjitha termat e përdorura, kur të arrijë koha për to.
Verifikimi i grafĂ«ve pĂ«r dy-pjeshmĂ«riÂ
Algoritmi i kontrollit të grafit për ndarjen në dy pjesë zakonisht jepet në kursin e algoritmeve si një nga algoritmet më të thjeshta të grafëve. Ideja e tij është e drejtpërdrejtë: fillimisht ne në njëfarë mënyre vendosim pikat në pjesën e majtë ose të djathtë, dhe kur zbulojmë një skaj të kundërt, konfirmojmë se grafi nuk është ndarë në dy pjesë.
Më në detaje: fillimisht ne vendosim një pikë në pjesën e majtë. Sigurisht, të gjitha fqinjët e kësaj pike duhet të jenë në pjesën e djathtë. Më pas, të gjithë fqinjët e fqinjëve të kësaj pike duhet të jenë në pjesën e majtë, dhe kështu me radhë. Ne vazhdojmë të caktojmë pikave pjesët derisa në komponentin e lidhjes së pikës me të cilën filluam, të ketë ende pika të pa caktuara fqinjët. Pastaj ne e përsërisim këtë veprim për të gjitha komponentët e lidhjes.
Nëse ka një skaj midis pikave që përfundojnë në të njëjtën pjesë, nuk është e vështirë të gjejmë një cikël çifte në grafik, siç dihet gjerësisht (dhe është mjaft e qartë) e pamundur në një grafik të ndarë në dy pjesë. Përndryshe kemi një ndarje të saktë në pjesë, dhe kështu, grafi është i ndarë në dy pjesë.
Si rregull, ky algoritëm realizohet me ose . Në gjuhët imperativë zakonisht përdoret kërkimi në thellësi, si pak më i thjeshtë dhe që nuk kërkon struktura të dhënash shtesë. Unë gjithashtu zgjodha kërkimin në thellësi si metodë më tradicionale.
Kështu, ne arritëm në skemën e mëposhtme. Ne kalojmë nëpër pikat e grafit duke përdorur kërkimin në thellësi dhe u caktojmë ato pjesë, duke ndryshuar numrin e pjesës kur kalojmë përmes një skaji. Nëse përpiqemi të ndajmë një pjesë një pikës, e cila tashmë ka një pjesë të caktuar, mund të konfirmojmë se grafi nuk është ndarë në dy pjesë. Në momentin që të gjitha pikat janë ndarë dhe ne e kemi parë të gjithë skajet, ne kemi një ndarje të mirë.
Pastrimi i llogaritjeve
Në Haskell supozojmë se të gjitha llogaritjet janë të pastra. Megjithatë, sikur kjo të ishte e vërtetë, ne nuk do të kishim mundësi të printonim asgjë në ekran. Në përgjithësi, llogaritjet të pastra janë kaq të ngadalta, saqë nuk ka asnjë arsye të pastrë për të llogaritur ndonjë gjë. Të gjitha llogaritjet që ndodhin në program, në një farë mënyre, përforcohen në "mungesën e pastërtisë" monadën IO.
Monadat janë një mënyrë për të paraqitur llogaritjet me efekte. në Haskell. Shpjegimi i mënyrës sesi ato punojnë del përtej këtij posti. Një përshkrim i mirë dhe i kuptueshëm mund të lexoni në anglisht .
Këtu dua të theksoj se, ndonëse disa monada, si IO, janë të implementuara përmes magjisë së kompilatorit, pothuajse të gjitha të tjerat janë të implementuara programërisht dhe të gjitha llogaritë në to janë të pastra.
Ekzistojnë shumë efekte dhe për secilin është krijuar një monadë e vetme. Kjo është një teori shumë e fuqishme dhe e bukur: të gjitha monadat implementojnë të njëjtën ndërfaqe. Ne do të flasim për këto tri monada:
- Either e a â njĂ« llogaritje qĂ« kthen njĂ« vlerĂ« tĂ« tipit a ose hedh njĂ« pĂ«rjashtim tĂ« tipit e. Sjellja e kĂ«saj monade Ă«shtĂ« shumĂ« e ngjashme me punĂ«n me pĂ«rjashtimet nĂ« gjuhĂ«t imperativĂ«: gabimet mund tĂ« kapen ose tĂ« kalojnĂ« mĂ« tej. Diferenca kryesore Ă«shtĂ« se monada Ă«shtĂ« e implementuar plotĂ«sisht nĂ« mĂ«nyrĂ« logjike nĂ« bibliotekĂ«n standarde nĂ« Haskell, ndĂ«rkohĂ« qĂ« nĂ« gjuhĂ«t imperativĂ« zakonisht pĂ«rdoren mekanizmat e sistemit operativ.
- State s a â njĂ« llogaritje qĂ« kthen njĂ« vlerĂ« tĂ« tipit a dhe ka akses nĂ« njĂ« gjendje tĂ« ndryshueshme tĂ« tipit s.
- Maybe a. Monada Maybe shpreh një llogaritje që mund të ndërpritet në çdo moment nga kthimi i Nothing. Megjithatë, ne do të flasim për implementimin e klasës MonadPlus për tipin Maybe, që shpreh efektin e kundërt: kjo është një llogaritje që mund të ndërpritet në çdo moment duke kthyer një vlerë konkrete.
Implementimi i algoritmit
Ne kemi dy tipe të dhënash, Graph a dhe Bigraph a b, i pari nga të cilët përfaqëson grafet me pika të shënuara me vlera të tipit a, ndërsa tjetri përfaqëson grafet e dyfishta me pika të majtë të shënuara me vlera të tipit a dhe pika të djathta të shënuara me vlera të tipit b.
Këto nuk janë tipe nga biblioteka Alga. Në Alga nuk ka përfaqësim për grafet e dyfishta të paorientuara. Tipet i krijova kështu për qartësi.
Gjithashtu, do të na nevojiten funksione ndihmëse me këto nënshkrime:
-- Lista e fqinjëve të kësaj kulme.
neighbours :: Ord a => a -> Graph a -> [a]
-- Ndërtoni një graf bipartit nga grafi dhe funksioni, për secilën kulm
-- që jep aksionin e saj dhe shënimin në aksionin e ri, duke injoruar skajet konfliktuese.
toBipartiteWith :: (Ord a, Ord b, Ord c) => (a -> Either b c)
                                         -> Graph a
                                         -> Bigraph b c
-- Lista e kulmeve në graf
vertexList :: Ord a => Graph a -> [a]
Sinshtet e funksionit që do të shkruajmë duket kështu:
type OddCycle a = [a]
detectParts :: Ord a => Graph a -> Either (OddCycle a) (Bigraph a a)Nuk është e vështirë të vëresh se nëse gjatë procesit të kërkimit në thellësi gjejmë një skaj konfliktues, cikli çuditshëm ndodhet në krye të grimit të rekursisë. Kështu që, për ta rikuperuar atë, na duhet të prishim të gjitha në grimin e rekursisë deri në shfaqjen e parë të kulmës së fundit.
Ne do të zbatojmë kërkimin në thellësi, duke mbajtur një array asociativ të numrave të aksionit për secilën kulmë. Grimi i rekursisë do të mbahet automatikisht përmes implementimit të klasës Functor të monadës që kemi zgjedhur: thjesht do të duhet të vendosim të gjitha kulmet në rrugë në rezultatin që kthehet nga funksioni rekursiv.
Ideja ime e parë ishte të përdorja monadën Either, e cila në dukje realizon efektet që na nevojiten. Implementimi i parë që shkrova ishte shumë i afërt me këtë variant. Në të vërtetë, unë kisha pesë implementime të ndryshme në një moment, dhe përfundimisht u ndala në një tjetër.
NĂ« radhĂ« tĂ« parĂ«, na nevojitet tĂ« mbajmĂ« njĂ« array asociativ tĂ« identifikuesve tĂ« aksionit â ky Ă«shtĂ« diçka nĂ« lidhje me State. NĂ« radhĂ« tĂ« dytĂ«, na nevojitet tĂ« dimĂ« si tĂ« ndalemi nĂ« rast se zbulojmĂ« njĂ« konflikt. Kjo mund tĂ« jetĂ« ose Monad pĂ«r Either, ose MonadPlus pĂ«r Maybe. Dallimi kryesor Ă«shtĂ« se Either mund tĂ« kthejĂ« njĂ« vlerĂ« nĂ« rastin kur llogaritja nuk Ă«shtĂ« ndalur, ndĂ«rsa Maybe kthen nĂ« kĂ«tĂ« rast vetĂ«m informacion nĂ« lidhje me kĂ«tĂ«. Duke qenĂ« se nuk na nevojitet njĂ« vlerĂ« e veçantĂ« nĂ« rastin e suksesit (ajo Ă«shtĂ« tashmĂ« e ruajtur nĂ« State), ne zgjedhim Maybe. Dhe nĂ« momentin kur na nevojitet tĂ« kombinojmĂ« efektet e dy monadave, dalin , tĂ« cilat nĂ« fakt kĂ«to efekte i kombinojnĂ«.
Pse e zgjodha një tip kaq të ndërlikuar? Dy arsye. Së pari, realizimi doli shumë i ngjashëm me të imperative. Së dyti, na nevojitet të manipulojmë me vlerën që kthehet në rast të konfliktit, kur kthehemi prapa nga rekursioni për të rikuperuar një cikël të çuditshëm, dhe kjo është shumë më e lehtë ta bëjmë në monadën Maybe.
Kështu, ne marrim një realizim të tillë.
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE ScopedTypeVariables #-}
data Part = LeftPart | RightPart
otherPart :: Part -> Part
otherPart LeftPart = RightPart
otherPart RightPart = LeftPart
type PartMap a = Map.Map a Part
type OddCycle a = [a]
toEither :: Ord a => PartMap a -> a -> Either a a
toEither m v = case fromJust (v `Map.lookup` m) of
LeftPart -> Left v
RightPart -> Right v
type PartMonad a = MaybeT (State (PartMap a)) [a]
detectParts :: forall a. Ord a => Graph a -> Either (OddCycle a) (Bigraph a a)
detectParts g = case runState (runMaybeT dfs) Map.empty of
(Just c, _) -> Left $ oddCycle c
(Nothing, m) -> Right $ toBipartiteWith (toEither m) g
where
inVertex :: Part -> a -> PartMonad a
inVertex p v = ((:) v) <$ do modify $ Map.insert v p
let q = otherPart p
msum [ onEdge q u | u a -> PartMonad a
onEdge p v = do m inVertex p v
Just q -> do guard (q /= p)
return [v]
processVertex :: a -> PartMonad a
processVertex v = do m <- get
guard (v `Map.notMember` m)
inVertex LeftPart v
dfs :: PartMonad a
dfs = msum [ processVertex v | v [a]
oddCycle c = tail (dropWhile ((/=) last c) c)
Blloku where â Ă«shtĂ« thelbi i algoritmit. Do pĂ«rpiqem tĂ« shpjegoj se çfarĂ« ndodh brenda tij.
- inVertex â Ă«shtĂ« pjesa e kĂ«rkimit nĂ« thellĂ«si, ku ne vizitojmĂ« njĂ« pikĂ« pĂ«r herĂ« tĂ« parĂ«. KĂ«tu i japim pikĂ«s numrin e pjesĂ«s dhe aktivizojmĂ« onEdge pĂ«r tĂ« gjithĂ« fqinjĂ«t. Gjithashtu, kjo Ă«shtĂ« vendi ku rikuperojmĂ« grumbullin e thirrjeve: nĂ«se msum ktheu njĂ« vlerĂ«, ne e lidhim atje pikĂ«n v.
- onEdge â Ă«shtĂ« pjesa kur vizitojmĂ« skajet. Ajo thirret dy herĂ« pĂ«r çdo skaj. KĂ«tu kontrollojmĂ« nĂ«se maja nga ana tjetĂ«r Ă«shtĂ« vizituar dhe e vizitojmĂ« atĂ« nĂ«se nuk Ă«shtĂ«. NĂ«se Ă«shtĂ« vizituar, kontrollojmĂ« nĂ«se skaji Ă«shtĂ« konfliktual. NĂ«se Ă«shtĂ«, kthejmĂ« vlerĂ«n â majĂ«n e stack-ut tĂ« rekursivitetit, ku mĂ« pas do tĂ« ngjiten tĂ« gjitha majat e tjera gjatĂ« kthimit.
- processVertex kontrollon për çdo majë nëse është vizituar dhe e nis inVertex në të nëse nuk është.
- dfs nis processVertex në të gjitha majat.
Kjo është gjithçka.
Historia e fjalës INLINE
Fjala INLINE nuk ishte në implementimin e parë të algoritmit, ajo u shfaq më vonë. Kur përpiqesha të gjeja një implementim më të mirë, vura re se në disa grafe versioni pa INLINE funksiononte ndjeshëm më ngadalë. Duke pasur parasysh që semantikisht funksionet duhet të punojnë njësoj, kjo më befasoi shumë. Edhe më e çuditshme ishte që në një makinë tjetër me një version tjetër të GHC nuk kishte asnjë ndryshim të dukshëm.
Pas një javë të kaluar duke lexuar daljen e GHC Core, arrita të rregulloj problemin me një rresht me INLINE të qartë. Në një moment midis GHC 8.4.4 dhe GHC 8.6.5 optimizuesi ndaloi ta bëjë këtë automatikisht.
Nuk e prisja tĂ« hasja njĂ« kaos tĂ« tillĂ« nĂ« programimin me Haskell. MegjithatĂ«, optimizuesit megjithatĂ«, madje edhe nĂ« kohĂ«t tona, ndonjĂ«herĂ« bĂ«jnĂ« gabime dhe t'u japĂ«sh atyre sugjerime â Ă«shtĂ« detyra jonĂ«. PĂ«r shembull, kĂ«tu e dimĂ« qĂ« funksioni duhet tĂ« jetĂ« i inlinuar, sepse Ă«shtĂ« i inlinuar nĂ« versionin imperativ, dhe kjo Ă«shtĂ« arsyeja pĂ«r tĂ« dhĂ«nĂ« njĂ« sugjerim pĂ«r kompajlerin.
ĂfarĂ« ndodhi mĂ« pas?
Më pas realizova algoritmin e Hopcroft-Karpa me monada të tjera, dhe me këtë programi përfundoi.
Falë Google Summer of Code, fitova përvojë praktike në programimin funksional, që jo vetëm që më ndihmoi të kaloj në një praktikë në Jane Street verën tjetër (nuk jam i sigurt se sa e njohur është kjo vend për audiencën e ditur të Habra, por është një nga pak ku në verë mund të merresh me programimin funksional), por gjithashtu më prezantoi me botën e habitshme të aplikimit të kësaj paradigme në praktikë, e cila ndryshon ndjeshëm nga përvoja ime në gjuhët tradicionale.
Burimi: habr.com
