top page > computer > haskell > web_lecture > for_programmer > adt_export.html
更新日:
文責: 重城良国

構文: 代数的データ型のエクスポート

はじめに

自分で作った代数的データ型をエクスポートすることを考える。何をエクスポートするかによって値の保護が可能となる。

すべてを公開する

エクスポートリストに型名(..)のように記述するとその型の持つ値構築子のすべてが公開される。それぞれの値構築子を明示することもでき...

Wake up, Neo ...
The Matrix has you ...
Follow the white rabbit

Knock, knock, Neo.

Morpheus: This is your last chance. After this, there is no turning back. You take the blue pill -- the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill -- you stay in Wonderland, and I show you how deep the rabbit hole goes. Remember: all I'm offering is the truth. Nothing more.

(モーフィアス: これがお前の最後のチャンスだ。後もどりできるのはここまでだ。青い薬を選べば物語は終わる。いつものベッドで目覚めるだろう。信じたいものを信じ続ければいい。赤い薬を選べば『夢の国』だ。うさぎの穴がどれだけ深いかを教えよう。すべて本当のこと、それだけだ。)

module Morpheus (Pill(..), select) where

data Pill = Blue | Red deriving Show

select :: Pill -> String
select Blue = "The story ends."
select Red = "You stay in Wonderland."

% ghci Morpheus.hs
*Morpheus> :m Morpheus
Prelude Morpheus> select Blue
"The story ends."
Prelude Morpheus> select Red
"You stay in Wonderland."

明示的にエクスポートしても同じだ。

module Morpheus (Pill(Blue, Red), select) where

もっと強引なモーフィアスであれば

module Morpheus (Pill(Red), select) where

となる。

Prelude Morpheus> :reload
*Morpheus Morpheus> :m Morpheus
Prelude Morpheus> select Blue

<interactive>X:Y: Not in scope: data constructor Blue
Prelude Morpheus> select Red
"You stay in Wonderland"

値構築子Blueは決して使えない。「夢の国」から今までの生活にはもどれない。

値構築子を公開しない

Morpheusモジュールで値構築子Blueを公開しないのは実際には意味がない。単にdata Pill = Redとすれば良い。値構築子を公開しない場合には何らかの方法でその値構築子を使った値を公開することになる。値そのものを用意するか、あるいは値を返す関数を公開するかだ。

用意した値以外使わせない

2つの整数の組を、互いに素な整数のべき乗で表現する。互いに素な整数として2, 3を使う例だ。

tuple23.hs

cons :: Integer -> Integer -> Integer
cons x y = 2 ^ x * 3 ^ y

unpower :: Ingeger -> Integer -> Integer
unpower n x
| x `mod` n /= 0 = 0
| otherwise = 1 + unpower n (x `div` n)

uncons :: Integer -> (Integer, Integer)
uncons xy = (unpower 2 xy, unpower 3 xy)

% ghci tuple23.hs
*Main> cons 5 7
69984
*Main> uncons it
(5,7)

2, 3以外のペアを使いたい。モジュール側でいくつかのペアを用意する。それ以外は使えないようにする。

TupleAB.hs

module TupleAB (cons, uncons, Pair, p1, p2, p3, p4, p5) where

data Pair = Pair Integer Integer deriving Show

p1, p2, p3, p4, p5 :: Pair
p1 = Pair 2 3
p2 = Pair 5 7
p3 = Pair 4 9
p4 = Pair 10 21
p5 = Pair 15 16

cons :: Pair -> Integer -> Integer -> Integer
cons (Pair a b) x y = a ^ x * b ^ y

unpower :: Integer -> Integer -> Integer
unpower n x
| x `mod` n /= 0 = 0
| otherwise = 1 + unpower n (x `div` n)

uncons :: Pair -> Intger -> (Integer, Integer)
uncons (Pair a b) xy = (unpower a xy, unpower b xy)

*Main> :load TupleAB.hs
*TupleAB> :m TupleAB
Prelude TupleAB> cons p1 5 7
69984
Prelude TupleAB> uncons p1 it
(5,7)
Prelude TupleAB> cons p3 5 7
4897760256
Prelude TupleAB> uncons p3 it
(5,7)
Prelude TupleAB> cons p5 5 7
203843174400000
Prelude TupleAB> uncons p5 it
(5,7)

型名Pairだけを公開して値構築子Pairを隠蔽している。p1からp5までの5個の値以外のPair型の値は作れない。たとえば2と4のペアなどの不正な値は使えない。

特定の条件を満たす値以外使わせない

関数の実行前に引数が特定の条件を満たしていることを保証したいことがある。リストがソート済みでなければならない、などだ。ここでは例として偶数であることを保証する型を考える。

Even.hs

型Even

data Even = Even Integer deriving Show

値構築子をそのまま作えばEven 5とかEven 7とかの値もできてしまうので値を作る関数を別に作っておく。

toEven :: Integer -> Maybe Even
toEven n | even n = Just $ Even n | otherwise = Nothing

この関数は奇数を与えるとNothingを返す。Even型の値から整数値をとりだす関数も作る。

fromEven :: Even -> Integer
fromEven (Even n) = n

整数値を2倍することでEven型の値を作成する関数も作っておく。

double :: Integer -> Even
double n = Even $ 2 * n

% ghci Even.hs
*Main>

モジュール名をまだ指定していないのでMainモジュールとなる。

*Main> toEven 8
Just (Even 8)
*Main> toEven 7
Nothing
*Main> let Just e = toEven 8
*Main> e
Even 8
*Main> fromEven e
8
*Main> double 3
Even 6

公開する予定の関数のみを作えばEven型の値のなかには偶数しかありえないことがわかる。

*Main> Even 7
Even 7

値構築子を直接作うと不正な値が作れてしまう。

モジュール

モジュール名を指定して公開する識別子を明示する。先頭に

module Even (Even, toEven, fromEven, double) where

をつける。識別子Evenのあとに(..)がない。これは型だけ公開し値構築子を公開しないということだ。

% ghci Even.hs
*Even> Even 7
Even 7

この状態ではまだモジュールEvenのなかなので値構築子は使えてしまう。

*Even> :m Even
Prelude Even>

このようにモジュールEvenを明示的にインポートするとモジュールEvenのそとからモジュールEvenをインポートした状態になる。明示的に公開した識別子だけ使える。

Prelude Even> Even 7

<interactive>:X:Y: Not in scope: data constructor `Even'

その他の道具

Even型の値を扱ういくつかの関数を定義する。偶数同士の足し算や引き算では結果も偶数となる。

add :: Even -> Even -> Even
Even n1 `add` Even n2 = Even $ n1 + n2

引き算を定義する前に符号を反転する関数を定義する。

neg :: Even -> Even
neg (Even n) = Even $ - n

sub :: Even -> Even -> Even
e1 `sub` e2 = e1 `add` neg e2

偶数値に整数値をかければ結果は偶数となる。

mul :: Even -> Integer -> Even
Even n1 `mul` n2 = Even $ n1 * n2

偶数値を2で割った場合結果は偶数または奇数となる。

div2 :: Even -> Either Integer Even
div2 (Even n)
| odd n' = Left n'
| otherwise = Right $ Even n'
where n' = n `div` 2

これらの関数も公開する。

出来上がり

module Even (Even, toEven, fromEven, double, add, sub, mul, neg, div2) where

data Even = Even Integer deriving Show

toEven :: Integer -> Maybe Even
toEven n | even n = Just $ Even n | otherwise = Nothing

fromEven :: Even -> Integer
fromEven (Even n) = n

double :: Integer -> Even
double n = Even $ 2 * n

add :: Even -> Even -> Even
Even n1 `add` Even n2 = Even $ n1 + n2

neg :: Even -> Even
neg (Even n) = Even $ - n

sub :: Even -> Even -> Even
e1 `sub` e2 = e1 `add` neg e2

mul :: Even -> Integer -> Even
Even n1 `mul` n2 = Even $ n1 * n2

div2 :: Even -> Either Integer Even
div2 (Even n)
| odd n' = Left n'
| otherwise = Right $ Even n'
where n' = n `div` 2

試してみる

Prelude Even> :reload
*Even Even> :m Even
Prelude Even> let Just e1 = toEven 8
Prelude Even> let e2 = double 5
Prelude Even> e1 `add` e2
Even 18
Prelude Even> e1 `sub` e2
Even (-2)
Prelude Even> e1 `mul` 3
Even 24
Prelude Even> div2 e1
Right (Even 4)
Prelude Even> div2 e2
Left 5

使ってみる

'a'を偶数個くりかえした文字列を作るには

evenAs :: Even -> String
evenAs e = replicate (fromIntegral $ fromEven e) 'a'

:m - モジュール名でインポートしたモジュールを消すことができる。

Prelude Even> :m - Even
Prelude> :load useEven.hs
*Main> let Just e = toEven 8
*Main> evenAs e
"aaaaaaaa"
*Main> evenAs $ double 3
"aaaaaa"

この関数でできる文字列の長さは必ず偶数となる。

まとめ

モジュールを作成すると公開する識別子を制限できる。引数が特定の条件を満たすことを保証できる。

課題

  1. モジュールAnpanmanを作成し型Friendを定義せよ
    • 型Friendは値構築子Friendをもつ
    • 値構築子Friendは文字列を引数としてもつ
    • deriving Showをしておく
  2. 値ai, yuukiを定義せよ
    • 値aiは文字列"Ai"をもつFriend型の値
    • 値yuukiは文字列"Yuuki"をもつFriend型の値
  3. エクスポートリストを明示して型Friend、値ai、yuukiだけを指定せよ
    • 値構築子Friendは公開しない
  4. 対話環境に読みこめ
    • まずはモジュール内ではaiとyuuki以外のFriendも作れることを確認する
    • 明示的に:m Anpanmanとしてモジュールの外に出よ
    • aiとyuukiだけがFriendであることを確認せよ

「構文: モジュールの作成」へもどる 「確認のための演習: NML(1)」へ

正当なCSSです! HTML5 Powered with CSS3 / styling, and Semantics