Help us understand the problem. What is going on with this article?

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ができません。
例を見てみましょう。

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を定義してしまったことが問題になっているようです。
……ちゃんと選択的に、veryVeryBenriBaringveryVeryBenriBazingだけ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 Baringinstance Semigroup Bazingを定義・利用することができました。
かつ、必要のないBaring (..)は、ちゃんとimportから除外されています!

このように、instanceの重複を避けるために、そのnewtypeにinstanceを定義することを、ここでは「newtype-instance文化」と呼びます。

この文化はData.SemigroupSumProduct等で、広く使われています。

  • 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 BaringSemigroup 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に、マージされていない状態のようです。

こうご期待。


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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away