メインコンテンツへ移動

newtype_Haskell ノート 8

無料2018-06-08#Functional_Programming#Haskell type vs newtype vs data#Haskell newtype#Haskell类型别名#Haskell ZipList

なぜ newtype が必要なのか?data、type などのキーワードとどのような違いがあるのか?

一.ZipList と List

List シーンにおいて、xs <*> ys は左側の xs から関数を取り出して右側の ys の各項に作用させることを表し、2 つの実装方式があります:

  • デカル��積

  • ジッパー式の一対一のペアリング

それぞれ []ZipList に対応します。例えば:

import Control.Applicative;

-- デカルト積
> [(+2), (*2), (/2)] <*> [1, 2, 3]
[3.0,4.0,5.0,2.0,4.0,6.0,0.5,1.0,1.5]
-- ジッパー式ペアリング
> getZipList $ ZipList [(+2), (*2)] <*> ZipList [1..]
[3,4]

デカルト積は有限長の List にのみ使用でき、ジッパー式ペアリングは無限長の List のシーンにも適用できます。<*> 而言、これら 2 つの実装はどちらも許容されますが、[] は同時に 2 つの異なる Applicative 実装を持つことができないため、ZipList を作り出し、ジッパーペアリング方式で Applicative を実装させます

P.S.ここで言及した <*>Applicative クラスが定義する動作で、詳細は [Functor と Applicative_Haskell ノート 7](/articles/functor 与 applicative-haskell 笔记 7/) を参照

二.newtype

ZipList はこのシーンのために生まれたもので、本質的には List のラッピングです。定義は以下の通り:

newtype ZipList a = ZipList { getZipList :: [a] }
              deriving ( Show, Eq, Ord, Read, Functor
                       , Foldable, Generic, Generic1)

Control.Applicative から引用)

newtype キーワードを通じて、既存のタイプ([])に基づいて新しい(ZipList)を作成し、そのインターフェース実装を書き換えます:

instance Applicative ZipList where
  pure x = ZipList (repeat x)
  liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)

P.S.ここでは liftA2 のみを実装し、<*> は現れていません。Applicative には最小完全定義(minimal complete definition)の制約があるためです:

A minimal complete definition must include implementations of pure and of either <*> or liftA2. If it defines both, then they must behave the same as their default definitions:

(<*>) = liftA2 id liftA2 f x y = f <$> x <*> y

これら 2 つの関数の関連が事前に定義されているため、どちらか 1 つを実装すれば十分です(関連関係に基づいて自動的に他方を生成できます)

では、newtype は実際には何を行ったのでしょうか?

実際には、newtype が行うことは新しいタイプを作成し、既存のタイプをラッピングするだけです

類似のシーンでは、JS の場合、このようにします:

class ThisType {
  constructor(value) {
    this.value = value;
  }
  ['<*>']() {
    console.log('デカルト積');
  }
};
class ThatType {
  constructor(...args) {
    this.originalValue = new ThisType(...args);
  }
  getOriginalType() {
    return this.originalValue;
  }
  ['<*>']() {
    console.log('ジッパーペアリング');
  }
};

// test
let thisOne = new ThisType(1);
thisOne['<*>']();     // デカルト積
console.log(thisOne); // ThisType?{value: 1}
let thatOne = new ThatType(2);
thatOne['<*>']();     // ジッパーペアリング
console.log(thatOne.getOriginalType()); // ThisType?{value: 2}

新しいタイプ(ThatType)を作成し、元のタイプ(ThisType)をラッピングし、異なる <*> 実装を提供します

両者は単純な依存関係であり、継承関係ではないため、newtype で作成されたタイプは自動的に元のタイプのすべてのメソッドを持つわけではありません(元のタイプが実装した typeclass も自動的に獲得しません)。タイプ而言、両者は完全に独立した異なるタイプです。したがって:

> [3] ++ [1, 2]
[3,1,2]
> type IntList = [Int]
> [3] ++ ([1, 2] :: IntList)
[3,1,2]
> (ZipList [3]) ++ (ZipList [1, 2])
<interactive>:109:1: error:
? Couldn't match expected type '[a]'
          with actual type 'ZipList Integer'
? In the first argument of '(++)', namely 'ZipList [3]'
...

type で作成された別名タイプ は元のタイプと等価に交換使用できますが、newtype で作成された新タイプ は元のタイプとは完全に異なるもので、唯一のつながりは新タイプ内部で実際に操作するのは元のタイプ(元のタイプインスタンスの参照を保持することを通じて)であり、この方式を通じて外層で元のタイプの拡張/強化を実現します

構文要求

構文作用から見ると、newtypedata と同じく、すべて新しいタイプを作成するために使用されますが、newtype の方が制限が多いです:

data can only be replaced with newtype if the type has exactly one constructor with exactly one field inside it.

newtype 宣言のタイプは値コンストラクタが 1 つだけであり、かつこの値コンストラクタはパラメータが 1 つだけ でなければなりません(field)。これ之外、data キーワードとはほとんど違いがありません

P.S.値コンストラクタとパラメータについては、[タイプ_Haskell ノート 3](/articles/类型-haskell 笔记 3/#articleHeader7) を参照

三.type と data の比較

キーワード作用応用シーン
data自分の(データ)タイプを定義完全に新しいタイプを定義したい場合
type既存のタイプに別名を付け、得られたものは元のタイプと完全に等価で、無条件に交換/混用可能タイプシグネチャをより明確に(意味的に)したい場合
newtype既存のタイプを新しいタイプにラッピングし、得られたタイプは元のタイプと異なり、交換/混用不可既存のタイプに異なるインターフェース(typeclass)実装を持たせたい場合

四.newtype と遅延計算

Haskell ではほとんどの計算は遅延的です(少数は foldl'Data.ByteString などの厳格版を指します)。つまり、計算は不得不算の時にのみ発生します

遅延計算は一般に直感的に見えます(計算する必要がないものはまず計算しない)が、特殊なのは、タイプ関連のシーンには暗黙の計算が存在することです(あまり直感的ではありません)

undefined

undefined はエラーを引き起こす計算を表します:

> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:12:1 in interactive:Ghci1

遅延性を検査 するために使用できます(計算が実際に実行されたかどうか)。例えば:

> head [1, undefined, 3, undefined, undefined]
1
> let (a, _) = (1, undefined) in a + 1
2

特殊に、関数呼び出し時のパターンマッチ自体は計算が必要です。マッチ結果が使用されるかどうかに関わらず、例えば:

sayHello (_, _) = "hoho"
> sayHello undefined
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:37:10 in interactive:Ghci17

そして以下の形式のものは計算されません:

sayHello _ = "hoho"
> sayHello undefined
"hoho"

両者の違いは、前者に対しては、Tuple のどの値コンストラクタを使用すべきかを見るために基本的な計算を行う必要があるのに対し、後者は不要であることです

しかし奇妙なのは、Tuple には明らかに値コンストラクタが 1 つしかない(「Tuple のどの値コンストラクタを使用すべきかを見る」必要がない)ことです:

data () = ()

Tuple のどの値コンストラクタを使用すべきかを確認する必要がないことは我们知道っていますが、Haskell は知りません。なぜなら、約束に従って、data キーワードで定義されたデータタイプは複数の値コンストラクタを持つことができ、1 つしか宣言していなくても、それを探して知る必要があるからです。では、何を思いつきましたか?

newtype です。値コンストラクタが 1 つだけ(かつこの値コンストラクタはパラメータが 1 つだけ)であることを明確に約束しています。試してみましょう:

newtype MyTuple a b = MyTuple {getTuple :: (a, b)}
> sayHello (MyTuple _) = "hh"
> sayHello undefined
"hh"

確かにその通りで、Haskell は十分に賢く、複数の値コンストラクタが存在しないことが明確な場合、無意味な計算を行いません

参考資料

コメント

コメントはまだありません

コメントを書く