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に、マージされていない状態のようです。
こうご期待。
本稿で出てきたコードは、下記で実行可能です。