GSoC 2019: グラフの二郚性ずモナド倉換子のチェック

去幎の倏に参加したのですが、 Google Summer of Codeの - Google の孊生向けプログラム。 毎幎、䞻催者は、次のような有名な組織からのオヌプン゜ヌス プロゞェクトをいく぀か遞択したす。 Boost.org О Linux Foundation。 Google は、これらのプロゞェクトに取り組むよう䞖界䞭から孊生を招埅しおいたす。 

Google Summer of Code 2019 の参加者ずしお、ラむブラリ内でプロゞェクトを実行したした 藻類 組織ずずもに Haskell.org、最も有名な関数型プログラミング蚀語の XNUMX ぀である Haskell 蚀語を開発しおいたす。 Alga は以䞋を衚すラむブラリです。 タむプセヌフ Haskell でのグラフ衚珟。 たずえば、次のように䜿甚されたす。 セマンティック — コヌドに基づいおセマンティック ツリヌ、呌び出しグラフ、䟝存関係グラフを構築し、それらを比范できる Github ラむブラリ。 私のプロゞェクトは、XNUMX 郚グラフのタむプセヌフ衚珟ずその衚珟のためのアルゎリズムを远加するこずでした。 

この投皿では、Haskell でグラフの二郚性をチェックするアルゎリズムの実装に぀いお説明したす。 このアルゎリズムは最も基本的なものの XNUMX ぀ですが、それを関数型スタむルで矎しく実装するには数回の反埩が必芁で、かなりの䜜業が必芁でした。 その結果、モナドトランスフォヌマヌを䜿甚した実装に萜ち着きたした。 

GSoC 2019: グラフの二郚性ずモナド倉換子のチェック

私に぀いお

私の名前はノァシリヌ・アルフェロフ、サンクトペテルブルク倧孊の XNUMX 幎生です。 以前曞いたブログで 私のプロゞェクトに぀いお パラメヌタ化されたアルゎリズムに぀いお О ZuriHacぞの旅行に぀いお。 珟圚、私はむンタヌンシップに参加しおいたす ベルゲン倧孊 ノルりェヌでこの問題ぞのアプロヌチに取り組んでいたす リストの色付け。 私の興味は、パラメヌタヌ化されたアルゎリズムず関数型プログラミングです。

アルゎリズムの実装に぀いお

序文

プログラムに参加する孊生にはブログを曞くこずが匷く掚奚されたす。 圌らは私にブログのプラットフォヌムを提䟛しおくれたした ハスケルの倏。 この蚘事は翻蚳です 蚘事、XNUMX月に私がそこに英語で曞いたもので、短い序文が付いおいたす。 

問題のコヌドを含むプルリク゚ストが芋぀かりたす ここで.

私の仕事の結果に぀いお読むこずができたす英語 ここで.

この投皿では、読者が関数型プログラミングの基本抂念に粟通しおいるこずを前提ずしおいたすが、必芁なずきに䜿甚されおいるすべおの甚語を思い出すように努めたす。

グラフの二郚性のチェック 

グラフの二郚性をチェックするアルゎリズムは、通垞、最も単玔なグラフ アルゎリズムの XNUMX ぀ずしおアルゎリズムのコヌスで説明されたす。 圌のアむデアは単玔です。たず、䜕らかの方法で頂点を巊偎たたは右偎の共有に配眮し、矛盟する゚ッゞが芋぀かった堎合、グラフが XNUMX 郚構成ではないず䞻匵したす。

もう少し詳しく説明するず、たず巊偎の共有に頂点を配眮したす。 明らかに、この頂点の隣接する頂点はすべお右ロヌブに存圚する必芁がありたす。 さらに、この頂点の隣接頂点のすべおの隣接頂点は巊ロヌブに存圚する必芁があり、以䞋同様になりたす。 開始した頂点の接続コンポヌネント内に隣接を割り圓おおいない頂点がただ存圚する限り、頂点ぞの共有の割り圓おを続けたす。 次に、接続されおいるすべおのコンポヌネントに察しおこのアクションを繰り返したす。

同じパヌティションに属する頂点間に゚ッゞがある堎合、グラフ内で奇数のサむクルを芋぀けるのは難しくありたせんが、これは XNUMX 郚グラフでは䞍可胜であるこずが広く知られおいたす (そしお非垞に明癜です)。 それ以倖の堎合は、正しいパヌティションが埗られたす。これは、グラフが XNUMX 郚であるこずを意味したす。

通垞、このアルゎリズムは次を䜿甚しお実装されたす。 幅優先怜玢 たたは 深さ優先探玢。 呜什型蚀語では、通垞、深さ優先怜玢が若干単玔で远加のデヌタ構造を必芁ずしないため、深さ優先怜玢が䜿甚されたす。 たた、より䌝統的な深さ優先怜玢を遞択したした。

そこで、次のようなスキヌムにたどり着きたした。 深さ優先怜玢を䜿甚しおグラフの頂点を暪断し、それらにシェアを割り圓お、゚ッゞに沿っお移動するに぀れおシェアの数を倉曎したす。 すでに共有が割り圓おられおいる頂点に共有を割り圓おようずするず、そのグラフは XNUMX 郚構成ではないず安党に蚀えたす。 すべおの頂点に共有が割り圓おられ、すべおの゚ッゞが確認された時点で、適切なパヌティションが埗られたす。

蚈算の玔床

Haskell では、すべおの蚈算が次のように行われるず仮定したす。 きれいです。 しかし、これが本圓に事実であれば、画面に䜕も出力するこずができなくなりたす。 たったく、 きれいな 蚈算が怠惰すぎお蚈算が䞀぀もありたせん 掃陀 䜕かを蚈算する理由。 プログラム内で行われるすべおの蚈算は䜕らかの方法で匷制的に実行されたす。 「䞍朔」 モナドIO。

モナドは蚈算を衚珟する方法です。 効果 ハスケルで。 これらがどのように機胜するかに぀いおの説明は、この投皿の範囲を超えおいたす。 適切で明確な説明は英語で読むこずができたす ここで.

ここで指摘しおおきたいのは、IO などの䞀郚のモナドはコンパむラの魔法によっお実装されおいるが、その他のほずんどすべおのモナドは゜フトりェアで実装されおおり、それらの蚈算はすべお玔粋であるずいうこずです。

゚フェクトはたくさんあり、それぞれに独自のモナドがありたす。 これは非垞に匷力で矎しい理論です。すべおのモナドは同じむンタヌフェむスを実装したす。 次の XNUMX ぀のモナドに぀いお説明したす。

  • ea は、a 型の倀を返す蚈算か、e 型の䟋倖をスロヌする蚈算です。 このモナドの動䜜は呜什型蚀語の䟋倖凊理に非垞に䌌おおり、゚ラヌは捕捉たたは枡されたす。 䞻な違いは、呜什型蚀語は通垞オペレヌティング システムのメカニズムを䜿甚するのに察し、モナドは Haskell の暙準ラむブラリに完党に論理的に実装されるこずです。
  • 状態 sa は、型 a の倀を返し、型 s の可倉状態にアクセスする蚈算です。
  • たぶん、 might モナドは、Nothing を返すこずでい぀でも䞭断できる蚈算を衚したす。 ただし、Maybe 型の MonadPlus クラスの実装に぀いお説明したす。これは逆の効果を衚したす。これは、特定の倀を返すこずでい぀でも䞭断できる蚈算です。

アルゎリズムの実装

Graph a ず Bigraph ab ずいう XNUMX ぀のデヌタ タむプがあり、XNUMX ぀目はタむプ a の倀でラベル付けされた頂点を持぀グラフを衚し、XNUMX ぀目はタむプ a の倀でラベル付けされた頂点ず右偎の頂点を持぀ XNUMX 郚グラフを衚したす。 -タむプ b の倀でラベル付けされた偎の頂点。

これらは Alga ラむブラリのタむプではありたせん。 Alga には、無向二郚グラフの衚珟がありたせん。 わかりやすくするためにこのようなタむプを䜜成したした。

次のシグネチャを持぀ヘルパヌ関数も必芁になりたす。

-- СпОсПк сПсеЎей ЎаММПй вершОМы.
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)

深さ優先怜玢䞭に競合する゚ッゞが芋぀かった堎合、奇数のサむクルが再垰スタックの最䞊郚にあるこずが簡単にわかりたす。 したがっお、それを埩元するには、再垰スタックから最初に出珟する最埌の頂点たですべおを切断する必芁がありたす。

各頂点の共有番号の連想配列を維持するこずにより、深さ優先怜玢を実装したす。 再垰スタックは、遞択したモナドの Functor クラスの実装を通じお自動的に維持されたす。必芁なのは、パスのすべおの頂点を再垰関数から返された結果に入れるこずだけです。

私の最初のアむデアは、必芁な効果を正確に実装しおいるず思われる、Either モナドを䜿甚するこずでした。 私が最初に曞いた実装は、このオプションに非垞に近いものでした。 実際、私はある時点で XNUMX ぀の異なる実装を考えたしたが、最終的には別の実装に萜ち着きたした。

たず、共有識別子の連想配列を維持する必芁がありたす。これは状態に関するものです。 次に、競合が怜出されたずきに停止できる必芁がありたす。 これは、Either の Monad か、Maybe の MonadPlus のいずれかになりたす。 䞻な違いは、どちらも蚈算が停止しおいない堎合に倀を返すこずができるのに察し、Maybe はこの堎合、これに関する情報のみを返すこずです。 成功を衚す別の倀は必芁ない (倀は既に State に保存されおいる) ため、Maybe を遞択したす。 そしお、XNUMX ぀のモナドの効果を組み合わせる必芁がある瞬間に、それらは出おきたす。 モナド倉換子、これらの効果を正確に組み合わせたす。

なぜこのような耇雑なタむプを遞択したのでしょうか? 理由は XNUMX ぀ありたす。 たず、実装は呜什型ず非垞に䌌おいるこずがわかりたす。 次に、再垰から戻るずきに競合が発生した堎合に戻り倀を操䜜しお、奇劙なルヌプを埩元する必芁がありたす。これは、Maybe モナドで行う方がはるかに簡単です。

したがっお、この実装が埗られたす。

{-# 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)

where ブロックはアルゎリズムの䞭栞です。 その内郚で䜕が起こっおいるのかを説明しおみたす。

  • inVertex は、初めお頂点にアクセスする深さ優先怜玢の䞀郚です。 ここでは、頂点に共有番号を割り圓お、すべおの近傍に察しお onEdge を実行したす。 ここはコヌル スタックを埩元する堎所でもありたす。msum が倀を返した堎合は、そこに頂点 v をプッシュしたす。
  • onEdge ぱッゞにアクセスする郚分です。 これぱッゞごずに XNUMX 回呌び出されたす。 ここでは、反察偎の頂点が蚪問されおいるかどうかを確認し、蚪問されおいない堎合は蚪問したす。 アクセスされた堎合は、゚ッゞが競合しおいるかどうかを確認したす。 そうであれば、再垰スタックの最䞊郚の倀を返したす。戻り時に他のすべおの頂点がそこに配眮されたす。
  • processVertex は、各頂点が蚪問されおいるかどうかを確認し、蚪問されおいない堎合は inVertex を実行したす。
  • dfs はすべおの頂点で processVertex を実行したす。

それで党郚です。

INLINEずいう蚀葉の歎史

INLINE ずいう単語はアルゎリズムの最初の実装には存圚せず、埌から登堎したした。 より良い実装を芋぀けようずしたずころ、䞀郚のグラフでは非 INLINE バヌゞョンの方が著しく遅いこずがわかりたした。 意味的には関数は同じように動䜜するはずであるこずを考えるず、これには非垞に驚きたした。 さらに奇劙なこずに、異なるバヌゞョンの GHC を搭茉した別のマシンでは目立った違いはありたせんでした。

8.4.4 週間かけお GHC コアの出力を読んだ埌、8.6.5​​ 行の明瀺的な INLINE で問題を解決できたした。 GHC XNUMX ず GHC XNUMX の間のある時点で、オプティマむザはこれを独自に行うこずを停止したした。

Haskell プログラミングでこのような汚れに遭遇するずは予想しおいたせんでした。 しかし、今でもオプティマむザヌは時々間違いを犯すので、それらにヒントを䞎えるのが私たちの仕事です。 たずえば、ここでは関数が呜什型バヌゞョンでむンラむン化されおいるため、むンラむン化する必芁があるこずがわかり、これがコンパむラにヒントを䞎える理由になりたす。

次に䜕が起こりたしたか

次に、他のモナドを䜿甚しお Hopcroft-Karp アルゎリズムを実装し、プログラムは終了したした。

Google Summer of Code のおかげで、私は関数型プログラミングの実践的な経隓を積むこずができ、それが翌幎の倏にゞェヌン ストリヌトでむンタヌンシップに参加できるだけでなく、ハブルの知識豊富な聎衆の間でもこの堎所がどの皋床知られおいるかはわかりたせんが、倏䌑みに関数型プログラミングに取り組むこずができる数少ない堎所の䞀぀ですだけでなく、䌝統的な蚀語での私の経隓ずは倧きく異なる、このパラダむムを実際に適甚する玠晎らしい䞖界にも私を玹介しおくれたした。

出所 habr.com

コメントを远加したす