モノイドと継続渡しの道具箱

関数型言語Haskellにおいて、普通は計算の結果は関数の戻り値として扱うが、「結果を受け取る関数」 に渡すという継続渡しというスタイルもある。これは単なる冗長なやり方ではなく、様々な興味深い性質を持つ。

基本形は、aという値を渡すところを

∀r. (a -> r) -> r

のような表現にする。たとえば、与えられた数の42倍を渡したいとき、そのまま\x -> x * 42ではなく、\x f -> f (x * 42)と書く。もちろんこれだけではありがたみが分からない。

さて、与えられた文字列の中のうち、大文字のアルファベットを取り出し、それがアルファベットの何番目か計算するプログラムを作りたい。普通はリストを使ってこのように書くかもしれない。

import Data.Char

uppers :: [Char] -> [Int]
uppers [] = []
uppers (x:xs)
   | isUpper x = fromEnum x - fromEnum 'A' : uppers xs
   | otherwise = uppers xs

継続渡しにすると([Int] -> r) -> rという形にもできるが、あえて(Int -> r) -> rのようにIntを渡したい場合はどうなるだろうか?すると、rのために新たな演算が必要になる。

uppers :: (Int -> r) -> [Char] -> r
uppers f [] = (空っぽ)
uppers f (x:xs)
   | isUpper x = (がっちゃんこ) (f (fromEnum x - fromEnum 'A')) (uppers f xs)
   | otherwise = uppers f xs

ここで(空っぽ)(がっちゃんこ)の型に着目しよう。

(空っぽ) :: r (がっちゃんこ) :: r -> r -> r

つまり、rは空の値と、結合する演算を持つような型であることがわかる。これはモノイドと呼ばれる代数的構造である。HaskellではMonoid型クラスとして提供されている。

class Monoid a where
    mempty :: a
    (<>) :: a -> a -> a -- 実際は(<>) = mappendとして定義されている

Monoid rの制約をつけ、空っぽとがっちゃんこはそれぞれmempty(<>)で置き換えてやれば望みのプログラムは作れる。

uppers :: Monoid r => (Int -> r) -> [Char] -> r
uppers f [] = mempty
uppers f (x:xs)
   | isUpper x = f (fromEnum x - fromEnum 'A') <> uppers f xs
   | otherwise = uppers f xs

大文字をカウントするには、要素を数えるような振る舞いを持つモノイドを作ればよい。

data Count = Count { getCount :: Int }

instance Monoid Count where
    mempty = Count 0
    Count m <> Count n = Count (m + n)

single :: a -> Count
single _ = Count 1

実際にはSumというモノイドが定義されており、Countと同様、足し算がなすモノイドとして働く。

upperssingle関数を渡すとCountが返ってくる。その中身は数え立てほやほやの大文字の個数だ。

getCount . uppers single :: [Char] -> Int

uppersは、リスト以外の構造に対しても考えられる一方、要素の数え上げを実現するCountは、uppersの扱う対象の構造には関与しない。その関心の分離に、このスタイルのパワーが表れている。

uppers :: Monoid r => (Char -> r) -> Map String Char -> r
uppers :: Monoid r => (Char -> r) -> Maybe Char -> r
uppers :: Monoid r => (Char -> r) -> Seq Char -> r

標準ライブラリでは、uppersのような操作を抽象化するFoldableというクラスが定義されている。 foldMapは、コンテナf aの要素をすべて与えられた関数に渡すことが期待されている(期待されているというのは、一部だけ渡しても、あるいはまったく渡さなくても正当なインスタンスたりうる)。

class Foldable f where
    foldMap :: Monoid r => (a -> r) -> f a -> r

なお、大文字のみをフィルターする機能は独立して定義することが可能だ。

filtering :: Monoid r => (a -> Bool) -> (a -> r) -> a -> r
filtering p f a
    | p a = f a
    | otherwise = mempty

また、要素のマッピングも独立した関数として定義できる。こちらは単なる関数合成だ。

maps :: (a -> b) -> (b -> r) -> a -> r
maps f g = g . f

foldMapfilteringmapsを組み合わせれば、uppersは以下のように書ける。上から下に処理の流れが表現されているのがわかるだろうか。

uppers :: (Foldable f, Monoid r) => (Int -> r) -> f Char -> r
uppers = foldMap
  . filtering isUpper
  . maps (subtract (fromEnum 'A') . fromEnum)

このような継続とモノイドを用いた畳み込みの仕組みは、高い柔軟性と美しい合成を提供する。 しかし、これでは不足する場合がある。というのも、foldMapは元の構造を忘れてしまうので、構造を保ったまま要素を書き換えたりする目的には使えない。たとえば、シーザー暗号を実装したい場合、今までのuppersでは元のリストを失ってしまうため実現できない。

元のuppersの定義に戻ってみよう。(空っぽ)(がっちゃんこ)に元の構造を取り戻すヒントを教えてやれば、どうにかうまくやれそうだ。

uppers' :: (Int -> (Intを保つ何か)) -> [Char] -> ([Char]を保つ何か)
uppers' f [] = (空っぽ) []
uppers' f (x:xs)
   | isUpper x = (がっちゃんこ) (:) x' (uppers' f xs)
   | otherwise = (がっちゃんこ) (:) ((空っぽ) x) (uppers' f xs)
   where
       x' = (ごにょ) (\n -> toEnum $ n + fromEnum 'A') (f (fromEnum x - fromEnum 'A'))

ここでの(空っぽ)、(ごにょ)、(がっちゃんこ)はそれぞれ以下のような型を持つはずだ。

(空っぽ) :: s -> (sを保つ何か)
(ごにょ) :: (a -> b) -> (aを保つ何か) -> (bを保つ何か)
(がっちゃんこ) :: (a -> s -> (sを保つ何か)) -> (aを保つ何か) -> (sを保つ何か) -> (sを保つ何か)

(xを保つ何か)というのは型パラメータxを持つ型で表せる。ここではfとしよう。

(空っぽ) :: s -> f s
(ごにょ) :: (a -> s) -> f a -> f s
(がっちゃんこ) :: (a -> s -> f s) -> f a -> f s -> f s

これらの操作ができるようなfにはApplicativeという名前がついている。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

instance Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

liftA2 :: Applicative f => (a -> b -> f c) -> f a -> f b -> f c
liftA2 f a b = fmap f a <*> b

(ごにょ)fmap(空っぽ)(がっちゃんこ)がそれぞれpureliftA2である。すると、uppers'はこう書ける。

import Control.Applicative

uppers' :: Applicative f => (Int -> f Int) -> [Char] -> f [Char]
uppers' f [] = pure []
uppers' f (x:xs)
   | isUpper x = liftA2 (:) x' (uppers' f xs)
   | otherwise = fmap (x:) (uppers' f xs)
   where
       x' = fmap (\n -> toEnum $ n + fromEnum 'A') (f (fromEnum x - fromEnum 'A'))

Applicativeは「元の構造を保てる」モノイドになっている。もしシーザー暗号を実装する場合、結果として[Char]そのものが欲しい。 そんな時は、元の構造をそのまま包むIdentityが使える。

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
    fmap f (Identity a) = Identity (f a)

instance Applicative Identity where
    pure = Identity
    Identity f <*> Identity a = Identity (f a)

シーザー暗号は以下のように実装できる。Identityは操作をそのまま中身に伝えるので、純粋な結果が得られる。

caesar :: Int -> [Char] -> [Char]
caesar k = runIdentity . uppers' (Identity . (`mod`26) . (+k))

いちいちIdentityrunIdentityを書くのは骨なので、以下のような関数で共通化すると便利だ。

purely :: ((a -> Identity a) -> s -> Identity s) -> (a -> a) -> s -> s
purely t f = runIdentity . t (Identity . f)

各要素に対してアクションを走らせたい場合は、それをそのまま渡すだけだ。一番簡単なパターンかもしれない。

printUppers :: [Char] -> IO [Char]
printUppers = uppers' (\x -> print x >> return x)

もし今までと同じように元の構造を捨て、モノイドにしたいときは、そのようなふるまいを持つApplicativeが使える。

newtype Const r a = Const { getConst :: r }

instance Functor (Const r) where
    fmap _ (Const r) = Const r

instance Monoid r => Applicative (Const r) where
    pure _ = Const mempty
    Const a <*> Const b = Const (a <> b)

uppers'ではfmappureに元の構造のための操作が渡されていたが、Constはそれらを捨てており、代わりにモノイドの演算をしていることがわかる。以下のsmashは、Constを用いて「格下げ」を行う関数で、uppers = smash uppers'の関係が成り立つ。

smash :: ((a -> Const r a) -> s -> Const r s) -> (a -> r) -> s -> r
smash t f = getConst . t (Const . f)

この強化されたuppers'だが、こちらも共通化のためのクラスが提供されており、こちらはTraversableと呼ばれている。traversefoldMapの上位版に位置する。

class (Functor t, Foldable t) => Traversable t where
    traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

フィルターは今までとほぼ同じような形で、独立して定義できる。

filtered :: Applicative f => (a -> Bool) -> (a -> f a) -> a -> f a
filtered p f a
    | p a = f a
    | otherwise = pure a

要素へのマッピングをするには、元の構造に戻すための関数が追加で必要となる。

isomorphic :: Functor f => (s -> a) -> (a -> s) -> (a -> f a) -> s -> f s
isomorphic f g c = fmap g . c . f

traversefilteredisomorphicを使うとuppers'はこう書ける。IntCharに戻す関数が必要になったのを除けば、Foldable版とほぼ変わらない。

uppers' :: (Applicative f, Traversable t) => (Int -> f Int) -> t Char -> f (t Char)
uppers' = traverse
    . filtered isUpper
    . isomorphic (subtract (fromEnum 'A') . fromEnum) (\n -> toEnum $ n + fromEnum 'A')

このように、foldMaptraverseに代表されるようなこの手の関数は、コンテナの処理に対して素晴らしい表現力を持つ。 ならばこれを最大限に活用しようと作られたのがlensパッケージだ。

uppers'のような関数はtraversalと呼ばれ、lensパッケージでは型シノニムが定義されている。

type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s

たとえば、コンテナの特定の要素を指すtraversalなどを提供している。

ix :: Ixed m => Index m -> Traversal' m (IxValue m)
-- ix :: k -> Traversal' (Map k a) a
-- ix :: Int -> Traversal' (Vector a) a

また、ここで紹介したpurelysmashisomorphicに相当するものだけでなく、traversalを構築および使用する手段が豊富に提供されている。

lensが扱っていない範囲でも、この考え方はプログラミングに役に立つ。計算結果をリストか何かで返そうと思ったとき、是非このスタイルも思い出してみてほしい。

出、出~~wwwww銀行員待行列解説奴~wwwwwww

銀行員待行列(Banker's deque)、二つのリストで構成奴~~wwwww

入奴と出奴~wwwwwwwww

          ↓入奴

三(^o^)ノ [(^o^)ノ, (^o^)ノ, (^o^)ノ]

ヽ(^o^)三 [ヽ(^o^), ヽ(^o^), ヽ(^o^)]

          ↑出奴

追加は入奴にcons、取り出しは出奴にuncons奴~wwwリストなので基本定数時間奴~wwwwww

リスト枯渇防止の為、リストの長さに以下の条件課奴~~~wwwwww

length (入奴) <= length (出奴) * 3 + 1
length (出奴) <= length (入奴) * 3 + 1

条件充足不能場合、|length (入奴) - length (出奴)| <= 1なるよう余剰分反転後短い側の末尾に結合して調整奴~wwwww時間計算量O(length (入奴) + length (出奴))必要~~~~wwwwww

動作奴~wwww個数nのリストを[n]表記奴~wwwwwww

入、入~~wwwww調整奴~~wwwwww

[1][1]

入、入、入~wwwww調整不要~wwwwwwww

[4][1]

入~~wwwwwここで調整奴~~wwwwwww

[5][1] → [3][3]

入、入、入、入、入入入入~wwwwww再度調整奴~wwwwww

[11][3] → [7][7]

入入入入入入入入入入入入入入入入wwwwwwww調整必要奴~wwwwwww

[23][7] → [15][15]

入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入~~wwwwwww調整奴~wwwwwww

[47][15] → [31][31]

入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入入~~wwwwwwwやっと調整奴~wwwwwww

[95][31] → [63][63]

調整間隔指数関数的増大奴~wwwwwwwww

相対的調整コスト、限りなく0に漸近奴~~~wwwww

\displaystyle
\sum_{k=0}^{\infty} \frac{1}{a^k} (a \gt 1)
収束故、償却定数時間奴~~~~wwwwwwwww

取り出しも同様奴~wwwwwww

出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出~~~~wwwwwww

[63][20] → [41][40]

出出出出出出出出出出出出出出出出出出出出出出出出出出出出出~wwwwww

[40][12] → [26][26]

長ければ長いほど調整コスト相殺奴~~wwwwwwwwww

実装簡単、効率的データ構造奴~~~wwww

参考文献奴~wwwww

  • Chris Okasaki (1999). Purely Functional Data Structures

ぼくのかんがえた最強の拡張可能レコード

動機

GHCに、OverloadedRecordFields(ORF)という拡張の導入が提案されている。

(Records/OverloadedRecordFields/Design – GHCより) Haskellのレコードの深刻な欠点は、フィールドをオーバーロードできないことだ。例えば、

data Person  = Person  { personId :: Int, name :: String }
data Address = Address { personId :: Int, address :: String }

と定義したとき、personIdはどちらを参照すべきかわからない。よくある対策としてデータ型に接頭辞をつけるという方法があるが、コードやフィールド間の関係が不明瞭になる。モジュール単位で修飾するという方法もあるが、レコードごとにモジュールを作るのは非実用的だ。

そこで、personIdをその型によって解決するような、多相なフィールドの写像を扱いたい。r { x :: t }で、rxという名前の、型tのフィールドを持つことを表す、新しい形の制約の記法が必要となる。そうすれば、以下のような記述が可能になる。

getPersonId :: r { personId :: Int } => r -> Int
getPersonId e = personId e

問題点

最も大きな問題は、フィールドがファーストクラスでないことである。 lensなどのライブラリは、フィールドをデータとして表すことにより、合成などの演算を可能にしている。しかし、ORFのフィールドの概念は制約としてのみ現れる。残念ながら、Haskellは制約レベルのプログラミングは得意ではないため、非常に扱いにくいのだ。

解決

ならばORFに代わる最強の多相レコードを作ってやろう、と私は立ち上がった。仕様は以下の通りだ。

  • フィールドは値であり、フィールド名と、それが指し示す対象を型によって決めることができる。
  • レコードの型は、フィールド名の集まりによって決まる。
  • フィールドはLensとして扱える(重要)。
  • レコードの構築にフィールドを使える。必要なフィールドが欠けている場合、型エラーになる。

まず、フィールド名を型に対応させるために型族を用いる。

type family FieldValue (s :: Symbol) :: *

Symbol型レベル文字列の型(DataKinds拡張を使う)で、*は値の型である。 type instance FieldValue "foo" = Barと定義すれば、名前"foo"に型Barを対応させることができる。 そして、フィールドの型を以下のように定義する。

data Field (s :: Symbol) = Field { getField :: FieldValue s }

そして、Fieldの拡張可能な集まりとして、レコードを実現する。「拡張可能な集まり」のために、拙作のextensibleを利用した。

extensibleパッケージでは、積の表現として(:*)という型を定義している。型h :* [a, b, c, …](型レベルリストを使用していることに注意されたい)は、(h a, h b, h c, …)のタプルに相当する。直接(a, b, c, …)と対応させない理由は、すぐに明らかになる。

積を扱うのに必要なAPIはこの3つだ。

Nil :: h :* []
(<:*) :: h x -> h :* xs -> h :* (x : xs)
sector :: (x ∈ xs) => Lens' (h :* xs) (h x)

Nilと(<:*)はnilとcons、sectorは、積の特定の要素に対するLensである。x ∈ xsは、型レベルリストxsの中にxがただ一つ存在することを表す。

ちなみに、xがない場合、Couldn't match type ‘Missing x’ with ‘Expecting one’xが重複している場合は‘Couldn't match type ‘Ambiguous x’ with ‘Expecting one’というエラーが出る。地味な売りの一つだ。

そんなわけで、Field :* '["foo", "bar", "baz"]とすると、(Field "foo", Field "bar", Field "baz")相当になり、その中身は、foo, bar, bazがFieldValueが指し示す型になるのだ。レコードの型を以下のように定義しよう。

type Record = (:*) Field

FieldValueインスタンスと、Lens (x ∈ xs) => Lens' (Record xs) (FieldValue x)はできれば一緒に生成したいが、こんなのはTemplate Haskellをちょいと練ればすぐにできるだろう。

mkField "personId" [t|Int|] -- personId :: ("personId" ∈ xs) => Lens' (Record xs) Int
mkField "name" [t|String|]
mkField "address" [t|String|]

getPersonIdはORF版と同じくらいシンプルに表現でき、言語自体を拡張する必要もない。

getPersonId :: ("personId" ∈ xs) => Record xs -> Int
getPersonId = view personId

さて、問題は最後の要件だ。定義したLensをそのまま使い、こんな風に書ければ理想的ではある。

(@=) :: ((s ∈ xs) => Lens' (Record xs) (FieldValue s)) -> FieldValue s -> Field s
(@=) _ = Field

fubuki :: Record '["personId", "name"]
fubuki = personId @= 1
     <:* name @= "吹雪"
     <:* Nil

しかし、mkFieldによって生成されるLensと、型族FieldValueは、どちらもフィールド名を具体化するのには使えず、(@=)は作れないのだ!この世の終わりだー!

諦めるのはまだ早い。実は、既存のLensのインターフェイスを損なうことなく、フィールド名をLensに忍ばせることが可能なのである。

ちょっとした型クラスを用意する。

class Labelable s p where
  unlabel :: proxy s -> p a b -> a -> b

instance Labelable s (->) where
  unlabel _ = id
  {-# INLINE unlabel #-}

フィールドの型を以下のように定義し直す。比較のため、普通のLens'の場合をその下に書く。

type FieldLens s = forall f p xs. (Functor f, Labelable s p, s ∈ xs)
  => p (FieldValue s) (f (FieldValue s)) -> Record xs -> f (Record xs)

-- Lens' (Record xs) (FieldValue s) = forall f. (Functor f)
--  => (FieldValue s -> f (FieldValue s)) -> Record xs -> f (Record xs)

-- field f = sector (fmap Field . unlabel f . getField)のような形になる(実際は型アノテーションが必要)

(->)Labelable p => pに一般化しているのがポイントだ。これを使うことによって、以下のように単相な「代表」を構成し、フィールド名の曖昧性を排除できるのだ!

data LabelPhantom s a b

instance (s ~ t) => Labelable s (LabelPhantom t) where
  unlabel _ = error "Impossible"

type FieldName s = LabelPhantom s (FieldValue s) (Proxy (FieldValue s))
  -> Record '[s] -> Proxy (Record '[s])

これのおかげで、無事に(@=)コンビネータを定義できる。

(@=) :: FieldName s -> FieldValue s -> Field s
(@=) _ = Field

めでたしめでたし…いや、志の高い諸君ならば、束縛の順番を入れ替えても大丈夫なようにしたいだろう。そんなときのために、Data.Extensible.Inclusionshrink関数が使える。

shrink :: (xs ⊆ ys) => h :* ys -> h :* xs

前に定義したように、Recordは単なる型シノニムなので、shrinkを適用することによって順番を自由に入れ替えられる(し、一部を切り出すこともできる)。もちろん、型エラーの読みやすさは損なわれていないので、気になる人は試してみるとよいだろう。

fubuki :: Record '["personId", "name"]
fubuki = shrink 
       $ name @= "吹雪"
     <:* personId @= 1
     <:* Nil

こうして、街に平和が訪れためうー!型レベルプログラミングのおかげだね!

このアイデアの実装はGitHubextensibleのリポジトリにあり、次のextensibleのリリースに組み込む予定だ。

結論

OverloadedRecordFieldsなんていらんかったんや!

副作用の話

最近、副作用や関数型言語についてもめているのをよく目にする。副作用と関数型言語に関する私の見解をここに示す。

処理系はソースコードを解釈し、コンピュータの入出力、つまり副作用に変換する。ほとんどのプログラミング言語は、副作用を表現するプリミティブとその組み合わせによってプログラムを記述する。副作用は実行時に生まれるものだから、「Cの関数は副作用がある」「Haskellのコードに副作用はない」といった議論は、残念ながら意味をなしていない。実行していないのに副作用が出てきたら、超自然的な力を信じざるを得ない。

副作用の扱いについてよく議論の的になる言語としてHaskellがある。Haskellが多くの手続き型言語と異なるのは、副作用を含む計算に対して、専用の型(IO)が定義されているというだけであり、「そのコードが副作用を記述できるかどうか」を区別しやすくするためのちょっとした助けに過ぎない。その区別が拡大解釈され「副作用がない」といった主張が行われるのは、Haskell使いとしてとても残念だ。

私を含む多くのHaskellerがHaskellを支持する理由は、実は「明示的に副作用を扱える」点にある。そして、副作用の扱いに優れている理由は、Haskellは関数を扱うのが得意だからなのだ。

Haskellでは、プログラムを表現するために、モナドと呼ばれる構造を用いている。その仕組みはいたって単純だ。

  • ただ値を返すだけの処理も計算(プログラム)である。
  • 前の計算の結果をもとに、次にどのような計算をするか決めることができる。

後者を表しているのが有名な演算子(>>=)である。IOについて、(>>=)はIO a -> (a -> IO b) -> IO bという型を持っている。

IO aは、aという型が結果である計算を表す。a -> IO bは、aを受け取りIO bを返す関数で、IO baに依存して決まることを意味する。モナドは変数の概念を必要としない、シンプルなプログラムの表現方法の一つである。

既にお気づきかと思うが、(>>=)は関数が第一級オブジェクトであることを前提に置いている。Haskellが関数の記述に長けているゆえに、Haskellは副作用の表現のためのツールが作りやすいのだ。

Haskell以外の関数型言語では、暗黙に副作用を埋め込むほか、一意型を用いるといった方法も使われているが、私はモナドが一番馴染みやすかった。

ほとんどのプログラミング言語は、副作用を表現することができる。しかし、これからの手続き型言語は、副作用を明示的に扱う仕組みを積極的に採用すべきだと考えている。

最後に、「関数型言語」という言葉は、言語の分類のための便宜的で曖昧な言い方であり、プログラミングパラダイムに関する議論で積極的に用いるのは推奨しない。

オブジェクト指向関連の不満な点

また遅れてしまった…オブジェクト指向 Advent Calendar11日目。

最近いろいろオブジェクト指向について調べていて、やっぱりこの概念はいかがなものか、と思ったことを軽く書く。

継承

まずこれ。オブジェクト指向において「AがBを継承している」というのは、外延的には「任意のBのメソッドはAのメソッドである」であり、実装としては「AはBが持つ内部状態やメソッドの実装を利用できる」ことが多い。しかし、Twitterにも似たようなことを書いたが、「車クラスが乗り物クラスを継承している」みたいなゆるふわな使い方が目立つ。クラスインターフェイスの用語と、それらの継承の定義ははっきりさせたい。則のない関係に意味なし。

抽象クラス、抽象メソッド

実装のない「抽象メソッド」と実装のあるメソッドをごちゃ混ぜにできるような仕様は害があると考えている。インターフェイスがあるにもかかわらずこのような中途半端な仕組みを作るのは、不完全な抽象化と複雑化の原因になりうる。

多重継承

暗黙に何もかも引き継いでしまう仕様と、複数の概念から引き継ぐ仕様は本質的に両立しない。言語の簡単さを犠牲にして小難しいテクニックを導入するより、どこから引き継ぐのか明示したほうが楽に見える。

デザインパターン

デザインパターンは「道具」ではなく、「よくあるパターン」である。なぜそのパターンになったかを感覚的に理解せずに、デザインパターンを「使おう」としてもうまくいかないのに、なぜありがたがっている人が多いのかよくわからない。また、言語設計者はデザインパターンを不要にしていく方向への努力が必要だと考えている。

オブジェクト指向

すべてオブジェクトで扱うのが正義という風潮、これはよくない。オブジェクト指向は、内部状態を持った概念に対する操作の列(メインループでUpdateメソッドを繰り返し呼ぶなど)を抽象化するとき初めて必要になるものだと私は考えている。オブジェクト指向の表現力が高いのは確かだが、だからといって基本的な型や関数をおろそかにするのは本末転倒だ。


私が満足できるようなオブジェクト指向プログラミングができる言語はまだ存在しない。近いうちに理想的なオブジェクト指向プログラミング言語を作って、オブジェクト指向はどうあるべきか、また考えてみたい。

状態、手続き、OOP

これはオブジェクト指向 Advent Calendar 2014の7日目の記事です。

入力によって変化する状態をうまく扱うことはプログラミングにおいて重要である。この記事では、状態や手続きを扱うさまざまなアプローチを紹介、比較する。

手続き型プログラミング

命令の列である手続きを「定義」「呼び出し」する記述力を提供する。命令の結果として得られた値に依存して、次に行う手続きを決定するという性質が肝である。

コルーチン

手続きに区切りを設けることで、「途中まで実行された手続き」を値として扱う。手続きが持つ潜在的な状態を閉じ込めることができるため、柔軟性が高い。

第一級手続き型プログラミング

手続きをデータ構造の一つとして表現し、呼び出すだけではなく、手続きそのものに対する操作も可能にする。「手続きの結果として得られた値に依存して、次に行う手続きを決定する」性質に対応するものとしてモナドがある。モナドHaskellやPureScriptでは非常によく使われている。

構造体とフィールド

複数の値をまとめて一つの値として扱うための構造。フィールドを指定して、値を取得したり更新したりできる。

第一級フィールド

フィールドを値で表現することにより、より柔軟に構造に対してアクセスできるようにする。第一級フィールドの利点は、それが対象とする構造と強く結びつけられていないことであり、例えばRGBを格納する構造に対し、色相、彩度、明度のフィールドを定義することも可能である。lensはもっとも完成された第一級フィールドの実装であり、フィールド同士の合成や、データ型からの自動生成などができる。

オブジェクト指向

「オブジェクト」と「メソッド」の仕組みを新たに提供する。オブジェクトに対してメソッドを呼び出すと、メソッドに関連付けられた手続きが呼び出され、オブジェクトの内部状態が変化する。オブジェクトの内部状態を表現するのに構造体の仕組みがよく使われている。また、インターフェイスがあれば内部状態の持ち方が異なっても同じように扱えるため、複雑な状態を持つアプリケーションの記述に特に有用である。

第一級オブジェクト指向

オブジェクト指向の諸概念を言語に組み込むのではなく、あくまでデータ構造で表現することで、より自由にオブジェクトを扱う。第一級オブジェクト指向においては、オブジェクトは手続きを手続きに変換するオートマトンインターフェイスメソッドの集合であり、継承はメソッドの集合の包含関係で表される。オブジェクト自体が合成可能であるほか、メソッドモナドにすることで、メソッドを合成可能にもできる。従来のオブジェクト指向と異なり、副作用が常に表現できるわけではないため、ゲームのロジックなどを安全に記述するDSLに応用できる。第一級オブジェクト指向の実装としてobjectiveがある(作った)。

FRP(Functional Reactive Programming)

Behavior(時間によって連続的に変化する値)とEvent(飛び飛びの時間で発生する値)を扱う。BehaviorでEventを振り分けたり、Eventを畳み込んでBehaviorにしたりする関数を組み合わせてプログラムを記述する。入力と出力がはっきりしていれば、信号の流れを非常にきれいに表現できる。一方、手続き的な考え方との相性は良くないので注意する必要がある。FRPの実装として、netwireYampaelereasodiumreactive-bananaordreaなどがある。

まとめ

一般に、言語の組み込み機能として何かの機能を提供するより、型と値を用いて目的のものを実装するほうがより柔軟性が高い。目的が何であれ、型と値の記述力を重視するのが、最高のプログラミング体験への鍵であるとして、この記事を締め括ろう。