OverloadedStringsとANNプラグマが干渉する場合の回避方法

  • 4
    いいね
  • 0
    コメント

tl;dr

プラグマ内部での文字列リテラルを ::String と型指定すればよい
{-# ANN module ("HLint: ignore Use camelCase" :: String) -#}

これに出くわした経緯

新しいプロジェクトで、テストスイートに tasty を使い、テストコードのディスカバリには tasty-discover を導入しようとしました。

tasty-discover

tasty-discover は ファイル名が *Test.hs あるいは *Spec.hs とサフィックスのついたモノをテスト対象のモジュールと判別し、その中で宣言されている prop_ spec_ などのプレフィックスのついた関数をテストスイートに自動的に追加してくれるというものです。
- prop_ QuickCheck でテストしてくれる
- scprop_ SmallCheck でテストしてくれる
- unit_ HUnit でテスト
- spec_ Hspec
- test_ Tastyでこれら上記のテストをまとめる等

cabal ファイルのテストのビルド設定で main-is に指定しているエントリーポイントには

Tasty.hs
{-#
 OPTIONS_GHC -F -pgmF tasty-discover
 -optF --debug --moduleSuffix=Spec
#-}

とか書いておくだけで、あとはテストファイルを追加していけば良いっていう感じになります

HLint の ANN プラグマ指定

ここで、 prop_ とかのスネークケースな命名をすると、当然 HLint は「キャメルケース使え」って怒ってくるのですが、それを回避する方法として、モジュール内にGHCの ANN プラグマを指定する方法があります。 ANNAnnotation の意らしいです。

Main.hs
module Main where

import System.IO
--
--
{-# ANN module "HLint: ignore Use camelCase" #-}
hoge_huga :: a -- 怒られなくなる

なお、注意事項としては、 ANNプラグマは import の後に書くようにしないとコンパイルが通りません。 気をつけよう

このように特定のモジュールや関数をLintの検査対象から外す使い方もあれば、逆に特定の対象にだけより強い厳格なルールを付与したりすることもできます。みんな使おうね

Megaparsec でTextをパースした結果をテストしたい→コンパイルできない→「!?」

で、 Megaparsec を利用して、Textをパースした結果のテストを書こうとして、このようにテストを書き始めたら、そもそもコンパイルができなくなってしまいました

LexerSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module LexerSpec where

import Test.Tasty.Hspec

import Lexer
import Parser

{-# ANN module "HLint: ignore Use camelCase #-}
LexerSpec.hs:__:__: error:
    • Ambiguous type variable ‘t0’ arising from the literal ‘"HLint: ignore Use camelCase"’
      prevents the constraint ‘(Data.String.IsString
                                  t0)’ from being solved.
      Probable fix: use a type annotation to specify what ‘t0’ should be.
      These potential instances exist:
        instance a ~ Char => Data.String.IsString [a]
          -- Defined in ‘Data.String’
        ...plus 10 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In the annotation:
        {-# ANN module "HLint: ignore Use camelCase" #-}

技術的解説

調べたところ、ちょうど2016年の頭にチケットがGHCに寄せられていました(修正の必要なしとしてクローズされています)

OverloadedStrings の脱糖

↑ に書いてある通りですが、 OverloadedStrings は文字列リテラルを糖衣構文とみなし、以下のような脱糖をします

"hello" -- このままだとただの [Char]

(fromString ("hello" :: String)) -- この時の型は IsString a

なので、件のプラグマのコンパイルエラーが Ambiguous type variable で始まっていたんですね。

{-# LANGUAGE OverloadedStrings #-}
-- ...
{-# ANN module ("HLint: ignore Use camelCase" :: String) -#}

こう書いた場合は

{-# ANN module ((fromString ("HLint: ignore Use camelCase" :: String)) :: String) -#}

たぶんこんな感じに展開されるのですが、冗長でも無事に型が特定できてコンパイルできるようになります。