3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-12

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

3
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0