Avendia19
English

日記 (2019 年 4 月 25 日)

Haskell には lens という非常に便利なパッケージがあり、 これを用いると、 値の属性を取り出したり変更したりするのが簡単にできるようになります。 ただ、 機能が豊富すぎてどこで何をしてるのかよく分からないし、 型を見ても抽象的でよく分からないし、 何となく使えるけどどういう仕組みになってるのか一見した限りでは全然掴めません。 ということで、 ちょっとずつ何をやってるのか理解しようという試みです。

そもそも lens はどういうライブラリなのかという話ですが、 これはオブジェクト指向におけるゲッターとセッターのようなものを扱えるようにするものです。 例えば、 以下のようなレコード型があるとしましょう。

data Person = Person {_name :: Name, _age :: Int}
data Name = Name {_first :: String, _family :: String}

Person 型の値が渡されたとき、 その各フィールドの値を取り出したり変更したりする関数が欲しいとします。 これは以下のように実装できます。

-- _name フィールドの取り出しと変更
getName :: Person -> Name
getName = _name
setName :: Person -> Name -> Person
setName person newName = person {_name = newName}
-- _age フィールドの取り出しと変更
getAge :: Person -> Int
getAge = _age
setAge :: Person -> Int -> Person
setAge person newAge = person {_age = newAge}

同様に、 Name 型の値からフィールドの値を取り出したり変更したりする関数として、 getFirst, setFirst, getFamily, setFamily も定義できます。

一般に、 ゲッターは s -> a という形の型で表され、 セッターは s -> a -> s という形の型で表されます。 s が全体のオブジェクトの型を表し、 a はそのオブジェクトの今取り扱いたい一部分の型を指します。

さて、 それでは、 Person 型の値が渡されたとき、 _name フィールドの値のさらに _first フィールドの値を扱いたい場合はどうなるでしょうか。 要するに、 ネストされたレコード型の奥の方のフィールドを操作したいわけです。 すでに Person 型の _name フィールドを扱う関数 (getName, setName) と、 Name 型の _first フィールドを扱う関数 (getFirst, setFirst) は作ってあるので、 汎用性を考えると、 できればこれらの関数の組み合わせで実現したいです。 実際、 これは以下のようにすれば可能です。

-- 順にゲッターを呼び出す
getNameFirst :: Person -> String
getNameFirst = getFirst . getName
-- 一度 _name フィールドの値を getName で取得して _first フィールドを setFirst で変更する
-- これで得られた変更後の値を setName で置き換える
-- ちょっと長い・・・
setNameFirst :: Person -> String -> Person
setNameFirst person = setName person . setFirst (getName person)

ゲッターの方は良いですが、 セッターの方はちょっと長いですね。 ネストしたレコード型を操作したいときに毎回これを書くのはちょっと嫌です。 ただ幸い、 すでに定義された関数の組み合わせだけで書けているので、 以下のようなヘルパー関数を用意しておけば、 毎回この定義を書く必要はなくなります。

-- ゲッターとセッターをまとめて扱うための型シノニム
-- s 型に含まれる a 型の値を操作する
type Getset s a = (s -> a, s -> a -> s)
-- ゲッターとセッターの合成
(@.) :: Getset s a -> Getset a u -> Getset s u
(outerGet, outerSet) @. (innerGet, innerSet) = (compGet, compSet)
where
compGet = innerGet . outerGet
compSet obj = outerSet obj . innerSet (innerGet obj)

こうすれば、 上の getNameFirstsetNameFirst(getName, setName) @. (getFirst, setFirst) で得られます。 これによって、 ゲッターとセッターを最初からまとめてタプルとして定義しておけば、 あとは @. で組み合わせることで、 いくらでも深い位置にあるフィールドを操作できるようになります。

ついでに、 ゲッターとセッターを実際に適用するヘルパー関数も作ってみましょう。

-- ゲッターを利用する
(^.) :: s -> Getset s a -> a
obj ^. (get, set) = get obj
-- セッターを利用する
-- ゲッターセッターのタプルと新しい値を受け取る
-- 更新前のオブジェクトから更新後のオブジェクトを作る関数を返す
(.~) :: Getset s a -> a -> (s -> s)
(get, set) .~ newVal = \obj -> set obj newVal

こうすると、 以下のように非常に簡潔にネストされたフィールドの値を取得したり変更したりできます。

-- ゲッターセッターのタプルとして定義
getsetName :: Getset Person Name
getsetName = (getName, setName)
getsetFirst :: Getset Name String
getsetFirst = (getFirst, setFirst)
-- もととなるオブジェクトの定義
person = Person {_name = Name {_first = "Taro", _family = "Sato"}, _age = 14}
-- 値の取得
first = person ^. (getsetName @. getsetFirst)
-- 値の変更
-- & は $ を左右反転したもの
newPerson = person & (getsetName @. getsetFirst) .~ "Jiro"

画期的! ・・・ですが、 実はさらにこれを画期的にする魔法のような型があって、 それが lens パッケージに定義されている Lens' です。

type Lens' s a = forall f. Functor f => (a -> f a) -> (s -> f s)

この型ですが、 先程定義した Getset s a と同型です。 例えば、 Getset s a から Lens' s a へは以下のように変換できます。

toLens :: Getset s a -> Lens' s a
toLens (get, set) = \func sVal -> set sVal <$> func (get sVal)

逆に、 Lens' s a から Getset s a への変換については、 ゲッターは fConst a だと思うことで、 セッターは fIdentity だと思うことで、 それぞれ以下のように作ることができます。

-- ゲッターを作る
-- ((a -> Const a a) -> (s -> Const a s)) -> (s -> a) だと思っている
toGet :: Lens' s a -> (s -> a)
toGet lens = \sVal -> getConst $ lens Const sVal
-- セッターを作る
-- ((a -> Identity a) -> (s -> Identity s)) -> (s -> a -> s) だと思っている
toSet :: Lens' s a -> (s -> a -> s)
toSet lens = \sVal aVal -> runIdentity $ lens (Identity . const aVal) sVal

これらは互いに逆の構成になっていて、 例えば、 ゲッターとセッターから toLens でレンズを作り、 そこから toGettoSet でゲッターとセッターを作り直すと、 もとと同じ関数が得られます。 レンズから始めた場合ももとに戻ります。

ということで、 Getset s aLens' s a は実質同じであることが分かったわけですが、 ではなぜ Lens' s a の方を使うのでしょうか。 その 1 つの利点として、 合成が関数適用で済ませられるという点があります。 Lens' s a(a -> f a) -> (s -> f s) という関数を受け取って関数を返す関数型として定義されていますが、 よく見るとこの関数型の始域と終域は同じ形をしています。 つまり、 Lens' s a 型と Lens' a u 型は普通に関数合成することができます。

ちょっと計算してみましょう。 以下のようなゲッターセッターのタプルがあったとします。

(outerGet, outerSet) :: Getset S A
(innerGet, innerSet) :: Getset A U

ここから toLens でレンズを作って、 関数合成してみます。

toLens (outerGet, outerSet) . toLens (innerGet, innerSet)
≡ \func -> toLens (outerGet, outerSet) $ (toLens (innerGet, innerSet)) func
≡ \func -> toLens (outerGet, outerSet) $ \aVal -> innerSet aVal <$> func (innerGet aVal)
≡ \func -> \sVal -> outerSet sVal <$> (\aVal -> innerSet aVal <$> func (innerGet aVal)) (outerGet sVal)
≡ \func sVal -> outerSet sVal <$> innerSet (outerGet sVal) <$> func (innerGet (outerGet sVal))
≡ \func sVal -> outerSet sVal . innerSet (outerGet sVal) <$> func ((innerGet . outerGet) sVal)

ここに出てくる outerSet sVal . innerSet (outerGet sVal)innerGet . outerGet、 どこかで見たことありますね。 @. の定義です。 つまり、 ゲッターとセッターを Lens' s a 型として扱っておくことで、 ゲッターとセッターの合成がただの関数合成として実現できるわけです。

・・・ということで、 lens パッケージに定義されている Lens' 型がどうしてあんな形なのかについて、 素朴なゲッターとセッターのタプル型と同値であるという点と、 合成が関数適用でできるという点から見てみました。 それ以外にもこの形の利点はありますが、 それはまた別の機会にまとめられたら良いなと思います。

追記 (2019 年 4 月 29 日)

4 月 29 日に続きます。