Haskellで「ソースコード上での位置」を取得する
はじめにお読みください
「エラーメッセージを表示するときに、ソースコード上での位置も出力できたら、かっこいいよね」という話でしたが、GHC 8.0.1からは、単にerror "foo"とするだけで、出力できるようです。GHC 8.0.1以降のGHCは「かっこいいよね」ということで、この話はおわりです。
TemplateHaskellの使用例として書いてみたのですが、「より良い方法がある」というか「そもそも必要ない」ということを、コメントいただきました。「限定共有」にしようかと思ったのですが、「一度公開した記事は限定公開にはできない」ということでした。削除しようかとも思ったのですが、いただいたコメントの内容が「有益」なものなので、迷っているところです。
以降の話は「とりあえずは、とっておいてある記事」とお考えください。
はじめに
ちょっとしたチップス。「エラーメッセージを表示するときに、ソースコード上での位置も出力できたら、かっこいいよね」という話です。
エラーメッセージを表示するときに「ソースコード上での位置」を出力したいことがある。モジュール名とファイルの何行目の何文字目かを表示できれば、便利だ。
言語拡張TemplateHaskellを使えば、かんたんにできる。
ソースコード
ソースコードは下記に置いてあります。
問題提起
つぎのような例を考える。
module Foo where
foo :: [String] -> IO ()
foo [] = error "foo: The argument should not be empty."
foo strs = putStrLn `mapM_` strs
「Data.List.NonEmpty.NonEmptyを使えばいい」とか、「部分関数を定義するのは、いかがなものか」などの意見もあるかもしれないけれど、そういった話は置いておくことにする。これを呼び出す。
module Main where
import Foo
main :: IO ()
main = foo []
コンパイル・実行するとつぎのようになる。
try-get-position-th-exe: foo: The argument should not be empty.
CallStack (from HasCallStack):
error, ...
たとえば、(「そんなコードを書くな」という話かもしれないが)モジュールFooが数千行あって、fooの定義がつぎのようになっていたとする。
foo :: Bar -> Baz -> ...
foo ... = error ...
foo ... = error ...
.
.
.
foo ... = error ...
.
.
.
「どの行のエラーを見ればいいの?」となる。そんなとき、エラーメッセージに「ソースコード上での位置」が表示されていると親切だ。
ソースコード上での位置を出力すればいい
こんなとき、エラーメッセージに「かんたんに」ソースコード上での位置を表示できる。言語拡張TemplateHaskellを使う。
パッケージtemplate-haskellを使用するので、Stackを使っているなら、packate.yamlにつぎのように追加する。
dependencies:
- base >= 4.7 && < 5
- template-haskell
- ...
Foo.hsをつぎのように書きかえる。
{-# LANGUAGE TemplateHaskell #-}
module Foo where
import Language.Haskell.TH
foo :: [String] -> IO ()
foo [] = error $
"\n" ++ $(litE =<< stringL . pprint <$> location) ++
"\nfoo: The argument should not be empty."
foo strs = putStrLn `mapM_` strs
言語拡張を使用することをコンパイルにつたえる。ここではプラグマを使用する。先頭の{-# LANGUAGE TemplateHaskell #-}
だ。モジュールLanguage.Haskell.THを導入する(import Language.Haskell.TH
)。$(litE =<< stringL . pprint <$> location)
が「ソースコード上での位置」を取得している部分だ。
Main.hsはそのままでいい。コンパイル・実行すると、つぎのようになる。
try-get-position-th-exe:
try-get-position-th-0.1.0.0-...:Foo:(10,19)-(10,57)
foo: The argument should not be empty.
CallStack (from HasCallStack):
error, called at ...
「モジュールFooの10行19文字目から10行57文字目まで」を見れば、エラーがどこで起きているかがわかる。
位置を取得している部分の説明
TemplateHaskellではIOモナドと似ているQモナドというものが使われている。位置を取得している部分をdo記法で書くと、つぎのようになる。
getPos = do
loc <- location
sl <- stringL (pprint loc)
litE sl
locationで位置を表すデータ構造を取り出し、それをpprintによって「人間が読みやすい文字列」に変換。stringLで「文字列リテラル」に変換してから、litEで、その文字列リテラルを「値」に変換している。
まとめ
「エラーメッセージを出力するとき、ソースコード上での位置も表示できたら、かっこいいよね」という話。ちょっとしたチップスですが、「言語拡張TemplateHaskellで、こんなこともできるよ」という話でもあり、興味を持っていただけるきっかけになれば、と。