Haskellの「import-hiding-instaces問題」と「newtype-instance文化」

# Haskellの「import-hiding-instaces問題」と「newtype-instance文化」

この記事の想定読者レベル

- Haskellに入門済み
- Haskellの文化には詳しくない

- - - - -

こんにちHaskell〜〜。

今日のHaskellではモジュールをimportしたときに

- そのモジュールで定義されたあらゆるインスタンスも勝手にimportされてしまい
- しかもそれを`hiding`することはできません。

そこでこの記事では、その問題の具体例と、それを解決する**newtype-instance文化**を紹介します。

## **import hiding instances問題**

現在のHaskell（GHC）では、`instance`へのimport-hidingができません。
例を見てみましょう。

```haskell
module Data.Meiwaku where

data Meiwaku = Meiwaku

-- 絶対にimportしたい！　超便利な関数。
veryVeryUseful :: Meiwaku -> Meiwaku
veryVeryUseful Meiwaku = Meiwaku

-- 絶対にimportしたくない！　突然の()インスタンス。
instance Semigroup () where
  _ <> _ = ()
```

```haskell
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を定義します。

```haskell
-- あるライブラリfooで定義されたモジュール
module Data.Foo where

data Foo = Bar | Baz
  deriving (Show, Eq)
```

次に２つの、そのSemigroupインスタンスを定義します。


```haskell
module Data.Foo.Baring where

import Data.Foo

-- Barを優先する実装
instance Semigroup Foo where
  _ <> Bar = Bar
  Bar <> _ = Bar
  _ <> _   = Baz

-- importしたい関数
veryVeryBenriBaring :: Foo -> Foo
veryVeryBenriBaring _ = Bar
```

```haskell
module Data.Foo.Bazing where

import Data.Foo

-- Bazを優先する実装
instance Semigroup Foo where
  _ <> Baz = Baz
  Baz <> _ = Baz
  _ <> _   = Bar

-- importしたい関数
veryVeryBenriBazing :: Foo -> Foo
veryVeryBenriBazing _ = Baz
```

完成！
では、動作確認をしてみましょう。

```haskell
-- 大事なところ
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を期待する
```

```haskell

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`を使って解決しましょう。

```haskell
{-# 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
```

```haskell
{-# 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セクションをご覧ください。

- [「しんさんきぼう」のDerivingストラテジー](https://aiya000.github.io/Maid/haskell-day-2019-deriving)

```haskell
-- 大事なところ
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を期待する
```

```haskell
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`の値を直接操作することができます。

```haskell
{-# 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
```

それでは実行してみましょう……。

```haskell

Main2.hs:1:14: error: Unsupported extension: ApplyingVia
  |
1 | {-# LANGUAGE ApplyingVia #-}
  |              ^^^^^^^^^^^
```

はい、すみません。
`ApplyingVia`はまだGHCに、マージされていない状態のようです。

- [-XApplyingVia — sort @(Int via Down Int) by Icelandjack - Pull Request #218 - ghc-proposals/ghc-proposals - GitHub](https://github.com/ghc-proposals/ghc-proposals/pull/218)

こうご期待。

- - - - -

本稿で出てきたコードは、下記で実行可能です。

https://github.com/aiya000/qiita-draft/tree/master/Haskell/calture-of-newtype-instances