tag:crieit.net,2005:https://crieit.net/users/reouno/feed reounoの投稿 - Crieit Crieitでユーザーreounoによる最近の投稿 2019-09-29T19:46:32+09:00 https://crieit.net/users/reouno/feed tag:crieit.net,2005:PublicArticle/15437 2019-09-29T19:46:32+09:00 2019-09-29T19:46:32+09:00 https://crieit.net/posts/Data-Aeson Data.Aesonの使い方 <p><a target="_blank" rel="nofollow noopener" href="https://www.leo-leo.uno/2019/02/09/798/">自分のブログ記事</a>からのクロス投稿です。</p> <p>HaskellのJSONライブラリとしておそらくメジャーな<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson">Data.Aeson</a>ですが、使い方が難しいので色々試行錯誤しています。</p> <p>以降のコードはghci上で実行することを想定しています。<br /> 適当にstackでプロジェクトを作って、aesonをcabalファイルのbuild-dependsに追加してビルドし、<code>stack ghci</code>とするのが良いと思います。あと、<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/text">Data.Text</a>や<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/unordered-containers">Data.HashMap</a>なども必要です。</p> <pre><code class="shell"># ここはターミナルで stack new sandbox-prj simple cd sandbox-prj # cabalファイルのbuild-dependsにaeson, text, unordered-containers, vectorを追記 stack build stack ghci </code></pre> <p>はじめに、以下をghci上でコピペしておいてください。以降のコードが全部動くはずです。</p> <pre><code class="haskell">-- ghci上で -- 最初にこれらをコピペしておくと、以降のコードが全部動くはず -- 後でGHC拡張を使う :set -XDeriveGeneric :set -XExistentialQuantification :set -XFlexibleContexts :set -XFlexibleInstances :set -XGADTs :set -XInstanceSigs :set -XOverloadedStrings :set -XTypeFamilies :set +m -- 後で色々使う import Control.Applicative (empty, pure) import Data.Aeson import qualified Data.Aeson.Encoding.Internal as DAE import Data.Aeson.Types import Data.Char (toLower, toUpper) import qualified Data.HashMap.Lazy as HML import qualified Data.HashMap.Strict as HMS import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Vector as V import GHC.Generics </code></pre> <h3 id="基本的な使い方"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9">基本的な使い方</a></h3> <p>基本的な型については特にパーサーを定義しなくて良いので、いきなりエンコード・デコードできます。</p> <p>エンコード</p> <pre><code class="haskell">encode 1 -- --> "1" encode "hello" -- --> "\"hello\"" encode [1,2,3] -- --> "[1,2,3]" encode (1,2) -- --> "[1,2]" encode (Just 1) -- --> "1" encode True -- --> "true" encode Null -- --> "null" encode (1234, "hello", True, False, [1,2,3], Just 10) -- --> "[1234,\"hello\",true,false,[1,2,3],10]" </code></pre> <p>デコードは、Haskellの型を指定する必要があります。</p> <pre><code class="haskell">-- 型を指定しないとデコードできない decode "1" -- --> Nothing decode "1" :: Maybe Int -- --> Just 1 decode "\"hello\"" :: Maybe String -- --> Just "hello" decode "[1234,\"hello\",true,false,[1,2,3],10]" :: Maybe (Int,String,Bool,Bool,[Int],Maybe Int) -- --> Just (1234,"hello",True,False,[1,2,3],Just 10) </code></pre> <h3 id="ToJSON, FromJSON"><a href="#ToJSON%2C+FromJSON">ToJSON, FromJSON</a></h3> <p>エンコード・デコードするためには、そのデータ型が<code>ToJSON</code>・<code>FromJSON</code>のインスタンスになっている必要があります。<br /> このように定義します。<code>DeriveGeneric</code>拡張(はじめにセットしたやつ)と<code>GHC.Generics</code>が必要です。</p> <pre><code class="haskell">data MyData = MyData { field :: String } deriving (Show, Generic) instance FromJSON MyData instance ToJSON MyData myData = MyData "my data!!" encode myData -- --> "{\"field\":\"my data!!\"}" decode "{\"field\":\"my data!!\"}" :: Maybe MyData -- --> Just (MyData {field = "my data!!"}) </code></pre> <p>簡単ですね。基本的には、エンコード・デコードの実装を自分で定義しなくても自動的に導出してくれます。</p> <pre><code class="haskell">-- :{ :} はghciで複数行定義をするときに使う記号 :{ data Person = Person { name :: String , age :: Int , group :: [String] } deriving (Show, Generic) :} instance FromJSON Person instance ToJSON Person p1 = Person "Neo" 30 ["groupA", "groupB"] p2 = Person "Morpheus" 40 ["groupA"] p3 = Person "Trinity" 30 [] encode p1 -- --> "{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"}" decode "{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"}" :: Maybe Person -- --> Just (Person {name = "Neo", age = 30, group = ["groupA","groupB"]}) -- リストはもともとToJSONとFromJSONのインスタンスなので、 encode [p1, p2, p3] -- --> "[{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"},{\"group\":[\"groupA\"],\"age\":40,\"name\":\"Morpheus\"},{\"group\":[],\"age\":30,\"name\":\"Trinity\"}]" decode "[{\"group\":[\"groupA\",\"groupB\"],\"age\":30,\"name\":\"Neo\"},{\"group\":[\"groupA\"],\"age\":40,\"name\":\"Morpheus\"},{\"group\":[],\"age\":30,\"name\":\"Trinity\"}]" :: Maybe [Person] -- --> Just [Person {name = "Neo", age = 30, group = ["groupA","groupB"]},Person {name = "Morpheus", age = 40, group = ["groupA"]},Person {name = "Trinity", age = 30, group = []}] </code></pre> <p>※ 彼らの年齢は間違っているかもしれません。</p> <p>Haskellのレコードを使うときは、以下のように型名のプレフィックスをフィールド名につけて定義することが多いかもしれません(あるいはlensなどを使うのかもしれませんが)。それでも、JSONのフィールド名はシンプルに"title", "created"としておきたいですよね。<br /> こういう場合、<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#v:defaultOptions"><code>defaultOptions</code></a>というのを使います。<code>fieldLabelModifier</code>に、フィールド名の変換関数を定義しておけば、エンコード時に適用してくれます。ここでは、プレフィックス("movie"の5文字)を取り除き、かつ小文字にするというルールにしました。</p> <pre><code class="haskell">:{ data Movie = Movie { movieTitle :: String , movieCreated :: Int } deriving (Show, Generic) :} movieOptions = defaultOptions {fieldLabelModifier = map toLower . drop 5} instance ToJSON Movie where toEncoding = genericToEncoding movieOptions instance FromJSON Movie where parseJSON = genericParseJSON movieOptions movie = Movie "The Matrix" 1999 encode movie -- --> "{\"title\":\"The Matrix\",\"created\":1999}" decode "{\"title\":\"The Matrix\",\"created\":1999}" :: Maybe Movie -- --> Just (Movie {movieTitle = "The Matrix", movieCreated = 1999}) </code></pre> <h3 id="エンコーダー・デコーダーを自前で定義"><a href="#%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC%E3%83%BB%E3%83%87%E3%82%B3%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92%E8%87%AA%E5%89%8D%E3%81%A7%E5%AE%9A%E7%BE%A9">エンコーダー・デコーダーを自前で定義</a></h3> <p>では、代数データ型の場合はどうでしょうか。基本的には以下のように自動導出できます。</p> <pre><code class="haskell">data Size = Small|Medium|Large deriving (Show, Generic) instance ToJSON Size instance FromJSON Size encode [Small, Medium, Large] -- --> "[\"Small\",\"Medium\",\"Large\"]" decode "[\"Small\",\"Medium\",\"Large\"]" :: Maybe [Size] -- --> Just [Small,Medium,Large] </code></pre> <p>ただ、JSONデータではキャメルケースでなく<code>small</code>, <code>medium</code>, <code>large</code>のように小文字で表現したいかもしれません。その場合は、以下のように自分で定義する必要があります。<code>ToJSON</code>のインスタンスにするには、<code>toJSON</code>か、<code>toEncoding</code>を自分で定義します。以下のような型です。</p> <pre><code class="haskell">toJSON :: a -> Value toEncoding :: a -> Encoding </code></pre> <p><code>toJSON</code>は任意の型をData.Aesonの<code>Value</code>型にします。したがって、<code>toJSON</code>のみを定義した場合は、<code>toEncoding</code>が自動導出されます。JSONにエンコードするときは、一旦<code>Value</code>に変換され、その後JSONの文字列に変換されます。<br /> 一方、<code>toEncoding</code>を直接定義することもできて、その場合は、<code>Value</code>を経由せず直接エンコードされることになります。ここには<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#g:7">歴史的経緯</a>があるようです。</p> <pre><code class="haskell">-- toJSONを定義する場合 data Size = Small|Medium|Large deriving (Show, Generic) instance ToJSON Size where toJSON Small = String "small" toJSON Medium = String "medium" toJSON Large = String "large" encode [Small, Medium, Large] -- --> "[\"small\",\"medium\",\"large\"]" </code></pre> <pre><code class="haskell">-- toEncodingを定義する場合 data Size = Small|Medium|Large deriving (Show, Generic) instance ToJSON Size where toEncoding Small = DAE.string "small" toEncoding Medium = DAE.string "medium" toEncoding Large = DAE.string "large" encode [Small, Medium, Large] -- --> "[\"small\",\"medium\",\"large\"]" </code></pre> <p><code>FromJSON</code>のインスタンスについては、<code>parseJSON</code>を定義します。こちらは少し面倒です。以下のような型をしています。</p> <pre><code class="haskell">parseJSON :: Value -> Parser a </code></pre> <pre><code class="haskell">-- withTextの第一引数("Size")は、どんな文字列でも良いみたい。 -- なんのための引数なのかまだわかっていない。 instance FromJSON Size where parseJSON = withText "Size" $ \t -> fromString (TL.unpack (TL.fromStrict t)) where fromString :: String -> Parser Size fromString "small" = pure Small fromString "medium" = pure Medium fromString "large" = pure Large fromString _ = empty decode "[\"small\",\"medium\",\"large\"]" :: Maybe [Size] -- --> Just [Small,Medium,Large] </code></pre> <p>これで、<code>Size</code>データを使ったレコードもエンコード・デコードすることができます。</p> <pre><code class="haskell">:{ data Tomato = Tomato { size :: Size , brand :: String } deriving (Show, Generic) :} instance ToJSON Tomato instance FromJSON Tomato mini = Tomato Small "famous brand name" encode mini -- --> "{\"size\":\"small\",\"brand\":\"famous brand name\"}" decode "{\"size\":\"small\",\"brand\":\"famous brand name\"}" -- --> Just (Tomato {size = Small, brand = "famous brand name"}) </code></pre> <h3 id="多相型"><a href="#%E5%A4%9A%E7%9B%B8%E5%9E%8B">多相型</a></h3> <p>このようなデータ型でもこれまでと同じように<code>ToJSON</code>・<code>FromJSON</code>のインスタンスにすることができます。自動導出の場合は、以下の例のようにエンコード時に<code>Wrap</code>が外れます。</p> <pre><code class="haskell">data Wrap a = Wrap a deriving (Show, Generic) instance ToJSON a => ToJSON (Wrap a) instance FromJSON a => FromJSON (Wrap a) encode $ Wrap 1 -- --> "1" encode $ Wrap "wrapped" -- --> "\"wrapped\"" decode "1" :: Maybe (Wrap Int) -- --> Just (Wrap 1) decode "\"wrapped\"" :: Maybe (Wrap String) -- --> Just (Wrap "wrapped") </code></pre> <p>今度は<code>Point a</code>型で考えます。自動導出で問題ない場合は良いのですが、自分で定義しようとした時に、少し悩みました。<code>toJSON</code>も<code>parseJSON</code>も、<code>a</code>の部分の値をエンコード・デコードする処理を自分で定義してしまうと型を決めなくてはなりません。そうすると<code>a</code>のままではできず、<code>Point Int</code>などの具体的な型で定義せざるを得ません。そこで、<code>a</code>の部分については、自分で定義せず、<code>toJSON</code>や<code>parseJSON</code>をそのまま使います。もともと<code>a</code>はそれらのインスタンスであることを仮定しているので、自分で定義する必要がありません。以下のように書くと、型引数をとる型についても定義できます。</p> <pre><code>data Point a = Point a a deriving (Show, Generic) instance ToJSON a => ToJSON (Point a) where toJSON (Point x y) = Object $ HMS.fromList [("point", toJSON [x,y])] :{ instance FromJSON a => FromJSON (Point a) where parseJSON (Object o) = case HML.lookup "point" o of Just arr -> let pts = parseJSON arr :: FromJSON b => Parser (b, b) in Point <$> (fst <$> pts) <*> (snd <$> pts) Nothing -> empty :} pi = Point 1 2 ps = Point "one" "two" encode pi -- --> "{\"point\":[1,2]}" encode ps -- --> "{\"point\":[\"one\",\"two\"]}" decode "{\"point\":[1,2]}" :: Maybe (Point Int) -- --> Just (Point 1 2) decode "{\"point\":[\"one\",\"two\"]}" :: Maybe (Point String) -- --> Just (Point "one" "two") </code></pre> <h3 id="少し複雑な代数データ型"><a href="#%E5%B0%91%E3%81%97%E8%A4%87%E9%9B%91%E3%81%AA%E4%BB%A3%E6%95%B0%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B">少し複雑な代数データ型</a></h3> <pre><code class="haskell">data Gender = Female|Male deriving (Show, Generic) data Property = Name String|Age Int|Gender Gender deriving (Show, Generic) instance ToJSON Gender instance FromJSON Gender instance ToJSON Property instance FromJSON Property props = [Name "Neo", Age 30, Gender Male] encode props -- --> "[{\"tag\":\"Name\",\"contents\":\"Neo\"},{\"tag\":\"Age\",\"contents\":30},{\"tag\":\"Gender\",\"contents\":\"Male\"}]" decode "[{\"tag\":\"Name\",\"contents\":\"Neo\"},{\"tag\":\"Age\",\"contents\":30},{\"tag\":\"Gender\",\"contents\":\"Male\"}]" :: Maybe [Property] -- --> Just [Name "Neo",Age 30,Gender Male] </code></pre> <p>このような型でもすべて自動導出できました。しかし、JSONのフォーマットは微妙ですね。以下のようになって欲しいです。(レコード型で定義すれば良いだけの話なのですが、今は例ということで。。)</p> <pre><code class="json">{ "name": "Neo", "age": 30, "gender": "male" } </code></pre> <p>自前で定義しましょう。名前を変えるため別の例にしますが、データ構造はほぼ一緒です。</p> <pre><code class="haskell">data CarType = Coupe|SUV deriving (Show, Generic) data Car = Model String|Release Int|CarType CarType deriving (Show, Generic) instance ToJSON CarType where toJSON Coupe = String "coupe" toJSON SUV = String "suv" instance FromJSON CarType where parseJSON = withText "CarType" $ \t -> fromString (T.unpack t) where fromString :: String -> Parser CarType fromString "coupe" = pure Coupe fromString "suv" = pure SUV fromString _ = empty instance ToJSON Car where toJSON (Model s) = object [("model", toJSON s)] toJSON (Release i) = object [("release", toJSON i)] toJSON (CarType c) = object [("car_type", toJSON c)] instance FromJSON Car where parseJSON (Object o) = case HML.lookup "model" o of Just (String s) -> pure (Model $ T.unpack s) _ -> case HML.lookup "release" o of Just i -> Release <$> (parseJSON i :: Parser Int) _ -> case HML.lookup "car_type" o of Just t -> CarType <$> (parseJSON t :: Parser CarType) myCar = [Model "Land Cruiser", Release 1954, CarType SUV] encode myCar -- --> "[{\"model\":\"Land Cruiser\"},{\"release\":1954},{\"car_type\":\"suv\"}]" decode "[{\"model\":\"Land Cruiser\"},{\"release\":1954},{\"car_type\":\"suv\"}]" :: Maybe [Car] -- --> Just [Model "Land Cruiser",Release 1954,CarType SUV] </code></pre> <p>これで、望みのJSON形式でエンコード・デコードできるようになりました。</p> <h3 id="Either型"><a href="#Either%E5%9E%8B">Either型</a></h3> <p><code>Either</code>型の場合はどうなるでしょうか。わからないのでまずは自動導出して見ましょう。</p> <pre><code class="haskell">:{ data Filter = Filter { value :: Either String [String] } deriving (Show, Generic) :} instance ToJSON Filter instance FromJSON Filter filterL = Filter (Left "eq") filterR = Filter (Right ["eq", "gt"]) encode filterL -- --> "{\"value\":{\"Left\":\"eq\"<span>}</span><span>}</span>" encode filterR -- --> "{\"value\":{\"Right\":[\"eq\",\"gt\"]<span>}</span><span>}</span>" decode "{\"value\":{\"Left\":\"eq\"<span>}</span><span>}</span>" :: Maybe (Filter) -- --> Just (Filter {value = Left "eq"}) decode "{\"value\":{\"Right\":[\"eq\",\"gt\"]<span>}</span><span>}</span>" :: Maybe (Filter) -- --> Just (Filter {value = Right ["eq","gt"]}) </code></pre> <p>一応できました。しかし、JSONデータの中に"Left"や"Right"が入ってしまって冗長です。この場合は、以下のようになって欲しいですね。</p> <pre><code class="json"># Leftの場合 {"value": "eq"} # Rightの場合 {"value": ["eq", "gt"]} </code></pre> <p>以下のように定義しましょう。別の例にするのが面倒なので、同じデータを再定義します。</p> <pre><code>:{ data Filter = Filter { value :: Either String [String] } deriving (Show, Generic) :} instance ToJSON Filter where toJSON (Filter (Left s)) = object [("value", toJSON s)] toJSON (Filter (Right ss)) = object [("value", toJSON ss)] -- Arrayの時に、判定できていないのが難点 -- でも、失敗した時はちゃんとNothingを返してくれるのでとりあえず良いか instance FromJSON Filter where parseJSON (Object o) = case HML.lookup "value" o of Just (String s) -> pure (Filter (Left $ T.unpack s)) Just arr -> (Filter . Right) <$> (parseJSON arr :: Parser [String]) _ -> empty filterL = Filter (Left "eq") filterR = Filter (Right ["eq", "gt"]) encode filterL -- --> "{\"value\":\"eq\"}" encode filterR -- --> "{\"value\":[\"eq\",\"gt\"]}" decode "{\"value\":\"eq\"}" :: Maybe Filter -- --> Just (Filter {value = Left "eq"}) decode "{\"value\":[\"eq\",\"gt\"]}" :: Maybe Filter -- --> Just (Filter {value = Right ["eq","gt"]}) </code></pre> <p><code>Either</code>型の場合もこのように定義することで自由にJSONとの変換ができます。ちなみに上記の例のようにではなく、<code>Either String a</code>としてエラーメッセージ付きの<code>Maybe</code>のような扱いをしたい場合は、<a target="_blank" rel="nofollow noopener" href="https://www.stackage.org/haddock/lts-13.6/aeson-1.4.2.0/Data-Aeson-Types.html#v:parseEither">専用の関数がすでに用意されている</a>ようです。</p> <h3 id="GADT"><a href="#GADT">GADT</a></h3> <p>この場合は、<code>Field String</code>と<code>Field Bool</code>の場合でそれぞれ定義するしかなさそうです。もしまとめて定義する方法があれば誰か教えてください。</p> <pre><code>data Field typ = (typ ~ String) => Title | (typ ~ Bool) => Done instance Show (Field typ) where show Title = "Title" show Done = "Done" instance ToJSON (Field typ) where toJSON Title = String "title" toJSON Done = String "done" :{ instance FromJSON (Field String) where parseJSON = withText "Field" $ \v -> case v of "title" -> pure Title _ -> empty instance FromJSON (Field Bool) where parseJSON = withText "Field" $ \v -> case v of "done" -> pure Done _ -> empty :} encode Title -- --> "\"title\"" encode Done -- --> "\"done\"" decode "\"title\"" :: Maybe (Field String) -- --> Just Title decode "\"done\"" :: Maybe (Field Bool) -- --> Just Done </code></pre> <p>で、この<code>Field</code>を使って、以下のようなレコードを定義します。</p> <pre><code>:{ data Filter = forall typ. Show typ => Filter { field :: Field typ , value :: typ } :} </code></pre> <p>なぜこんなデータ型を考えているかというと、実は<a target="_blank" rel="nofollow noopener" href="http://hackage.haskell.org/package/persistent">persistent</a>のクラスに、<a target="_blank" rel="nofollow noopener" href="https://github.com/yesodweb/persistent/blob/master/persistent/Database/Persist/Class/PersistEntity.hs#L135">Filter</a>っていうのがあって、これを<code>FromJSON</code>のインスタンスにしたかったのです。このデータをもう少し簡易にしたものが、上で定義した<code>Filter</code>です。ここでの肝は、型引数の<code>typ</code>が型としては明示的に現れていない点です。つまり<code>Filter Title "some title"</code>も、<code>Filter Done True</code>も型としては単に<code>Filter</code>型ですが、前者の<code>typ</code>は<code>String</code>で後者は<code>Bool</code>です。</p> <pre><code>-- どちらも単にFilter型 :t Filter Title "some title" -- --> Filter Title "some title" :: Filter :t Filter Done True -- --> Filter Done True :: Filter </code></pre> <p>このようなデータ型の場合、先ほど<code>Field</code>の<code>FromJSON</code>を<code>Field String</code>と<code>Field Bool</code>でそれぞれ定義した時のようなことができません。<code>field</code>の値が<code>Title</code>なら<code>typ</code>は<code>String</code>、<code>Done</code>なら<code>Bool</code>というような場合分けをする必要があります。色々試行錯誤してようやくできた定義が以下です。<code>mayTitle</code>と<code>mayDone</code>の場合分けのところは、この2つデータの型が違うので、それぞれ別の変数にせざるを得ませんでした。そのため<code>mayField</code>を必ず2回評価することになり、無駄の多い実装になってしまいました。</p> <pre><code>instance FromJSON Filter where parseJSON (Object o) = do let mayField = HMS.lookup "field" o let mayValue = HMS.lookup "value" o let mayTitle = case mayField of Just (String "title") -> Just Title _ -> Nothing let mayDone = case mayField of Just (String "done") -> Just Done _ -> Nothing case mayValue of Just value -> do case mayTitle of Just title -> Filter title <$> (parseJSON value :: Parser String) Nothing -> case mayDone of Just done -> Filter done <$> (parseJSON value :: Parser Bool) Nothing -> empty Nothing -> empty -- Filter Title "some title" case decode "{\"field\":\"title\", \"value\":\"some title\"}" :: Maybe Filter of Just (Filter field value) -> print value Nothing -> print Nothing -- Filter Done True case decode "{\"field\":\"done\", \"value\":true}" :: Maybe Filter of Just (Filter field value) -> print value Nothing -> print Nothing </code></pre> reouno tag:crieit.net,2005:PublicArticle/15415 2019-09-23T09:59:26+09:00 2019-09-26T13:00:18+09:00 https://crieit.net/posts/Haskell Haskellで複雑度測定 <p>※ この記事は<a target="_blank" rel="nofollow noopener" href="https://www.leo-leo.uno/2019/09/23/1050/">Haskellで複雑度測定</a>からの転載です。</p> <p>自分で書いているブログのPVが増えなくて悲しいので、こういうところにも載せて見たら多少増えるんじゃないかと思って試しに載せて見ます。</p> <hr /> <p>Haskellでサイクロマティック複雑度(循環的複雑度)を測定する場合、2つツールがあるようです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/mgajda/homplexity">homplexity</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/rubik/argon">argon</a></li> </ul> <p>githubのスター数はargonの方が多いのですが、コミット履歴を見るとhomplexityの方が今もメンテナンスされているようなのでhomplexityがオススメです。機能的な違いは特に調べていません。</p> <h3 id="homplexityの機能"><a href="#homplexity%E3%81%AE%E6%A9%9F%E8%83%BD">homplexityの機能</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://www.migamake.com/projects/homplexity.html">公式サイト</a>によれば、以下の統計値を出してくれるようです。</p> <ul> <li>Cyclomatic complexity</li> <li>Code metric</li> <li>Lines of code</li> <li>Branching depth</li> <li>Type metric</li> <li>Comment metric</li> <li>Code to comments ratio</li> <li>Type tree nodes</li> <li>Number of function arguments</li> </ul> <p>本題から外れますがBranching depthってなんでしょうか。説明には</p> <pre><code>Branching is used with conditionals and has been used as a criterion for both software complexity and logic. </code></pre> <p>と書いてあります。分岐の数のことなんでしょうか。ちょっと試して見たところ、if分岐がある場合でも0でしたが、ifの中にifをネストさせると1になり、ifの中にifの中にifとネストさせると2になりました。だから分岐の深さってことか。なんかサイクロマティック複雑度と似ていますね。</p> <h3 id="homplexityのインストール"><a href="#homplexity%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">homplexityのインストール</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://docs.haskellstack.org/en/stable/README/">stack</a>が入っていれば簡単です。クローンしてstack installするだけです。</p> <pre><code class="bash">git clone https://github.com/mgajda/homplexity.git cd homplexity stack install </code></pre> <p>で、<code>~/.local/bin</code>に<code>homplexity-cli</code>ができるはずです。パスを通すなら、<code>~/.bashrc</code>や<code>~/.bash_profile</code>などに</p> <pre><code class="bash">PATH=$PATH:~/.local/bin </code></pre> <p>を追記すれば良いです。</p> <p>これで簡単にインストールできるのは良いのですが、このリポジトリで指定しているGHCバージョンが入っていない場合、そのインストールと依存パッケージのインストールから始めるので<strong>結構時間がかかる</strong>かもしれません。これがstackのデメリットですよね(メリットなのか?cabal hellという言葉を思い出した、そして<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/50925938/what-is-cabal-hell">What is Cabal Hell?</a>というStackoverflowを見つけたw)。</p> <h3 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h3> <p>githubリポにも書いてありますが、以下のようにメインモジュールファイルと依存モジュールのルートディレクトリを指定します。</p> <pre><code class="bash"># プロジェクトディレクトリ構成がこのようになっている場合 root --- app --- Main.hs | |- src --- Lib.hs |- ... homplexity-cli app/Main.hs src/ </code></pre> <p>これで、メインモジュールと依存モジュールファイル群を見つけて各メソッドの複雑度を測定してくれます。出力は、</p> <pre><code class="bash">Warning:src/InterfaceAdapter/Presenter/StockAPIHandler.hs:SrcLoc "src/InterfaceAdapter/Presenter/StockAPIHandler.hs" 34 1:function stockServer has 48 lines of code should be kept below 20 lines of code. Correctly parsed 16 out of 16 input files. </code></pre> <p>こんなのが出てきます。このファイルのこのメソッドが48行もあるから20行未満に抑えた方が良いよ、と言っています。これは複雑度ではありませんが。</p> <p>もっと詳細な情報を出して欲しい場合は<code>--severity</code>というオプションを使います。ログレベルの設定のような感じです。<code>homplexity -h</code>を見てもらえばわかりますが、</p> <pre><code># ヘルプの説明 --severity={Debug|Info|Warning|Critical} level of output verbosity (Debug Info Warning Critical) (default: Warning, from module: Main) </code></pre> <p>とのことなのでデフォルトはWarningですがInfoにすればもっと色々出てきます。以下はInfoで解析した時の出力の一部です。</p> <pre><code>homplexity-cli --severity=Info app/Main.hs src/ Info:app/Main.hs:SrcLoc "app/Main.hs" 1 1:module Main has 14 lines of code Info:app/Main.hs:SrcLoc "app/Main.hs" 8 1:type signature for main has type constructor nesting of 1 Info:app/Main.hs:SrcLoc "app/Main.hs" 8 1:type signature for main has 1 arguments Info:app/Main.hs:SrcLoc "app/Main.hs" 9 1:function main has 8 lines of code Info:app/Main.hs:SrcLoc "app/Main.hs" 9 1:function main has cyclomatic complexity of 1 Info:app/Main.hs:SrcLoc "app/Main.hs" 9 1:function main has branching depth of 0 </code></pre> <ul> <li>メソッドの行数</li> <li>コンストラクタのネスト数</li> <li>引数の数(+1された数字が出ている?Haskellの関数には暗黙的な引数が一つあるのかな?それか単に型一つにつき1とカウントしているのかな?)</li> <li>サイクロマティック複雑度</li> <li>Branching depth<br /> が出てきました。</li> </ul> <p>これは、Makefileなどに書いておいて、毎回ビルドの時に一緒に測定してもらうなどが良さそうですね。それかIDEでファイル保存のトリガーで実行してくれる方が良いのかな。そこで警告などが出てくれればソースコードを見直そうという気になるかもしれません。</p> reouno