PagÄjuÅ”ajÄ vasarÄ es piedalÄ«jos ā studentiÅu programmÄ no uzÅÄmuma Google. Ik gadu organizatori izvÄlas vairÄkus Open Source projektus, tostarp no tÄdÄm pazÄ«stamÄm organizÄcijÄm kÄ un . Projektu Ä«stenoÅ”anai Google aicina studentus no visas pasaules.Ā
KÄ Google Summer of Code 2019 dalÄ«bnieks, es veicu projektu bibliotÄkas ietvaros sadarbÄ«bÄ ar organizÄciju , kas nodarbojas ar Haskell valodas attÄ«stÄ«bu ā vienu no pazÄ«stamÄkajÄm funkcionÄlÄs programmÄÅ”anas valodÄm. Alga ir bibliotÄka, kas nodroÅ”ina grafu attÄlojumu Haskell valodÄ. To izmanto, piemÄram, bibliotÄkÄ ā uzÅÄmuma Github bibliotÄkÄ, kas konstruÄ semantiskos kokus, izsaukumu un atkarÄ«bu grafus, un spÄj tos salÄ«dzinÄt. Mans projekts sastÄvÄja no tipiski droÅ”a attÄlojuma pievienoÅ”anas divu daļu grafiem un algoritmu izstrÄdes Å”im attÄlojumam.Ā
Å ajÄ ierakstÄ es stÄstīŔu par savu algoritma izstrÄdi grafu dvÄ«Åu pÄrbaudei Haskell valodÄ. Neskatoties uz to, ka algoritms ir viens no visvienkÄrÅ”Äkajiem, tÄ skaistÄ pÄrrakstīŔana funkcionÄlajÄ stilÄ man aizÅÄma vairÄkas iterÄcijas un prasÄ«ja diezgan daudz darba. RezultÄtÄ es izstrÄdÄju realizÄciju ar monadiskajiem transformatoriem.Ā

Par sevi
Mani sauc VasÄ«lijs AlfjÄrovs, es esmu ceturtÄ kursa students SanktpÄterburgas AugstÄkajÄ ekonomikas skolÄ. IepriekÅ” blogÄ es rakstÄ«ju un . Patlaban es esmu prakses periodÄ NorvÄÄ£ijÄ, kur es strÄdÄju pie pieejÄm uzdevumam . Mani interesÄ parametrizÄtie algoritmi un funkcionÄlÄ programmÄÅ”ana.
Par algoritma realizÄciju
Ievads
Studentiem, kuri piedalÄs programmÄ, ieteicams veidot blogu. Man blogam pieŔķirta platforma . Å is raksts ir tulkojums , ko es uzrakstÄ«ju tur jÅ«lijÄ angļu valodÄ, ar nelielu ievadu.Ā
Pull Request ar kodu, par kuru iet runa, var atrast .
Par manas darba rezultÄtiem var lasÄ«t (angļu valodÄ) .
Raksts paredz, ka lasÄ«tÄjam bÅ«s pamatzinÄÅ”anas funkcionÄlajÄ programmÄÅ”anÄ, lai gan es cenÅ”os atgÄdinÄt visus izmantotos terminus, kad pienÄks to kÄrta.
Grafu pÄrbaude uz dvÄ«nÄ«buĀ
Grafu bipartitÄtes pÄrbaudes algoritms parasti tiek pasniegts algoritmu kursÄ kÄ viens no vienkÄrÅ”Äkajiem grafu algoritmiem. T tÄ ideja ir vienkÄrÅ”a: vispirms mÄs kaut kÄdÄ veidÄ novietojam virsotnes kreisajÄ vai labajÄ daļÄ, un, atklÄjot konfliktÄjoÅ”u malu, apgalvojam, ka graphs nav bipartÄ«ts.
Nedaudz sÄ«kÄk: vispirms mÄs novietojam kÄdu virsotni kreisajÄ daļÄ. Tas ir acÄ«mredzami, ka visÄm kaimiÅu virsotnÄm Å”ai virsotnei jÄbÅ«t labajÄ daļÄ. PÄc tam visÄm kaimiÅu kaimiÅu virsotnÄm jÄbÅ«t kreisajÄ daļÄ, un tÄ tÄlÄk. MÄs turpinÄm pieŔķirt virsotnÄm daļas, kamÄr komponentÄ, kurÄ esam sÄkuÅ”i, joprojÄm ir virsotnes, kurÄm neesam pieŔķīruÅ”i kaimiÅus. Tad mÄs atkÄrtojam Å”o darbÄ«bu visÄm komponentÄm.
Ja ir mala starp virsotnÄm, kas iekļūst vienÄ un tajÄ paÅ”Ä daļÄ, nav grÅ«ti atrast grafÄ pÄru ciklu, kas, kÄ plaÅ”i zinÄms (un pietiekami acÄ«mredzams), ir neiespÄjams bipartÄ«tÄ grafikÄ. PretÄjÄ gadÄ«jumÄ mums ir pareiza daļas sadalīŔana, un tÄdÄjÄdi grafiks ir bipartÄ«ts.
Parasti Å”o algoritmu Ä«steno ar vai . ImperatÄ«vÄs valodÄs parasti izmanto dziļuma meklÄÅ”anu, jo tÄ ir nedaudz vienkÄrÅ”Äka un neprasa papildu datu struktÅ«ras. Es arÄ« izvÄlÄjos dziļuma meklÄÅ”anu kÄ tradicionÄlÄku.
TÄdÄjÄdi mÄs nonÄkam pie sekojoÅ”Ä shÄmas. MÄs apgÄjam grafu virsotnes ar dziļuma meklÄÅ”anu un pieŔķiram tÄm daļas, mainot daļas numuru, pÄrejot pa malu. Ja mÄs mÄÄ£inÄm pieŔķirt daļu virsotnei, kurai jau ir pieŔķirta daļa, varam droÅ”i apgalvot, ka grafiks nav bipartÄ«ts. TajÄ brÄ«dÄ«, kad visÄm virsotnÄm ir pieŔķirta daļa, un mÄs esam apskatÄ«juÅ”i visas malas, mums ir laba sadalīŔana.
SkaitliskÄ tÄ«rÄ«ba
HaskellÄ mÄs pieÅemam, ka visas aprÄÄ·ini ir tÄ«ri. TomÄr, ja tÄ patieÅ”Äm bÅ«tu, mums nebÅ«tu iespÄjas izdrukÄt neko uz ekrÄna. KopumÄ, tÄ«ri aprÄÄ·ini ir tik slinki, ka neeksistÄ neviena tÄ«ra iecÄloÅ”ana, lai kaut ko aprÄÄ·inÄtu. Visi aprÄÄ·ini, kas notiek programmÄ, tÄ vai citÄdi tiek piespiesti uz "netÄ«ro" IO monÄdes.
MonÄdes ir veids, kÄ attÄlot aprÄÄ·inus ar efektiem HaskelÄ«. Skaidrojums par to, kÄ tie darbojas, pÄrsniedz Ŕī raksta robežas. Labu un saprotamu aprakstu var izlasÄ«t angļu valodÄ. .
Å eit es gribu norÄdÄ«t, ka, kamÄr dažas monÄdes, piemÄram, IO, ir Ä«stenotas caur kompilatora burvÄ«bu, praktiski visas pÄrÄjÄs ir Ä«stenotas programmatÅ«ras lÄ«menÄ«, un visas to aprÄÄ·ini ir tÄ«ri.
Ir ļoti daudz efektu, un katram ir savs monÄde. TÄ ir ļoti spÄcÄ«ga un skaista teorija: visas monÄdes Ä«steno to paÅ”u saskarni. MÄs runÄsim par nÄkamajÄm trim monÄdÄm:
- Either e a ā aprÄÄ·ins, kas atgriež vÄrtÄ«bu ar tipu a vai izmet izÅÄmumu ar tipu e. Å Ä«s monÄdes uzvedÄ«ba ļoti lÄ«dzinÄs darbam ar izÅÄmumiem imperatÄ«vÄs valodÄs: kļūdas var tikt noÄ·ertas vai nodotas tÄlÄk. GalvenÄ atŔķirÄ«ba ir tÄ, ka monÄde ir pilnÄ«gi loÄ£iski Ä«stenota standarta bibliotÄkÄ tajÄ paÅ”Ä HaskelÄ«, savukÄrt imperatÄ«vÄs valodÄs parasti tiek izmantoti operÄtÄjsistÄmas mehÄnismi.
- State s a ā aprÄÄ·ins, kas atgriež vÄrtÄ«bu ar tipu a un kam ir piekļuve mainÄ«gajam stÄvoklim ar tipu s.
- Maybe a. MonÄde Maybe izsaka aprÄÄ·inu, kuru jebkurÄ brÄ«dÄ« var pÄrtraukt, atgriežot Nothing. TomÄr mÄs runÄsim par MonadPlus klases Ä«stenoÅ”anu tipam Maybe, kas izsaka pretÄju efektu: tas ir aprÄÄ·ins, kuru jebkurÄ brÄ«dÄ« var pÄrtraukt, atgriežot konkrÄtu vÄrtÄ«bu.
Algoritma īstenoŔana
Mums ir divi datu tipi, Graph a un Bigraph a b, no kuriem pirmais attÄlo grafus ar virsotnÄm, kas marÄ·Ätas ar tipu a, bet otrais attÄlo divdaļīgus grafus ar kreisajÄ pusÄ esoÅ”ajÄm virsotnÄm, kas marÄ·Ätas ar tipu a, un labajÄ pusÄ esoÅ”ajÄm virsotnÄm, kas marÄ·Ätas ar tipu b.
Å ie nav tipi no Alga bibliotÄkas. Alga nesatur attÄlojumu nenoteiktiem divdaļīgiem grafiem. Es izveidoju tipus tÄdus, lai tie bÅ«tu saprotami.
Mums arÄ« bÅ«s nepiecieÅ”ams palÄ«gfunkcijas ar Å”ÄdÄm parakstÄm:
-- Blakus Ŕo virsotni
neighbours :: Ord a => a -> Graph a -> [a]
-- BÅ«vÄt divdaļīgu grafu, pamatojoties uz grafu un funkciju, kas katrai virsotnei sniedz tÄs daļu un atzÄ«mi jaunajÄ daļÄ, ignorÄjot konfliktÄjoÅ”Äs malas.
toBipartiteWith :: (Ord a, Ord b, Ord c) => (a -> Either b c)
Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā -> Graph a
Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā Ā -> Bigraph b c
-- VirsotÅu saraksts grafÄ
vertexList :: Ord a => Graph a -> [a]
Funkcijas paraksts, kuru mÄs rakstÄ«sim, izskatÄs Å”Ädi:
type OddCycle a = [a]
detectParts :: Ord a => Graph a -> Either (OddCycle a) (Bigraph a a)Nav grÅ«ti pamanÄ«t, ka, ja dziļumÄ meklÄÅ”anas procesÄ mÄs atrodam konfliktÄjoÅ”o malu, neparastais cikls ir virs rekursijas steka. TÄdÄjÄdi, lai to atjaunotu, mums nepiecieÅ”ams nogriezt visu no rekursijas steka lÄ«dz pirmajam pÄdÄjÄs virsotnes ievadÄ«jumam.
MÄs Ä«stenosim dziļuma meklÄÅ”anu, uzturot asociatÄ«vu masÄ«vu ar daļu numuriem katrai virsotnei. Rekursijas steku automÄtiski uzturÄs izvÄlÄtÄs monÄdes Functor klases realizÄcija: bÅ«s vien jÄdod visas virsotnes ceÄ¼Ä rezultÄtÄ, ko atgriež rekursÄ«vÄ funkcija.
Man pirmÄ ideja bija izmantot monÄdu Either, kas, Ŕķiet, realizÄ tieÅ”i tos efektus, kas mums nepiecieÅ”ami. Mans pirmais rakstÄ«tais realizÄjums bija ļoti tuvu Å”im variantam. PatiesÄ«bÄ man kÄdu brÄ«di bija piecas dažÄdas realizÄcijas, un es beigÄs apstÄjos pie citas.
PirmkÄrt, mums ir nepiecieÅ”ams uzturÄt asociatÄ«vu masÄ«vu ar daļu identifikatoriem ā tas ir kaut kas attiecÄ«bÄ uz State. OtrkÄrt, mums jÄspÄj apstÄties, ja tiek konstatÄts konflikts. Tas var bÅ«t vai nu Monad for Either, vai MonadPlus for Maybe. GalvenÄ atŔķirÄ«ba ir tÄ, ka Either var atgriezt vÄrtÄ«bu, ja aprÄÄ·ins nav pÄrtraukts, bet Maybe Å”ÄdÄ gadÄ«jumÄ atgriež tikai informÄciju par to. TÄ kÄ mums nav nepiecieÅ”ama atseviŔķa vÄrtÄ«ba panÄkumu gadÄ«jumÄ (tÄ jau ir saglabÄta State), mÄs izvÄlamies Maybe. Bet tajÄ brÄ«dÄ«, kad mums jÄapvieno divu monÄdu efekti, iznÄk , kas tieÅ”i kombinÄ Å”os efektus.
KÄpÄc es izvÄlÄjos tik sarežģītu tipu? Divi iemesli. PirmkÄrt, realizÄcija iznÄk ļoti lÄ«dzÄ«ga imperatÄ«vai. OtrkÄrt, mums ir nepiecieÅ”ams manipulÄt ar vÄrtÄ«bu, kas tiek atgriezta konfliktÄ, kad atgriežamies no rekursijas, lai atjaunotu nepÄra ciklu, un to ir daudz vienkÄrÅ”Äk darÄ«t ar Maybe monÄdu.
TÄdÄjÄdi mÄs iegÅ«stam tÄdu realizÄciju.
{-# 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)
Block where ā tas ir algoritma kodols. Es centīŔos izskaidrot, kas notiek tÄ iekÅ”ienÄ.
- inVertex ā tÄ ir daļa no dziļumÄ meklÄÅ”anas, kurÄ mÄs vispirms apmeklÄjam virsotni. Å eit mÄs pieŔķiram virsotnei daļu skaitu un palaidam onEdge visiem kaimiÅiem. TÄ ir arÄ« vieta, kur mÄs atjaunojam izsaukumu steku: ja msum atgrieza vÄrtÄ«bu, mÄs tur pieliekam virsotni v.
- onEdge ā tas ir daļa, kur mÄs apmeklÄjam malu. To izsauc divreiz katrai malai. Å eit mÄs pÄrbaudÄm, vai virsotne no otras puses ir apmeklÄta, un apmeklÄjam to, ja tÄ nav. Ja jau ir apmeklÄta, pÄrbaudÄm, vai mala ir konfliktÄjoÅ”a. Ja tÄ ir, atgriežam vÄrtÄ«bu ā paÅ”galveno rekursijas kaudzi, kurai pÄc tam pievienosies visas pÄrÄjÄs virsotnes.
- processVertex pÄrbauda katrai virsotnei, vai tÄ ir apmeklÄta, un izsauc inVertex, ja tÄ nav.
- dfs izsauc processVertex uz visÄm virsotnÄm.
Uz Ŕo brīdi viss.
VÄrda INLINE vÄsture
VÄrds INLINE nebija pirmajÄ algoritma realizÄcijÄ, tas parÄdÄ«jÄs vÄlÄk. Kad es centos atrast labÄku realizÄciju, es atklÄju, ka dažos grafos versija bez INLINE darbojÄs ievÄrojami lÄnÄk. Å emot vÄrÄ to, ka semantiski funkcijÄm vajadzÄtu darboties vienÄdi, tas mani ļoti pÄrsteidza. VÄl dÄ«vainÄk ir tas, ka citÄ datorÄ ar citu GHC versiju nebija redzamu izmaiÅu.
PÄc tam, kad es veltÄ«ju nedÄļu GHC Core izvades lasīŔanai, es spÄju atrisinÄt problÄmu ar vienu rindu ar skaidro INLINE. Kaut kur starp GHC 8.4.4 un GHC 8.6.5 optimizators pÄrstÄja to darÄ«t pats.
Es negaidÄ«ju sastapt tik daudz netÄ«rÄ«bas programmÄÅ”anÄ Haskell. TomÄr optimizatori pat mÅ«sdienÄs dažreiz pieļauj kļūdas, un mÅ«su uzdevums ir sniegt viÅiem norÄdes. PiemÄram, Å”eit mÄs zinÄm, ka funkcijai jÄbÅ«t inlinÄtai, jo tÄ ir iekļauta imperatÄ«vÄ versijÄ, un tÄdÄļ ir iemesls dot kompilatoram norÄdi.
Kas notika tÄlÄk?
TÄlÄk es Ä«stenoju Hopkrofta-Karpa algoritmu ar citÄm monÄdÄm, un uz to programma beidzÄs.
Pateicoties Google Summer of Code, es ieguvu praktisku pieredzi funkcionÄlajÄ programmÄÅ”anÄ, kas ne tikai palÄ«dzÄja man iekļūt prakses vietÄ Jane Street nÄkamajÄ vasarÄ (neesmu pÄrliecinÄts, cik pazÄ«stams ir Å”is vieta pat starp zinÄtnieku auditoriju HabrÄ, bet tas ir viens no nedaudzajiem, kur vasarÄ var nodarboties ar funkcionÄlo programmÄÅ”anu), bet arÄ« iepazÄ«stinÄja ar apbrÄ«nojamo Å”o paradigmu praktisko pielietojumu pasauli, kas ievÄrojami atŔķiras no manas pieredzes tradicionÄlajÄs valodÄs.
Avots: habr.com
