Haskellの「import-hiding-instaces問題」と「newtype-instance文化」
この記事の想定読者レベル
- Haskellに入門済み
- Haskellの文化には詳しくない
こんにちHaskell〜〜。
今日のHaskellではモジュールをimportしたときに
- そのモジュールで定義されたあらゆるインスタンスも勝手にimportされてしまい
- しかもそれを
hiding
することはできません。
そこでこの記事では、その問題の具体例と、それを解決するnewtype-instance文化を紹介します。
import hiding instances問題
現在のHaskell(GHC)では、instance
へのimport-hidingができません。
例を見てみましょう。
module Data.Meiwaku where
data Meiwaku = Meiwaku
-- 絶対にimportしたい! 超便利な関数。
veryVeryUseful :: Meiwaku -> Meiwaku
veryVeryUseful Meiwaku = Meiwaku
-- 絶対にimportしたくない! 突然の()インスタンス。
instance Semigroup () where
_ <> _ = ()
module Main where
import Data.Meiwaku (Meiwaku(..), veryVeryUseful)
main :: IO ()
main = do
print $ veryVeryUseful Meiwaku
print $ () <> ()
これをコンパイルすると、コンパイルエラーが起きるでしょう。
instance Semigroup ()
が、Data.Semigroupで定義されたinstance Semigroup ()
と重複しているせいです。
そう。
import Data.Meiwaku (veryVeryUseful)
は、はた迷惑なinstance Semigroup ()
をもimportしてしまうのです。
しかしながらveryVeryUseful
は絶対に使いたい。
ならばimport Data.Meiwaku hiding (instance Semigroup ())
するのがよいでしょう。
でもそれは、現在のHaskellではできません!
これをこの記事では「import hiding instances問題」と呼びます。
import hiding instances問題との出会い
もう少しだけ、リアルワールド寄りな例を見てみます。
あなたは今、あるライブラリfooの作者です。
まずはその主要な機能であるデータ型Fooを定義します。
-- あるライブラリfooで定義されたモジュール
module Data.Foo where
data Foo = Bar | Baz
deriving (Show, Eq)
次に2つの、そのSemigroupインスタンスを定義します。
module Data.Foo.Baring where
import Data.Foo
-- Barを優先する実装
instance Semigroup Foo where
_ <> Bar = Bar
Bar <> _ = Bar
_ <> _ = Baz
-- importしたい関数
veryVeryBenriBaring :: Foo -> Foo
veryVeryBenriBaring _ = Bar
module Data.Foo.Bazing where
import Data.Foo
-- Bazを優先する実装
instance Semigroup Foo where
_ <> Baz = Baz
Baz <> _ = Baz
_ <> _ = Bar
-- importしたい関数
veryVeryBenriBazing :: Foo -> Foo
veryVeryBenriBazing _ = Baz
完成!
では、動作確認をしてみましょう。
-- 大事なところ
module Main where
import Data.Foo (Foo (..))
import Data.Foo.Baring (veryVeryBenriBaring)
import Data.Foo.Bazing (veryVeryBenriBazing) -- `instance Semigroup Bazing`をimport
main :: IO ()
main = do
print $ veryVeryBenriBaring Baz -- 必要な処理
print $ veryVeryBenriBazing Bar -- 必要な処理
print $ (Baz <> Bar) == Baz -- Bazを期待する
Main.hs:12:12: error:
• Overlapping instances for Semigroup Foo
arising from a use of ‘<>’
Matching instances:
instance [safe] Semigroup Foo -- Defined at Data/Foo/Bazing.hs:6:10
instance [safe] Semigroup Foo -- Defined at Data/Foo/Baring.hs:6:10
• In the first argument of ‘(==)’, namely ‘(Baz <> Bar)’
In the second argument of ‘($)’, namely ‘(Baz <> Bar) == Baz’
In a stmt of a 'do' block: print $ (Baz <> Bar) == Baz
|
12 | print $ (Baz <> Bar) == Baz -- Bazを期待する
| ^^^^^^^^^^
アイエエエエエエエ!? エラー!?? エラー ナンデ!?!?
どうやらData.Foo.Baring
及びData.Foo.Bazing
の2箇所でinstance Semigroup Foo
を定義してしまったことが問題になっているようです。
……ちゃんと選択的に、veryVeryBenriBaring
とveryVeryBenriBazing
だけimportしたはずなのに!?
そう、「import hiding instances問題」です。
newtype-instance文化を導入する
ここで礼節のあるHaskell文化にならい、newtype
を使って解決しましょう。
{-# LANGUAGE DerivingVia #-}
module Data.Foo.Baring where
import Data.Foo
newtype Baring = Baring
{ unBaring :: Foo
} deriving (Eq) via Foo -- Fooのうち必要な性質を、Bazingに抜き出す
-- Barを優先する実装
instance Semigroup Baring where
_ <> (Baring Bar) = Baring Bar
(Baring Bar) <> _ = Baring Bar
_ <> _ = Baring Baz
-- importしたい関数
veryVeryBenriBaring :: Foo -> Foo
veryVeryBenriBaring _ = Bar
{-# LANGUAGE DerivingVia #-}
module Data.Foo.Bazing where
import Data.Foo
newtype Bazing = Bazing
{ unBazing :: Foo
} deriving (Eq) via Foo -- Fooのうち必要な性質を、Bazingに抜き出す
-- Bazを優先する実装
instance Semigroup Bazing where
(Bazing Baz) <> _ = Bazing Baz
_ <> (Bazing Baz) = Bazing Baz
_ <> _ = Bazing Bar
-- importしたい関数
veryVeryBenriBazing :: Foo -> Foo
veryVeryBenriBazing _ = Baz
BaringとBazingでも、Fooの便利な性質(ここでのEq
)を使いたいので、DerivingVia
を用いています。
DerivingVia
につきましては、下記のスライドのDerivingViaセクションをご覧ください。
-- 大事なところ
module Main where
import Data.Foo (Foo (..))
import Data.Foo.Baring (veryVeryBenriBaring) -- Baringはhidingしておく
import Data.Foo.Bazing (veryVeryBenriBazing, Bazing(..))
main :: IO ()
main = do
print $ veryVeryBenriBaring Baz -- 必要な処理
print $ veryVeryBenriBazing Bar -- 必要な処理
print $ (Bazing Baz <> Bazing Bar) == Bazing Baz -- Bazを期待する
Bar
Baz
True
これで、望んだ挙動を持つinstance Semigroup Baring
・instance Semigroup Bazing
を定義・利用することができました。
かつ、必要のないBaring (..)
は、ちゃんとimportから除外されています!
このように、instanceの重複を避けるために、そのnewtypeにinstanceを定義することを、ここでは「newtype-instance文化」と呼びます。
この文化はData.Semigroup
のSum
やProduct
等で、広く使われています。
instance Num a => Semigroup (Sum a)
instance Num a => Semigroup (Product a)
「import-hiding-instaces問題」「newtype-instance文化」とは
Haskellではモジュールをimportしたときに、そのモジュールで定義されたあらゆるインスタンスも勝手にimportされてしまい、しかもそれをhiding
することはできませんでした。
それをここでは「import-hiding-instaces問題」と呼びました。
それを解決するのが「newtype-instance文化」で、具体的には、ある型Foo
に直接instanceを定義せず、そのnewtypeにinstanceを定義することでした!
おまけ - ApplyingVia
でも私達が本当に作りたかったのは、Semigroup Baring
とSemigroup Bazing
という2つの型のインスタンスじゃなくて、ただひとつの型Foo
への2つのインスタンスだったような?
そこでApplyingVia
です。
ApplyingVia
拡張を用いると、下記のように、Foo
の値を直接操作することができます。
{-# LANGUAGE ApplyingVia #-}
module Main where
import Data.Foo (Foo (..))
import Data.Foo.Baring (veryVeryBenriBaring)
import Data.Foo.Bazing (veryVeryBenriBazing, Bazing(..))
main :: IO ()
main = do
print $ veryVeryBenriBaring Baz
print $ veryVeryBenriBazing Bar
print $ (<>) @(via Bazing) Baz Bar == Baz
それでは実行してみましょう……。
Main2.hs:1:14: error: Unsupported extension: ApplyingVia
|
1 | {-# LANGUAGE ApplyingVia #-}
| ^^^^^^^^^^^
はい、すみません。
ApplyingVia
はまだGHCに、マージされていない状態のようです。
こうご期待。
本稿で出てきたコードは、下記で実行可能です。