dot言語を使わずにGraphvizでグラフを描く便利なライブラリ

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/8850de9e-a338-505e-31d3-2cebf5657395.png" width=600 />

[Graphviz](https://www.graphviz.org/)はオープンソースのグラフ描画ソフトです。[dot言語](https://graphviz.gitlab.io/_pages/doc/info/lang.html)というグラフ構造を記述する言語によって書かれたグラフを綺麗に描画してくれます。Graphvizの良いところの一つはdot言語でグラフ構造さえ書けばノードやエッジをどのように配置するかは勝手に決めてくれるところでしょう。この機能があるおかげでグラフの管理や自動生成などが簡単に実現できます。

しかしグラフを描きたいと思っただけなのに新しい言語を覚えるのは大変です。加えてdot言語には変数や関数など抽象化をサポートする機能が乏しく複雑なグラフを描こうとするとコピペが大量に発生して記述が冗長になることもあります。そこで[graphviz](https://hackage.haskell.org/package/graphviz)というライブラリを使えばHaskellのEDSLとしてグラフ構造を記述することが可能になり、直接Graphvizを通してグラフ画像を生成することができます。必要ならdot言語を生成することも可能です。ちなみにどちらも同じ名前で紛らわしいので以降は小文字でgraphvizと書けばHaskellのライブラリの方を表すこととします。

例えばこんなグラフは

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/723c9d72-1374-b9b7-7763-b9fd6afc6b3f.png" width=100 />

以下のように記述して実行すれば画像ファイルとして生成できます。

```hs
hello :: DotGraph Text
hello = digraph' $ do
  "Hello"    --> "Graphviz"
  "Graphviz" --> "World"

main = runGraphviz hello Png "hello.png"
```

HaskellのEDSLとしてグラフ構造を記述できることのメリットは以下のようなものでしょう。

* dot言語の文法を知らなくても書ける
* Haddockを読めば出来ることがだいたい分かる[^3]
* 型が守ってくれる（dotだと文字列と数値を間違えても無視されたり…）
* EDSLなのでHaskellの言語機能（変数・関数など）が利用できる

さらにgraphvizは記述したグラフ構造を[fgl](http://hackage.haskell.org/package/fgl)のデータ型に変換できるので作ったグラフの構造を解析することも可能です。

以下では

1. グラフ構造の書き方
2. グラフ構造の出力方法

の順にgraphvizの使い方を見ていくことにしましょう。

また記事中のサンプルコードを実行するのに必要な言語拡張とimport文を以下に書いておきます。

```hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

import Data.GraphViz.Attributes
import Data.GraphViz.Commands
import Data.GraphViz.Types.Generalised
import Data.GraphViz.Types.Monadic

import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as Text
```

## グラフ構造の書き方
graphvizにはdot言語のグラフ構造を表現する4つの方法があります[^1]。

* `Data.GraphViz.Types.Canonical` - 他のグラフ構造からdotのグラフ構造に変換するのに適している
* `Data.GraphViz.Types.Generalised` - 既存のdot言語のコードをパースするための表現として適している
* `Data.GraphViz.Types.Graph` - 既存のdot言語のグラフ構造を操作する（ノードを足すとか）のに適している
* `Data.GraphViz.Types.Monadic` - 一番簡単な表現でありdot言語のグラフ構造を手で書くのに適している

この記事では `Data.GraphViz.Types.Monadic` に焦点を当てて解説していきます。ドキュメントにも「目を細めて見ればdot言語のようにも見えてくる」と書いてあるぐらいで、既にdot言語を知っている人にも分かりやすい表現でしょう。

グラフ構造を書くことのゴールは`DotGraph n`という型の値を作ることです。この型を返り値の型として持つ関数として

```hs
digraph' :: DotM n a -> DotGraph n
graph' :: DotM n a -> DotGraph n
```

の２つがあります[^2]。`digraph'`はDirected Graphつまり有向グラフを作成する関数で、`graph'`は無向グラフを作成する関数です。

グラフ構造を書く作業の大半は、上記いずれの関数も引数としている`DotM n a`という型の値を作ることになります。`DotM n a`の値を作る関数を列挙してみましょう。

```hs
type Dot n = DotM n ()

-- デフォルトの属性を設定する系
graphAttrs :: Attributes -> Dot n
nodeAttrs :: Attributes -> Dot n
edgeAttrs :: Attributes -> Dot n

-- サブグループを宣言する系
subgraph :: GraphID -> DotM n a -> Dot n
cluster :: GraphID -> DotM n a -> Dot n

-- ノードを宣言する系
node :: n -> Attributes -> Dot n
node' :: n -> Dot n

-- エッジを宣言する系
edge :: n -> n -> Attributes -> Dot n
(-->) :: n -> n -> Dot n
```

さて、ここまでくれば冒頭に掲載したOpticsの関係を表すグラフ[^4]を描画することが可能です。

```hs
lens :: DotGraph Text
lens = digraph' $ do
  let nFold      = "Fold s a"
      nSetter    = "Setter s t a b"
      nGetter    = "Getter s a"
      nTraversal = "Traversal s t a b"
      nLens      = "Lens s t a b"
      nReview    = "Review s a"
      nPrism     = "Prism s t a b"
      nIso       = "Iso s t a b"
      nEquality  = "Equality s t a b"

  nGetter     --> nFold
  nTraversal  -|> nFold
  nTraversal  --> nSetter
  nLens       -|> nGetter
  nLens       --> nTraversal
  nPrism      --> nTraversal
  nPrism      -|> nReview
  nIso        --> nLens
  nIso        --> nPrism
  nEquality   --> nIso
  where
    x -|> y = edge x y [textLabel "s = t, a = b"]

main = runGraphviz lens Png "lens.png"
```

実行すると、

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/b1dd93b7-bf19-90b3-a879-e8def09629d3.png" width=400 />

このような画像ファイルが生成されます。

コードを眺めると

* ノードIDを変数に入れて使い回している
* 同じテキストラベルの矢印を関数にして再利用している

のがわかると思います。まだ単純なグラフなので恩恵は少ないかもしれませんが、こうした再利用性の高いコードを書けるのがHaskellのEDSLとして書くことのメリットでしょう。

### Attributes
さて、グラフ構造を書く上でもう一つ大事な要素である`Attributes`について解説していきます。

```hs
type Attributes = [Attribute]
```

`Attributes`は`Attribute`のリストのエイリアスになっています。`Attribute`はグラフの見た目を制御する値の型で[定義](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Attributes-Complete.html#t:Attribute)を見ると、
<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/b62d9466-f671-b325-8680-c339246873b1.png" width=800 />
非常にたくさんの値が利用できることがわかります（まだまだあります）。さすがGraphvizのほとんど全ての属性とスタイルをカバーしていると銘打っているだけはありますね。この記事ではいくつかのAttributeに絞って解説していきたいと思います。


```hs
-- ラベル
textLabel :: Text -> Attribute

-- 色
color :: NamedColor nc => nc -> Attribute

-- ノードの形
shape :: Shape -> Attribute

-- エッジの属性
style :: Style -> Attribute
edgeEnds :: DirType -> Attribute
arrowTo :: Arrow -> Attribute
arrowFrom :: Arrow -> Attribute
```

以下のような単純なグラフが`Attributes`を設定することでどう変わっていくかを見てみましょう。

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/fa06c344-d566-6dce-234a-1a981da5ad40.png" width=100 />

```hs
attrTest :: DotGraph Text
attrTest = digraph' $ do
  "a" --> "b"
  "a" --> "c"
  "b" --> "c"
```

#### Label

まず`textLabel`は標準的なラベルを設定するAttributeです。

```hs
attrTest :: DotGraph Text
attrTest = digraph' $ do
  node "a" [textLabel "Label for a"]
  "a" --> "b"
  "a" --> "c"
  edge "b" "c" [textLabel "Label for b to c"]
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/af54199b-79c8-287e-dbb0-63d2b1869956.png" width=150 />

ノードに設定したラベルはノードに表示される文字列を変え、エッジに設定したラベルはエッジの説明を設定しているのが分かると思います。

#### Color

`color`は文字通り色を変更するAttributeです。

```hs
attrTest :: DotGraph Text
attrTest = digraph' $ do
  node "a" [color Red]
  "a" --> "b"
  "a" --> "c"
  "b" --> "c"
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/cf0d6220-73e5-06f2-d1e7-1232fd960a08.png" width=100 />

設定するとノードの色が変わりました。`color`以外にも

```hs
bgColor :: NamedColor nc => nc -> Attribute
fillColor :: NamedColor nc => nc -> Attribute
fontColor :: NamedColor nc => nc -> Attribute
penColor :: NamedColor nc => nc -> Attribute
```

のように細かい部分の色を設定する関数が用意されています。詳しい説明は[Haddock](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Attributes.html#g:3)を読むと分かりやすいでしょう。

#### Shape

`shape`はノードの形を変えるAttributeです。

```hs
attrTest :: DotGraph Text
attrTest = digraph' $ do
  node "a" [shape BoxShape]
  node "b" [shape Triangle]
  node "c" [shape Star]
  "a" --> "b"
  "a" --> "c"
  "b" --> "c"
```


<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/5f9efea9-eab0-2c23-b0fa-090dbf0bf0af.png" width=150 />

どのような形が使えるかは[Graphvizのドキュメント](https://graphviz.gitlab.io/_pages/doc/info/shapes.html)にまとまっています。対応する値は[Haddock](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Attributes.html#t:Shape)を参照してみてください。

#### Edge Attributes
最後にエッジに関連するAttributesを見ていきましょう。

```hs
attrTest :: DotGraph Text
attrTest = digraph' $ do
  "a" --> "b"
  edge "a" "c" [style dotted]
  edge "b" "c" [arrowFrom tee, arrowTo diamond, edgeEnds Both]
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/1bd06cd9-413e-e633-28c1-c582958470cd.png" width=150 />

点線などのエッジの見た目や矢印の形を変えることができました。Attributeの設定に使用できる`Style`, `DirType`, `Arrow`も[Haddock](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Attributes.html#g:4)から確認することができます。

### Record
レコードはノードをセルに分割することが出来る機能で、ラベルを`|`で区切ると横に、`{|}`を使って区切ると縦に分割することができます。

```hs
typeClasses :: DotGraph Text
typeClasses = digraph' $ do
  nodeAttrs [shape Record]

  node "Functor" [textLabel "{Functor|map :: (a -&gt; b) -&gt; f a -&gt; f b}"]
  node "Applicative" [textLabel "{Applicative|pure :: a -&gt; f a\n(&lt;*&gt;) :: f (a -&gt; b) -&gt; f a -&gt; f b}"]
  node "Monad" [textLabel "{Monad|join :: m (m a) -&gt; m a}"]
  node "Foldable" [textLabel "{Foldable|foldMap :: Monoid m =&gt; (a -&gt; m) -&gt; t a -&gt; m}"]
  node "Traversable" [textLabel "{Traversable|sequenceA :: Applicative f =&gt; t (f a) -&gt; f (t a)}"]

  "Functor" --> "Applicative"
  "Applicative" --> "Monad"
  "Functor" --> "Traversable"
  "Foldable" --> "Traversable"
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/17134/9db8a100-c79b-0b83-243d-6f52b8211cb7.png" width=500 />

レコードは便利ですが特殊な記法をラベルの文字列の中に押し付けてしまっているので視認性が悪いのが欠点です。今回の場合 他にもIDを2回書く必要があったり文字実体参照が読みにくかったり不満なところがいくつかあるので改善してみましょう。

```hs
typeClasses :: DotGraph Text
typeClasses = digraph' $ do
  nodeAttrs [shape Record]

  record "Functor" "map :: (a -> b) -> f a -> f b"
  record "Applicative" "pure :: a -> f a\n(<*>) :: f (a -> b) -> f a -> f b"
  record "Monad" "join :: m (m a) -> m a"
  record "Foldable" "foldMap :: Monoid m => (a -> m) -> t a -> m"
  record "Traversable" "sequenceA :: Applicative f => t (f a) -> f (t a)"

  "Functor" --> "Applicative"
  "Applicative" --> "Monad"
  "Functor" --> "Traversable"
  "Foldable" --> "Traversable"
  where
    record :: Text -> Text -> Dot Text
    record label desc = node label $
      [textLabel $ Text.concat ["{", label, "|", escapeText desc, "}"]]
    escapeText :: Text -> Text
    escapeText = Text.concatMap $ \case
      '"'  -> "&quot;"
      '&'  -> "&amp;"
      '\'' -> "&apos;"
      '<'  -> "&lt;"
      '>'  -> "&gt;"
      els  -> Text.singleton els
```

レコードを組み立てる部分を関数に切り出すことで視認性の高い記述ができるようになりました。このように関数というHaskellの言語機能を利用できるようになるのがEDSLで書くことの利点の一つと言えるでしょう。

## グラフ構造の出力方法

これまで記述したグラフ構造は `runGraphviz` という関数を使って画像ファイルに出力していました。この関数は[`Data.GraphViz.Commands`](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Commands.html)というモジュールで提供されており

```hs
runGraphviz :: PrintDotRepr dg n => dg n -> GraphvizOutput -> FilePath -> IO FilePath
```

のような型をしています。この `GraphvizOutput` を変更すれば`Bmp`や`Jpeg`など出力形式を変えることができ、`DotOutput`を指定すればdot言語を吐き出すこともできます。しかし`DotOuput`で出力されるdot言語のファイルはレイアウト情報なども全て含んでおり見やすいものではありません。dot言語のファイルを出力したいときは[`Data.GraphViz.Commands.IO`](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Commands-IO.html)で提供されている、

```hs
writeDotFile :: PrintDotRepr dg n => FilePath -> dg n -> IO ()
```

という関数を利用するのが良いでしょう。EDSLで組み立てたdot言語のファイルを直接出力することができます。

また[`Data.GraphViz`](https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz.html)で提供されている

```hs
graphToDot :: (Ord cl, Graph gr) => GraphvizParams Node nl el cl l -> gr nl el -> DotGraph Node
dotToGraph :: (DotRepr dg Node, Graph gr) => dg Node -> gr Attributes Attributes
```

という関数を使えばfglというライブラリで提供されているグラフのデータ構造と相互に変換することが可能です。

## まとめ
graphvizで提供されているHaskellのEDSLを使うことでdot言語を書かずにHaskellの言語機能を利用しながらグラフを生成することができました。今回紹介した `Data.GraphViz.Types.Monadic` だけでなく `Data.GraphViz.Types.Graph` には動的にグラフを生成するメソッドが用意されており、他のデータを参照してグラフを自動生成すようなことも非常に簡単にできると思います。みなさんもぜひgraphvizを使って快適なグラフ描画ライフを体験してみてください！

※この記事は [第20回Haskell-jpもくもく会](https://haskell-jp.connpass.com/event/121571/) のもくもく時間を利用して書かれました。


[^1]: https://hackage.haskell.org/package/graphviz-2999.20.0.3/docs/Data-GraphViz-Types.html
[^2]: この記事では`GraphID`を使った例は紹介しません。実際は`digraph`, `graph`という`'`を伴わない関数もあり、こちらの関数を使えば`GraphID`を明示的に与えることが可能です。
[^3]: Haskellでは大体どのライブラリにもHaddockという自動生成されたドキュメントが付属していて、そこに書かれている型や説明を見ることで使い方を把握することができます。例えば明示的なインポートをせずに使えるPreludeのHaddockはこちらです <http://hackage.haskell.org/package/base/docs/Prelude.html>
[^4]: http://hackage.haskell.org/package/lens