Edited at

(GHC 8.0.1以降では単にerrorを使えばすむ話でした)Haskellで「ソースコード上での位置」を取得する


Haskellで「ソースコード上での位置」を取得する


はじめにお読みください

「エラーメッセージを表示するときに、ソースコード上での位置も出力できたら、かっこいいよね」という話でしたが、GHC 8.0.1からは、単にerror "foo"とするだけで、出力できるようです。GHC 8.0.1以降のGHCは「かっこいいよね」ということで、この話はおわりです。

TemplateHaskellの使用例として書いてみたのですが、「より良い方法がある」というか「そもそも必要ない」ということを、コメントいただきました。「限定共有」にしようかと思ったのですが、「一度公開した記事は限定公開にはできない」ということでした。削除しようかとも思ったのですが、いただいたコメントの内容が「有益」なものなので、迷っているところです。

以降の話は「とりあえずは、とっておいてある記事」とお考えください。


はじめに

ちょっとしたチップス。「エラーメッセージを表示するときに、ソースコード上での位置も出力できたら、かっこいいよね」という話です。

エラーメッセージを表示するときに「ソースコード上での位置」を出力したいことがある。モジュール名とファイルの何行目の何文字目かを表示できれば、便利だ。

言語拡張TemplateHaskellを使えば、かんたんにできる。


ソースコード

ソースコードは下記に置いてあります。

try-get-position-th


問題提起

つぎのような例を考える。


Foo.hs

module Foo where

foo :: [String] -> IO ()
foo [] = error "foo: The argument should not be empty."
foo strs = putStrLn `mapM_` strs


「Data.List.NonEmpty.NonEmptyを使えばいい」とか、「部分関数を定義するのは、いかがなものか」などの意見もあるかもしれないけれど、そういった話は置いておくことにする。これを呼び出す。


Main.hs

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につぎのように追加する。


package.yaml

dependencies:

- base >= 4.7 && < 5
- template-haskell
- ...

Foo.hsをつぎのように書きかえる。


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で、こんなこともできるよ」という話でもあり、興味を持っていただけるきっかけになれば、と。