GSoC 2019: pārbauda diagrammas divpusības un monādes transformatoru noteikŔanai

PagājuÅ”ajā vasarā piedalÄ«jos Google Summer of Code - programma studentiem no Google. Katru gadu organizatori atlasa vairākus Open Source projektus, tostarp no tādām pazÄ«stamām organizācijām kā Boost.org Šø Linux fonds. Google aicina studentus no visas pasaules strādāt pie Å”iem projektiem. 

Kā Google Summer of Code 2019 dalÄ«bnieks es bibliotēkā Ä«stenoju projektu Aļģe ar organizāciju Haskell.org, kas izstrādā Haskell valodu - vienu no slavenākajām funkcionālās programmÄ“Å”anas valodām. Alga ir bibliotēka, kas pārstāv tipa seifs grafu attēlojums Haskellā. To izmanto, piemēram, in semantiskais ā€” Github bibliotēka, kas veido semantiskos kokus, zvanu un atkarÄ«bas grafikus, pamatojoties uz kodu, un var tos salÄ«dzināt. Mans projekts bija pievienot tipam droÅ”u attēlojumu divpusējiem grafikiem un algoritmiem Å”im attēlojumam. 

Å ajā ierakstā es runāŔu par algoritma ievieÅ”anu, lai pārbaudÄ«tu diagrammas divpusējo attiecÄ«bu Haskell. Lai gan algoritms ir viens no visvienkārŔākajiem, tā smuki ievieÅ”ana funkcionālā stilā man prasÄ«ja vairākas iterācijas un prasÄ«ja diezgan daudz darba. Rezultātā es izvēlējos ievieÅ”anu ar monādes transformatoriem. 

GSoC 2019: pārbauda diagrammas divpusības un monādes transformatoru noteikŔanai

Par sevi

Mani sauc Vasilijs Alferovs, esmu Sanktpēterburgas HSE ceturtā kursa students. IepriekÅ” emuārā es rakstÄ«ju par manu projektu par parametrizētiem algoritmiem Šø par braucienu uz ZuriHac. Å obrÄ«d esmu praksē plkst Bergenas Universitāte Norvēģijā, kur es strādāju pie problēmas pieejām Saraksta krāsoÅ”ana. Manas intereses ietver parametrizētus algoritmus un funkcionālo programmÄ“Å”anu.

Par algoritma ievieŔanu

priekŔvārds

Studenti, kas piedalās programmā, tiek ļoti aicināti rakstÄ«t emuārus. Viņi man nodroÅ”ināja platformu emuāram Haskela vasara. Å is raksts ir tulkojums Raksts, ko es tur jÅ«lijā uzrakstÄ«ju angļu valodā, ar Ä«su priekÅ”vārdu. 

Pull Request ar attiecīgo kodu var atrast Ŕeit.

Par mana darba rezultātiem varat lasīt (angļu valodā) Ŕeit.

Å is ieraksts ir paredzēts, lai iepazÄ«stinātu lasÄ«tāju ar funkcionālās programmÄ“Å”anas pamatjēdzieniem, lai gan es mēģināŔu atsaukt atmiņā visus lietotos terminus, kad pienāks laiks.

Grafiku divpusÄ«bas pārbaude 

Algoritms grafa divpusÄ«bas pārbaudei parasti tiek dots kursā par algoritmiem kā viens no vienkārŔākajiem grafu algoritmiem. Viņa ideja ir vienkārÅ”a: vispirms mēs kaut kādā veidā ievietojam virsotnes kreisajā vai labajā daļā, un, kad tiek atrasta pretrunÄ«ga mala, mēs apgalvojam, ka grafs nav divpusējs.

Nedaudz sÄ«kāk: vispirms kreisajā daļā ievietojam kādu virsotni. AcÄ«mredzot visiem Ŕīs virsotnes kaimiņiem jāatrodas labajā daivā. Turklāt visiem Ŕīs virsotnes kaimiņu kaimiņiem jāatrodas kreisajā daivā utt. Mēs turpinām pieŔķirt virsotnēm daļas, kamēr savienotajā virsotnes komponentā, ar kuru sākām, joprojām ir virsotnes, kurām neesam pieŔķīruÅ”i kaimiņus. Pēc tam mēs atkārtojam Å”o darbÄ«bu visiem pievienotajiem komponentiem.

Ja starp virsotnēm, kas ietilpst vienā nodalÄ«jumā, ir mala, grafā nav grÅ«ti atrast nepāra ciklu, kas ir plaÅ”i pazÄ«stams (un diezgan acÄ«mredzami) divpusējā grafā. Pretējā gadÄ«jumā mums ir pareizs nodalÄ«jums, kas nozÄ«mē, ka grafiks ir divpusējs.

Parasti Å”is algoritms tiek Ä«stenots, izmantojot pirmais meklējums platumā vai dziļuma pirmā meklÄ“Å”ana. Obligātajās valodās parasti tiek izmantota pirmā dziļuma meklÄ“Å”ana, jo tā ir nedaudz vienkārŔāka un neprasa papildu datu struktÅ«ras. Es arÄ« izvēlējos dziļuma meklÄ“Å”anu, jo tā ir tradicionālāka.

Tādējādi mēs nonācām pie Ŕādas shēmas. Mēs Ŕķērsojam diagrammas virsotnes, izmantojot dziļuma meklÄ“Å”anu, un pieŔķiram tām daļas, mainot daļas numuru, virzoties gar malu. Ja mēs mēģinām pieŔķirt daļu virsotnei, kurai jau ir pieŔķirta daļa, mēs varam droÅ”i teikt, ka grafs nav divpusējs. BrÄ«dÄ«, kad visām virsotnēm ir pieŔķirta daļa un mēs esam apskatÄ«juÅ”i visas malas, mums ir labs nodalÄ«jums.

Aprēķinu tīrība

Haskell mēs pieņemam, ka visi aprēķini ir tīrs. Tomēr, ja tas tā būtu, mēs nevarētu neko izdrukāt uz ekrāna. Pavisam, tīrs aprēķini ir tik slinki, ka nav neviena tīrs iemesli kaut ko aprēķināt. Visi aprēķini, kas notiek programmā, ir kaut kā spiesti "netīrs" monāde IO.

Monādes ir veids, kā attēlot aprēķinus ietekmi Haskelā. IzskaidroÅ”ana, kā viņi strādā, neietilpst Ŕīs ziņas darbÄ«bas jomā. Labu un skaidru aprakstu var izlasÄ«t angļu valodā Å”eit.

Šeit es gribu norādīt, ka, lai gan dažas monādes, piemēram, IO, tiek ieviestas ar kompilatoru maģiju, gandrīz visas pārējās ir ieviestas programmatūrā un visi aprēķini tajās ir tīri.

Ir daudz efektu, un katram ir sava monāde. Å Ä« ir ļoti spēcÄ«ga un skaista teorija: visas monādes Ä«steno vienu un to paÅ”u saskarni. Mēs runāsim par Ŕādām trim monādēm:

  • Vai nu ea ir aprēķins, kas atgriež a tipa vērtÄ«bu vai izmet e tipa izņēmumu. Å Ä«s monādes darbÄ«ba ir ļoti lÄ«dzÄ«ga izņēmumu apstrādei obligātajās valodās: kļūdas var tikt pieÄ·ertas vai nodotas tālāk. Galvenā atŔķirÄ«ba ir tā, ka monāde ir pilnÄ«bā loÄ£iski ieviesta Haskell standarta bibliotēkā, savukārt obligātajās valodās parasti tiek izmantoti operētājsistēmas mehānismi.
  • Stāvoklis sa ir aprēķins, kas atgriež a tipa vērtÄ«bu un kuram ir piekļuve mainÄ«gam s tipa stāvoklim.
  • VarbÅ«t a. VarbÅ«t monāde izsaka aprēķinu, ko jebkurā brÄ«dÄ« var pārtraukt, atgriežot neko. Tomēr mēs runāsim par MonadPlus klases ievieÅ”anu Maybe tipam, kas pauž pretēju efektu: tas ir aprēķins, kuru jebkurā brÄ«dÄ« var pārtraukt, atgriežot noteiktu vērtÄ«bu.

Algoritma realizācija

Mums ir divi datu tipi, Graph a un Bigraph ab, no kuriem pirmais attēlo grafikus ar virsotnēm, kas apzīmētas ar a tipa vērtībām, bet otrais attēlo divpusējus grafikus ar kreisās puses virsotnēm, kas apzīmētas ar a tipa un labās puses vērtībām. - sānu virsotnes, kas apzīmētas ar b tipa vērtībām.

Tie nav veidi no Algas bibliotēkas. Alga nav attēlojuma nevirzÄ«tiem divpusējiem grafikiem. SkaidrÄ«bas labad izgatavoju Ŕādus tipus.

Mums būs nepiecieŔamas arī palīgfunkcijas ar Ŕādiem parakstiem:

-- Š”ŠæŠøсŠ¾Šŗ сŠ¾ŃŠµŠ“ŠµŠ¹ Š“Š°Š½Š½Š¾Š¹ Š²ŠµŃ€ŃˆŠøŠ½Ń‹.
neighbours :: Ord a => a -> Graph a -> [a]

-- ŠŸŠ¾ŃŃ‚Ń€Š¾Šøть Š“Š²ŃƒŠ“Š¾Š»ŃŒŠ½Ń‹Š¹ Š³Ń€Š°Ń„ ŠæŠ¾ Š³Ń€Š°Ń„Ńƒ Šø фуŠ½ŠŗцŠøŠø, Š“Š»Ń ŠŗŠ°Š¶Š“Š¾Š¹ Š²ŠµŃ€ŃˆŠøŠ½Ń‹
-- Š²Ń‹Š“Š°ŃŽŃ‰ŠµŠ¹ ŠµŃ‘ Š“Š¾Š»ŃŽ Šø ŠæŠ¾Š¼ŠµŃ‚Šŗу Š² Š½Š¾Š²Š¾Š¹ Š“Š¾Š»Šµ, ŠøŠ³Š½Š¾Ń€Šøруя ŠŗŠ¾Š½Ń„Š»ŠøŠŗтŠ½Ń‹Šµ рёŠ±Ń€Š°.
toBipartiteWith :: (Ord a, Ord b, Ord c) => (a -> Either b c)
                                         -> Graph a
                                         -> Bigraph b c

-- Š”ŠæŠøсŠ¾Šŗ Š²ŠµŃ€ŃˆŠøŠ½ Š² Š³Ń€Š°Ń„Šµ
vertexList :: Ord a => Graph a -> [a]
Š”ŠøŠ³Š½Š°Ń‚ŃƒŃ€Š° фуŠ½ŠŗцŠøŠø, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ Š¼Ń‹ Š±ŃƒŠ“ŠµŠ¼ ŠæŠøсŠ°Ń‚ŃŒ, Š²Ń‹Š³Š»ŃŠ“Šøт тŠ°Šŗ:

type OddCycle a = [a]
detectParts :: Ord a => Graph a -> Either (OddCycle a) (Bigraph a a)

Ir viegli redzēt, ka, ja dziļuma pirmās meklÄ“Å”anas laikā mēs atradām konfliktējoÅ”u malu, nepāra cikls atrodas rekursijas steka augÅ”pusē. Tādējādi, lai to atjaunotu, mums ir jānogriež viss, sākot no rekursijas steka lÄ«dz pirmajai pēdējās virsotnes gadÄ«jumam.

Mēs Ä«stenojam dziļuma meklÄ“Å”anu, katrai virsotnei uzturot asociatÄ«vu koplietoÅ”anas numuru masÄ«vu. Rekursijas kaudze tiks automātiski uzturēta, ievieÅ”ot mÅ«su izvēlētās monādes Functor klasi: mums bÅ«s tikai jāievieto visas virsotnes no ceļa rezultātos, kas atgriezts no rekursÄ«vās funkcijas.

Mana pirmā ideja bija izmantot vai nu monādi, kas, Ŕķiet, Ä«steno tieÅ”i mums nepiecieÅ”amos efektus. Pirmā ievieÅ”ana, ko es uzrakstÄ«ju, bija ļoti tuvu Å”ai opcijai. PatiesÄ«bā man vienā brÄ«dÄ« bija piecas dažādas ievieÅ”anas, un galu galā es izvēlējos citu.

Pirmkārt, mums ir jāsaglabā asociatÄ«vs akciju identifikatoru masÄ«vs ā€” tas ir kaut kas par valsti. Otrkārt, mums ir jāspēj apstāties, kad tiek atklāts konflikts. Tas var bÅ«t vai nu Monad for Any, vai MonadPlus for Maybe. Galvenā atŔķirÄ«ba ir tā, ka vai nu var atgriezt vērtÄ«bu, ja aprēķins nav apturēts, un Maybe Å”ajā gadÄ«jumā atgriež tikai informāciju par to. Tā kā panākumiem nav nepiecieÅ”ama atseviŔķa vērtÄ«ba (tā jau ir saglabāta Å”tatā), mēs izvēlamies VarbÅ«t. Un tajā brÄ«dÄ«, kad mums ir jāapvieno divu monāžu ietekme, tās iznāk monādes transformatori, kas precÄ«zi apvieno Å”os efektus.

Kāpēc es izvēlējos tik sarežģītu veidu? Divi iemesli. Pirmkārt, Ä«stenoÅ”ana izrādās ļoti lÄ«dzÄ«ga imperatÄ«vai. Otrkārt, mums ir jāmanipulē ar atgrieÅ”anas vērtÄ«bu konflikta gadÄ«jumā, atgriežoties no rekursijas, lai atjaunotu nepāra cilpu, kas ir daudz vieglāk izdarāms Maybe monādē.

Tādējādi mēs iegÅ«stam Å”o ievieÅ”anu.

{-# 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 <- neigbours v g ]

        {-# INLINE onEdge #-}
        onEdge :: Part -> a -> PartMonad a
        onEdge p v = do m <- get
                        case v `Map.lookup` m of
                             Nothing -> 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 <- vertexList g ]

        oddCycle :: [a] -> [a]
        oddCycle c = tail (dropWhile ((/=) last c) c)

Kur bloks ir algoritma kodols. MēģināŔu paskaidrot, kas tajā iekŔā notiek.

  • inVertex ir dziļuma pirmās meklÄ“Å”anas daļa, kurā mēs pirmo reizi apmeklējam virsotni. Å eit mēs pieŔķiram virsotnei koplietoÅ”anas numuru un palaižam onEdge uz visiem kaimiņiem. Å eit mēs arÄ« atjaunojam izsaukuma steku: ja msum atgrieza vērtÄ«bu, mēs tur nospiežam virsotni v.
  • onEdge ir daļa, kurā mēs apmeklējam malu. Katrai malai to sauc divreiz. Å eit mēs pārbaudām, vai virsotne otrā pusē ir apmeklēta, un apmeklējiet to, ja nē. Ja apmeklējat, mēs pārbaudām, vai mala nav konfliktējoÅ”a. Ja tā ir, mēs atgriežam vērtÄ«bu - paÅ”u rekursijas kaudzes augÅ”daļu, kur pēc atgrieÅ”anās tiks novietotas visas pārējās virsotnes.
  • processVertex pārbauda katrai virsotnei, vai tā ir apmeklēta, un palaiž tajā inVertex, ja nē.
  • dfs palaiž processVertex visās virsotnēs.

Tas ir viss.

Vārda INLINE vēsture

Vārds INLINE nebija pirmajā algoritma realizācijā, tas parādÄ«jās vēlāk. Kad es mēģināju atrast labāku ievieÅ”anu, es atklāju, ka versija, kas nav INLINE, dažos grafikos ir ievērojami lēnāka. Ņemot vērā, ka semantiski funkcijām jādarbojas vienādi, tas mani ļoti pārsteidza. Vēl dÄ«vaināk, citā maŔīnā ar citu GHC versiju nebija manāmas atŔķirÄ«bas.

Pēc tam, kad pavadÄ«ju nedēļu, lasot GHC Core izvadi, es varēju novērst problēmu, izmantojot vienu INLINE rindiņu. Kādā brÄ«dÄ« starp GHC 8.4.4 un GHC 8.6.5 optimizētājs pārtrauca to darÄ«t pats.

Es nebiju gaidÄ«jis, ka Haskell programmÄ“Å”anas laikā sastapsies ar tik netÄ«rumiem. Tomēr pat mÅ«sdienās optimizētāji dažreiz pieļauj kļūdas, un mÅ«su uzdevums ir dot viņiem padomus. Piemēram, Å”eit mēs zinām, ka funkcijai jābÅ«t iekļautai, jo tā ir iekļauta obligātajā versijā, un tas ir iemesls, lai sniegtu kompilatoram mājienu.

Kas notika tālāk?

Tad es ieviesu Hopcroft-Karp algoritmu ar citām monādēm, un ar to arī programma beidzās.

Pateicoties Google Summer of Code, es ieguvu praktisku pieredzi funkcionālajā programmÄ“Å”anā, kas man ne tikai palÄ«dzēja nākamajā vasarā iegÅ«t praksi Džeinas ielā (es neesmu pārliecināts, cik labi Ŕī vieta ir pazÄ«stama pat Habra zinoŔās auditorijas vidÅ«, bet tā ir viena no retajiem, kur var nodarboties ar funkcionālo programmÄ“Å”anu), bet arÄ« iepazÄ«stināja mani ar Ŕīs paradigmas pielietoÅ”anas brÄ«niŔķīgo pasauli, kas bÅ«tiski atŔķiras no manas pieredzes tradicionālajās valodās.

Avots: www.habr.com

Pievieno komentāru