Profunctorってどう使うの? 〜Lensで学ぶProfunctor〜
はじめに
この記事は、Profunctor
という名称や定義などは見たことがあるが、これが何ものなのか、実際どう使うのかワカラン😭という人向けの記事です。
またPureScriptにおいてLens
がどのような仕組みで実現されているのかの興味がある人向けでもあります💪
難しそうなところはできるだけ図解を試みました。
どう学ぶ?Profunctor
Profunctor
は抽象的であるがゆえの強力さを持っていると思いますが、一方で抽象的であるがゆえ実用のイメージがしづらいかと思います。
このような対象について学ぶ場合のアプローチは色々あるかと思いますが、本記事のアプローチは
「実用されているライブラリでの使い方を見てみよう✨」
です!
実用に足るライブラリの機能がどう実現されているかも知ることができて一石二鳥だ、とモチベーションがわきませんか?(わかない?)
わかないという意見はガン無視🙈して進めます。ちなみに今回はProfunctor
が実用されているライブラリとしてタイトル通りLens
を取り上げます。
PureScriptにはprofunctor-lensesというライブラリがありますので、こちらのコードを見ながら解説を加えていきます。
この記事で登場する方々
解説に入る前に、この記事でご登場願う型クラスやtype
などを挙げておきます。
漫画とか小説の冒頭の登場人物一覧のようなもので、これらについて解説を行います。
-
Profunctorとそのお友達🤝
- Profunctor
- Strong
-
Lensとゆかいな仲間たち🤝
- Lens
- Optic
- AGetter
- Setter
- Fold
- Forget
LensにおけるProfunctorの使用例
では、早速ですがLens
の使用例と、Lens
のライブラリのコードを見てみましょう。
以下はLens
を作る例です。Lens
を作るにはlens
関数を使います。
-- テキトーに用意したデータ型
type BoxRec = { value :: String }
data Box = Box BoxRec
_Box :: Lens Box Box BoxRec BoxRec
_Box = lens (\(Box a) -> a) (\_ -> Box)
Lens
はgetterとsetterの組だとよく言われていますが、(\(Box a) -> a)
がgetterの部分で、(\_ -> Box)
がsetterの部分にあたります。
では、lens
のコードを見てみましょう。
はい!ドンッ💥
lens :: forall s t a b. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens get set = lens' (\s -> (Tuple (get s) \b -> set s b))
lens' :: forall s t a b. (s -> Tuple a (b -> t)) -> Lens s t a b
lens' to pab = dimap to (\(Tuple b f) -> f b) (first pab)
class Profunctor p where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d
上記のコードを図解してみます。
lens
関数からはlens'
という関数が呼び出されています。
get
とset
は、lens'
に渡している関数の中で呼び出されています(つまり後から使われます)。
そして、lens'
ではdimap
という関数を呼び出しています。
これはProfunctor
の関数ですね。
ということで出てきましたよ Profunctor
。
が、これだけでは「この関数ではProfunctor
が使われてるね😄」ということしかわからないでしょう。
私は初見では何もわかりませんでした😨
ということで、解説に移っていきます。
lens
とdimap
の話
わかること、わからないこと
さきほどのコードは複雑そうですが、そう見えるのはgetterやsetterなどの関数を使う関数を新しく作って別の関数に渡してたりするからで、冷静に追えばわかる程度のものです。
出てくる型もTuple
のような見知った型です。
そこを除くと、よくわからない部分というのはProfunctor
とLens
の部分になるでしょう。
これらがわかれば動作を理解できそうです。
ということでこれらを説明します👨🏫
🏹Profunctor
Profunctor
の定義はこうなっています。
型変数p
でパラメーター化された型クラスになっていますね。
そしてdimap
という関数が定義されています。
class Profunctor p where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d
このdimap
という関数はとても重要な関数で、上記のlens
関数の他にもLens
のライブラリではしばしば登場します。
それにしても初見ではぱっと見よくわからない関数に見えると思います。
そこでちょっと線を引いてみます。
-> のような意味合いで線を引いてみた
Function
である(->
)とこの線をあわせて辿っていくと、a
からd
まで繋がっています。
そうすると(a -> b)
と(c -> d)
とp b c
を引数として渡すと、a
をd
に写すp a d
のようなものが返ってくるような関数合成のような構造が見えてこないでしょうか。
実際、関数そのものに対するProfunctor
型クラスのインスタンスが定義されており、この実装は関数合成そのものです。
instance profunctorFn :: Profunctor (->) where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> (b -> c) -> (a -> d)
dimap a2b c2d b2c = a2b >>> b2c >>> c2d
テストを書いて確認してみます。
spec :: Spec Unit
spec = do
describe "Produnctor Test" do
it "dimap" do
let
fn = dimap show Just ("add " <> _)
fn 100 `shouldEqual` Just "add 100"
dimap
の結果が、show >>> ("add" <> _) >>> Just
という合成になることが確認できました。
これはインスタンスが関数の場合の例でしたが、Profunctor
は型クラスとして定義されているため、インスタンスによって様々なことを実現できます。例えば、Profunctor
を関数(a -> b)
と(c -> d)
を繋げるような部分と捉えた場合、繋げ方をインスタンスによって変えることで色々やれるわけです。つまり拡張性があります。これは単なる関数合成より抽象的で強力💪です。
しかし私がこのタイミングでPfofunctor
は強力🦾と書いてみたところで、あまりそういった実感やイメージが持てない方もおられるのではないかと思います。
やはり具体例が必要だと思います。
ということで、インスタンスの例を一つ紹介しましょう。
どうせならLens
で使われている型にします。
🥴Forget
それはForget
です。
見てください。次のようにこの型はProfunctor
型クラスのインスタンスになっています。
newtype Forget :: forall k. Type -> Type -> k -> Type
newtype Forget r a b = Forget (a -> r)
instance profunctorForget :: Profunctor (Forget r) where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> Forget r b c -> Forget r a d
dimap f _ (Forget z) = Forget (z <<< f)
型変数を見てください。型変数b
が右辺にまったく登場しません。
いわゆるPhantom Type
です。
オイオイオイ右辺のヤツ、完全にb
を忘れちまってるぞ😮💨
すぐわかりますがこれがイイんです。
この型変数としては出てくるけど、使われていないというのがすごくイイ。
ま、それは置いておいてdimap
の実装部分も見てみましょう。
Forgetのdimap
(c -> d)
の部分が_
となっており、まったく使われていません。
定義からForget r b c
型の値はForget (b -> r)
で、Forget r a d
型の値はForget (a -> r)
という形になります。
型変数c
やd
はPhantom Type
なため、出てきません。
Forget z
のz
は(b -> r)
なので、(a -> b)
と合成すれば(a -> r)
になるので、Forget (z <<< f)
はForget (a -> r)
にマッチします。
このForget
のdimap
の特徴は、2つ目の引数の関数をスルーするというところです。
さきほどイイと書いたのはこういうことができるからなんです。
ちなみにこの特徴がLens
で活かされることになります。
Forget
の他にもLens
にはProfunctor
のインスタンスになっている型が色々ありますので、気になったら調べてみてください。
🔍Lens
次にLens
にいきます。
Lens
はこのように定義されています。
type Lens s t a b = forall p. Strong p => Optic p s t a b
type Optic :: (Type -> Type -> Type) -> Type -> Type -> Type -> Type -> Type
type Optic p s t a b = p a b -> p s t
つまりLens
とは結局Optic
なわけです。わかりやすくするため、Optic
を使わないで定義してみます。
type Lens s t a b = forall p. Strong p => p a b -> p s t
案外シンプルです。
ここでStrong
という新しい型クラスが登場したので、これについても見ていきましょう。
💪Strong
これがStrong
の定義です。
class Profunctor p <= Strong p where
first :: forall a b c. p a b -> p (Tuple a c) (Tuple b c)
second :: forall a b c. p b c -> p (Tuple a b) (Tuple a c)
Profunctor
がsuperclass
になっていますね。
そして何やらfirst``second
という関数はTuple
を用いた何かを返すようです。
これまたイメージが浮かびづらいと思うので、具体的なインスタンスをお見せしましょう。
ということで、またForget
にご登場いただきます。
なぜならForget
はStrong
のインスタンスでもあるからです💪
instance strongForget :: Strong (Forget r) where
first :: forall a b c. Forget r a b -> Forget r (Tuple a c) (Tuple b c)
first (Forget z) = Forget (z <<< fst) -- Tuple b c は無視されて Tuple a c の a を fst で取って (a -> r) 渡している
--first (Forget z) = Forget (\(Tuple a _) -> z a) ↑はこういうこと
second :: forall a b c. Forget r b c -> Forget r (Tuple a b) (Tuple a c)
second (Forget z) = Forget (z <<< snd)
--second (Forget z) = Forget (\(Tuple _ a) -> z a) ↑はこういうこと
相変わらず、最後の型パラメーターのことは忘れてしまっています😄
Lens再び
必要な情報が揃ったので、再びlens
関数のところに戻ってきました。
(Lens
のOptic
を使わない定義や、Profunctor
やStrong
もあわせて載せています)
type Lens s t a b = forall p. Strong p => p a b -> p s t
lens :: forall s t a b. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens get set = lens' (\s -> (Tuple (get s) \b -> set s b))
lens' :: forall s t a b. (s -> Tuple a (b -> t)) -> Lens s t a b
lens' to pab = dimap to (\(Tuple b f) -> f b) (first pab)
class Profunctor p where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d
class Profunctor p <= Strong p where
first :: forall a b c. p a b -> p (Tuple a c) (Tuple b c)
まず、lens'
から呼ばれているdimap
やfirst
はこれまで見てきたように型クラスの関数です。
Lens
はProfunctor p => p a b -> p s t
なので、型変数p
がどの型になるかによってdimap
やfirst
の動作は変わってきますね。
これも何か実際のインスタンスを例にとって見たほうがよいでしょう。
ということで再びForget
の登場です。
p
がForget r
の場合
まずProfunctor
とStrong
の(Forget
の)インスタンスの定義をまとめて再掲します。
instance profunctorForget :: Profunctor (Forget r) where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> Forget r b c -> Forget r a d
dimap f _ (Forget z) = Forget (z <<< f)
instance strongForget :: Strong (Forget r) where
first :: forall a b c. Forget r a b -> Forget r (Tuple a c) (Tuple b c)
first (Forget z) = Forget (z <<< fst)
これをもとにlens'
をもう一度見てみましょう。
Lens s t a b
とはforall p. Strong p => p a b -> p s t
のことであったので、p
をForget r
にしてみます。
lens' :: forall r s t a b. (s -> Tuple a (b -> t)) -> Forget r a b -> Forget r s t
lens' to pab = dimap to (\(Tuple b f) -> f b) (first pab)
するとpab
の部分は、Forget r a b
であることがわかります。
次にForget
のdimap
では(\(Tuple b f) -> f b)
は使われないので、返されるのはfirst pab
とto
の合成関数を持つForget
となります。
図解: p
がForget r
の場合のlens'
first
でTuple a b
のa
の部分だけ取得しているため、setterであるb -> t
(赤枠の部分)は無視されています。
またfirst
で取得しているa
はgetterの結果でした。
このa
がpab
であるForget r a b
が持っている関数z
に渡されています。
つまり、Forget
におけるLens
関数とは、『getterの元ネタs
を渡すと、s
からgetterで値を取得し、その結果を、渡したForget
が持つ関数に渡した結果を返すという関数を持つForget
を返す』という関数です(文章で書くとわかりづらいな😵)。
さて、ここまででlens
関数が返しているLens
がなんだか何となくおわかりいただけたのではないでしょうか(特にForget
の場合は)😄
getterとsetterを使う大枠の部分と、関数を繋げる部分を切り離しておき、その繋げる部分をProfunctor
のインスタンスによって差し替えられるようになっているわけです。
ちなみにLens
の他の仲間たち、例えば代数的データ型のような直和型を扱うPrism
や、配列的な値を扱えるAffineTraversal
などでもdimap
が使われていますが、同じような考え方で作られていると思います。
Lensを使う
ここまでは、Lens
を作るところまでを解説してきましたので、次は実際に作ったLens
を使った場合の動作について見ていきたいと思います🙋♂
例として値の取得と設定を行うテストを書いてみました。
type BoxRec = { value :: String }
data Box = Box BoxRec
-- テストで使うためのShowとEq
instance showBox :: Show Box where
show (Box b) = _.value b
instance eqBox :: Eq Box where
eq (Box b1) (Box b2) = _.value b1 == _.value b2
-- BoxのLensを定義
_Box :: Lens' Box BoxRec
_Box = lens (\(Box a) -> a) (\_ -> Box)
spec :: Spec Unit
spec = do
describe "Lensのテスト" do
it "viewで値を取得することができる" do
let
box = view _Box (Box {value: "Value"})
box `shouldEqual` {value: "Value"}
it "setで値を設定することができる" do
let
box = set _Box {value: "NewValue"} (Box {value: "Value"})
box `shouldEqual` Box {value: "NewValue"}
👓view
Lens
を使って値を取得するために使えるのがview
関数です。
定義を見てみましょう。
view :: forall s t a b. AGetter s t a b -> s -> a
view l = unwrap (l (Forget identity))
新しいおともだちAgetter
が登場したので、view
の内容を見る前にAGetter
の定義を見てみましょう。
type AGetter :: Type -> Type -> Type -> Type -> Type
type AGetter s t a b = Fold a s t a b
type Fold :: Type -> Type -> Type -> Type -> Type -> Type
type Fold r s t a b = Optic (Forget r) s t a b
type AGetter s t a b = Forget a a b -> Forget a s t
結局AGetter
はtype Fold
なのですが、Fold
を見ると見慣れたOptic
とForget
を使って定義されています。
わかりやすくするため、Fold
やOptic
を使わないで定義してみるとこうなります。
type AGetter s t a b = Forget a a b -> Forget a s t
なんとAGetter
とはこれまで散々見てきたForget
でした。
Forget a a b = Forget (a -> a)
で、Forget a s t = Forget (s -> a)
です。
AGetter
の定義がわかったところで、view
の定義に戻ります。
view :: forall s t a b. AGetter s t a b -> s -> a
view l = unwrap (l (Forget identity))
これをForget
で置き換えてみるとこうなります。
view :: forall s t a b. (Forget a a b -> Forget a s t) -> s -> a
view l = unwrap (l (Forget identity))
l
は(Forget a a b -> Forget a s t)
という関数なので、引数を(Forget identity)
として呼び出すことができます(Forget a a b = Forget (a -> a)
だから)。
するとForget a s t
型の値が返ってきます。Forget a s t = Forget (s -> a)
なので、これをunwrap
すると(s -> a)
。この関数を引数s
で呼び出すとa
が返ってくる、というわけです。
ところでこの(Forget identity)
はどう使われているのでしょうか?
まず、そもそも上記の例ではview
関数の引数として、Lens
型の値を渡していました。
あらためてLens
の定義を見てみます(Optic
を使わない形に書き換えてあります)。
type Lens s t a b = forall p. Strong p => p a b -> p s t
そしてAGetter
の定義と見比べてみましょう(こちらもOptic
を使わない形に書き換えてあります)。
type AGetter s t a b = Forget a a b -> Forget a s t
ここで、Forget
が型クラスStrong
のインスタンスであったことを思い出してください。
故にAGetter
を引数にとる関数にLens
型の値を渡した場合、Forget
のインスタンスが選択されることになります。
ところでlens
関数の型変数p
がForget
だった場合の説明は、既にしていましたね?
もう一度、図を見てみましょう。
今回はpab
であるForget
が持っている関数がidentity
だということがわかっているので、そのように図を更新してあります。またForget
の場合setterは使われないので図では着目していません。
つまり、view
関数は、getterで取得した値をそのまんま返しているというわけです。
🤏set
次にset
関数です。
定義はこうです。
set :: forall s t a b. Setter s t a b -> b -> s -> t
set l b s = over l (const b) s
over :: forall s t a b. Setter s t a b -> (a -> b) -> s -> t
over l = l
また新しいお友達Setter
が出てきたのでこちらの定義も見ましょう。
type Setter s t a b = Optic Function s t a b
お馴染みのOptic
を使わない定義も見てみます。
Optic p
の部分を見てみると、単なる関数になっています。
type Setter s t a b = (a -> b) -> (s -> t)
ではSetter
の定義を見たところで、set
のコードを見てみましょう。
set :: forall s t a b. Setter s t a b -> b -> s -> t
set l b s = over l (const b) s
over
から返される関数(a -> b) -> s -> t
に、引数として(const b)
とs
を渡しています。
つまりa -> b
にconst b
が、s
にはまんまs
が渡されて、t
が返ってきます。
const b
なのでa
がどのような値であろうとb
はそのまま渡されます。
over
も見てみましょう。
Setter
をOptic
を使わない形で置き換えてみると、over
の型アノテーションはこうなります。
((a -> b) -> (s -> t)) -> (a -> b) -> s -> t
((a -> b) -> (s -> t))
型の関数に、引数として(a -> b)
型の関数を渡すと、(s -> t)
型の関数が返され、その関数に引数としてs
を渡してt
が返るというわけですね。
さて、set
に渡されているl
とは、Lens
型の値でした(view
のときと同じく)。
ということで、またまたLens
とlens
関数を見てみます。
type Lens s t a b = forall p. Strong p => p a b -> p s t
lens :: forall s t a b. (s -> a) -> (s -> b -> t) -> Lens s t a b
lens get set = lens' (\s -> (Tuple (get s) \b -> set s b))
lens' :: forall s t a b. (s -> Tuple a (b -> t)) -> Lens s t a b
lens' to pab = dimap to (\(Tuple b f) -> f b) (first pab)
class Profunctor p where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> p b c -> p a d
class Profunctor p <= Strong p where
first :: forall a b c. p a b -> p (Tuple a c) (Tuple b c)
set
の場合、Strong p
のp
が何になるかというと、Optic Function
となっていたことからわかるようにFunction
すなわち->
になります。
一番最初の方で書いたのですが、次のように->
もまたProfunctor
のインスタンスになっているため、可能なのです。
instance profunctorFn :: Profunctor (->) where
dimap :: forall a b c d. (a -> b) -> (c -> d) -> (b -> c) -> (a -> d)
dimap a2b c2d b2c = a2b >>> b2c >>> c2d
これは単なる関数合成ですね🙂
Strong
の方はこうです(Strong
にはsecond
という関数も定義されているのですが省略しています)。
instance strongFn :: Strong (->) where
first :: forall a b c. (a -> b) -> (Tuple a c) -> (Tuple b c)
first a2b (Tuple a c) = Tuple (a2b a) c
これでdimap
とfirst
の実装がわかったので、lens
の動きもわかりそうです。
図解してみましょう。
set
関数はLens
の関数であるl
に、引数pab
としてconst b
を渡していました。
set :: forall s t a b. Setter s t a b -> b -> s -> t
set l b s = over l (const b) s
これはfirst
におけるa2b
の部分にあたります。またconst x y
とは、y
の値がなんであれ必ずx
を返すという関数なのでconst b
は常にb
を返す(a -> b)
型の関数となります。
このb
はset
関数の引数として渡した値で、setしたい値そのものになります。
そしてこの関数const b
に渡されるa
とはLens
のgetterの結果です。
なのでgetterで取得した値は無視されるということになります(図の青枠の部分)。
set
の場合のdimap
は単なる関数合成なのですが、処理を見てみるとsetterはview
のときと異なり最後の関数まで渡ってきています(赤枠の部分)。そしてb -> set s b
の結果最終的にset s b
の結果であるt
が返ります。
view
とset
どちらも同じlens
関数で生成されたLens
を使っていますが、view
のときはsetterが無視され、set
のときはgetterが無視されるということが実現できていることがわかったかと思います🧙
そしてそれを実現しているのがProfunctor
なのでした。
おわりに
今回の記事では、Lens
という有名なライブラリの実装を通じて、実用的なProfunctor
の使われ方を見てきました。
抽象的ゆえに様々な使い方が考えられると思いますが、一例として、抽象化されている部分を『対象と対象を繋ぐ部分』と捉えることで、オープン・クローズドの原則を満たす形で拡張性を持たせるという使い方があることがご理解いただけたかと思います。
Lens
のライブラリには、他にもPrism
におけるChoice
の実例であったり、AffineTraversal
での更なるStrong
の実例だったりがあり、Profunctor
関連の実例の宝庫💎なので、もし興味がわきましたらぜひ参考にしていただきたいと思います。
おまけ (Lensの合成)
おまけとしてLens
を合成した場合、どうなっているのか軽く説明してみます。
まずLens
を2つ作ってみます。
-- テキトーな入れ物
type BoxRec = { value :: String }
data Box = Box BoxRec
-- Boxを持ってるだけ
newtype Container = Container Box
instance showBox :: Show Box where
show (Box b) = _.value b
instance eqBox :: Eq Box where
eq (Box b1) (Box b2) = _.value b1 == _.value b2
derive newtype instance showContainer :: Show Container
derive newtype instance eqContainer :: Eq Container
-- Lens
_Box :: Lens' Box BoxRec
_Box = lens (\(Box a) -> a) (\_ -> Box)
-- Lens
_Container :: Lens' Container Box
_Container = lens (\(Container b) -> b) (\_ -> Container)
このLens
を合成したものを使ってview
関数を呼び出してみると、普通に値を取得できます(当たり前)
spec :: Spec Unit
spec = do
describe "Getter Test" do
it "view" do
let
box = view (_Container <<< _Box) (Container $ Box {value: "Value"})
box `shouldEqual` {value: "Value"}
なぜ(_Container <<< _Box)
のようにLens
を合成することができるのでしょうか🤔?
ご存知の通りLens s t a b
とはforall p. Strong p => p a b -> p s t
のことでした。
上記の定義をこちらの定義で書き換えてみるとわかりやすいのでやってみます。
_Box' :: forall p. Strong p => p BoxRec BoxRec -> p Box Box
_Box' = _Box
_Container' :: forall p. Strong p => p Box Box -> p Container Container
_Container' = _Container
この定義であれば次のように合成できることが容易にわかるかと思います。
composed1 :: forall p. Strong p => p BoxRec BoxRec -> p Container Container
composed1 = _Container <<< _Box
今度は上記の定義をLens
を使った定義に戻してみます。
composed2 :: Lens' Container BoxRec
composed2 = _Container <<< _Box
Lens
の合成はこのようになっています。
最後にview
関数を使う場合どうなるかを見てみましょう。
view
関数の引数はAGetter
で、これは定義的にはForget
を用いた関数なのでインスタンスとしてはForget
のインスタンスが選択されます。
ということで上記をForget
にしてみます。
composed3 :: Forget BoxRec BoxRec BoxRec -> Forget BoxRec Container Container
composed3 = _Container <<< _Box
以上で、おまけは終りになります🔚
Discussion