LoginSignup
44
28

More than 5 years have passed since last update.

Observableってモナドらしいですよ

Last updated at Posted at 2018-03-20

Angularのチュートリアルを進めていたらRxJSだのObservableだのいう謎概念が登場して面食らっていたのですが

どうやら調べてみると、このObservable、「モナド」らしいんですね。

僕は以前「すごいHaskell たのしく学ぼう」という楽しそうな本でHaskellを勉強し、会社で使うプログラムをHaskellで書いたり競技プログラミングにHaskellで挑戦していたことがあるので、モナドについては全て完璧にわかっています(大嘘)

ということで、この記事では、モナドという側面からObservableを理解してみたいと思います。

「モナドって何?」については、ググるといろんな説明が出てきて理解に苦労しますが、僕はこちらの記事の説明方針が良いんじゃないかと思いました。
https://qiita.com/hiruberuto/items/8bbc0343bf794c368287

この記事の方針

Observableを他のモナドと比較して、「あ、確かに他のモナドと同じ性質を持ってるな〜。」っていう感覚を得ていく、という流れにします。

Observableがモナドであることを証明するわけではありません。少数の具体例を通して「それっぽ〜い」と思えればヨシとします。

具体的には、HaskellのMaybeモナド、リストモナドと、RxJSのObservableを比較していきます。

RxJSの方はTypeScriptのコードを見ていくことにします。

モナド則とかファンクター則とかも見ていくんですけど、「確かにモナド則を満たしてるぞ!」という部分に注目するよりは、「ObservableもMaybeやリストと同じような性質があるぞ!」という部分に注目して見ていってもらえればと思います。

注意(RxJS 6.x以上では、この記事内のコードは動きません)

2018年04月24日に、RxJSのバージョン6.0.0がリリースされました。

RxJS 6.x以上では、この記事内にあるコードはそのままでは動かなくなっています。(まだ記事書いて半年なのに動かなくなるなんて)
必要な変更を簡単に書くと

  • import文をimport { Observable, of } from 'rxjs';のように書く
  • operatorのimportは import { map, reduce } from 'rxjs/operators';のように書く
  • operatorはobservable1.pipe(map(...),reduce(...)).subscribe(...);のように書く

となっています。

詳しくは下記のサイトを御覧ください。
RxJS v5.x to v6 Update Guide (本家、英語)
RxJS 6.0 変更点まとめ(適宜更新) (Qiita記事、日本語)

ファンクターであることの確認

モナドは、ファンクターです。

モナドはよく「箱」に例えられますが、モナドだから箱っぽいというよりは、ファンクターだから「箱」っぽいんです。

箱っぽさの確認

Maybe

HaskellのMaybeInt(整数)とかString(文字列)とかの型を一つ引数にとって

Maybe IntとかMaybe Stringみたいな型になります。

Maybeという箱にIntStringが入ってるわけです。ほ〜らね!!箱っぽい!!!!!!!!

具体的な値としては、
Maybe Int型の値はJust 3とかJust (-1)とかNothingとかになります。
Maybe String型の値はJust "hoge"とかNothingとかになります。

Maybe ◯◯型の値は、頭にJustがつくか、Nothingになるんですね。

リスト

リストについてはどうかというと
[Int]とか[String]という型になります。見るからに箱っぽいですね。

[] Intとか[] Stringという書き方もできます。この書き方の方が、IntStringを引数にとっている感じがしますね。

意味は[Int][String]とまったく同じです。

具体的な値としては、
[] Int型なら[1,2,3][]など
[] String型なら["sakurai","sakazaki","takamizawa"][]などです。

Observable

ではObservableではどうかというと

Observable<number>とかObservable<string>といった型になります。

Observable型はよくObservable<T>と書かれますが、このTの所に好きな型を入れるわけです。間違いなく、型を一つ引数にとってますね!箱っぽい!

ちなみにObservableはリストと同じように、複数の値を持つことができます。

Observable<number>型なら、number型の値を複数持てる、ということです。

具体的な値としては、

…………これを簡単に表記する方法がないんですが……

でもまあ簡単に言うと、

Observable<number>型なら、その具体的な値は
「Observable箱に3が入ったもの」とか、
「Observable箱に3と4と5が入ったもの」とか、
「Observable箱に何も入っていないもの」とかになります。

Observable<string>型なら、その具体的な値は
「Observable箱に"hoge"が入ったもの」とか
「Observable箱に"hoge"と"fuga"が入ったもの」とか
「Observable箱に何も入っていないもの」とかになります。

試しに、Observable<number>型について、TypeScriptのコードを書いて見てみましょう。

import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/Observable/of';

const x1$ : Observable<number> = of(3);
const x2$ : Observable<number> = of(3,4,5);
const x3$ : Observable<number> = of();

console.log(x1$);
console.log(x2$);
console.log(x3$);

このコードをtscでJavaScriptになおしてnodeで実行すると出力はこうなります

ScalarObservable { _isScalar: true, value: 3, scheduler: null }
ArrayObservable { _isScalar: false, array: [ 3, 4, 5 ], scheduler: null }
EmptyObservable { _isScalar: false, scheduler: null }

この3行の出力の各行が、「Observable箱に3が入ったもの」「Observable箱に3,4,5が入ったもの」「Observable箱に何も入ってないもの」にあたります。

x1$とかに$がついているのは、「これはObservableですよ」という意味を表すために慣例で付けているだけで、なくても動作は変わりません。

fmapの確認

Maybeとリスト

3に10をかけると、3*10=30ですね。当たり前ですね。

3という整数には「*10」という演算を適用できるわけです。

ではJust 3というファンクター値には「*10」を適用できるでしょうか?

答えはNOです。

アバウトな表現で説明すると、ファンクター箱の中身に「*10」を適用するには、「*10」もファンクター箱に入れる必要があるんです。

それをやるのが、Haskellではfmapという関数です。(flatMapとは全然別物なので注意)

使い方は簡単で、*10の前にfmapをつけてやればOK。

適宜カッコなどを調整して、次のようになります。

fmap (*10) (Just 3) -- Just 30 になる
fmap (*10) [3,4,5] -- [30,40,50] になる

Maybeの例とリストの例をまとめて紹介しました。

Maybe String[] Stringの場合は下のようになります。

fmap (++"hoge") (Just "fuga") -- Just "fugahoge"
fmap (++"ko") ["ai","hana","ko"] -- ["aiko","hanako","koko"]

Observable

では、Observableではどうなのかというと、

mapという関数があって、これがその役割をします。fmapとほぼ同じ名前ですね。

やってみると次のようになりますが、コードの見た目が、Haskellの場合とかな〜り変わってしまいました…。。。

Haskellではfmap 関数 値の順になるのが、TypeScriptでは値.map(関数)という順になるので、見た目がそこそこ変わります。

import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/Observable/of';
import 'rxjs/add/operator/map';

const x1$ : Observable<number> = of(3,4,5);//Observable箱に数字を入れて
const x2$ : Observable<number> = x1$.map(x => 10*x);//mapで10倍
x2$.subscribe(x => console.log(x));//結果出力

const x3$ : Observable<string> = of("ai","hana","ko");//Observable箱に文字列を入れて
const x4$ : Observable<string> = x3$.map(x => x+"ko");//mapで +"ko"
x4$.subscribe(x => console.log(x));//結果出力

出力はこうなります

30
40
50
aiko
hanako
koko

リストの時と同じですね!!

でもなんか、Observable箱に入ってないように見えますね。

これは、さっきまでの方法で出力すると中身をちゃんと見せてくれないので、subscribeというものを使って中身を表示しているからです。だからあまり気にしないで…。
さっきまでと同じ方法で出力すると、「Observableに操作を施したものですよ」みたいな出力になって、最終結果がわからないんです…。

ファンクター則の確認

まだまだ、「ファンクターであることの確認」を続けていきます。

ファンクター則とは、ファンクターが満たす必要があるルールのことです。

ルールは2つあります。

第一法則

一つは、「恒等関数にファンクターを適応したものも、恒等関数」というルールです。

恒等関数とは、入力をそのまま出力する関数のことです。

ファンクターをF、恒等関数をidと書くことにすると

F(id) = id

という感じになりますが、まあ例を見ていきましょう。各行の右端にあるコメントが出力結果です。

fmap id (Just 3) -- Just 3
fmap id [1,2,3] -- [1,2,3]

idというのが恒等関数です。id 3などとするとそのまま3が出力されます。

ということで、問題ないですね。ファンクター箱の中身に恒等関数が適用されて、結果、何も変化していません。これでOK。

ではObservableの場合はというと

恒等関数を自分で書いてmapしてやれば良いですね。

恒等関数は、x => xでOKです。xを受け取ってxを返す、という意味です。

const x$ : Observable<number> = of(1,2,3);//初期値を作って
const y$ : Observable<number> = x$.map(x => x);//恒等関数をmap
y$.subscribe(x => console.log(x));

インポート文は省略しました。出力はこうなります。

1
2
3

与えた1,2,3がそのまま出力されているということで、これで良いです。

第二法則

ファンクター則の二つ目は、「関数の合成とファンクターの適用、どちらを先にやっても結果は同じ」というものです。

関数の合成とは、h(x) = g(f(x))みたいに、関数を複数回適用する新しい関数を作ることです。

Fでファンクターを表し、.で関数合成を表すことにすると、

F(g.f) = (Fg).(Ff)

ってことです。意味わかります…?見た目だけで言うと分配法則っぽいですね。

Fのかわりにfmapで表記すると

fmap (g.f) = (fmap g).(fmap f)

となります。

まあ例で見ていきましょう。今回はMaybeは置いといて、リストの場合で確認します。

f :: Int -> String
f n = (show n) :: String -- fは、数値を文字列に変換

g :: String -> String
g str = str++"!!" -- gは、文字列の最後に!!を付ける

main = do
  print $ fmap (g.f) [10,11,12] -- 合成を先にやる
  print $ ((fmap g).(fmap f)) [10,11,12] --fmapを先にやる

こいつをrunghcで実行してやると、出力はこうなります

["10!!","11!!","12!!"]
["10!!","11!!","12!!"]

同じものが出ましたね。これで良いわけです。

ではObservableではどうかというと

function f(x: number): string {
  return String(x);
}// fは数値を文字列に変換
function g(y: string): string {
  return y+"!!";
}// gは文字列の後ろに!!を付ける

const x$ :Observable<number> = of(10,11,12);//初期値を作る

const z1$ :Observable<string> = x$.map(x => g(f(x)));//合成が先
z1$.subscribe(x=>console.log(x))//結果出力

const z2$ : Observable<string> = x$.map(x=>f(x)).map(x=>g(x));//mapが先
z2$.subscribe(x=>console.log(x))//結果出力

出力はこうなります。

10!!
11!!
12!!
10!!
11!!
12!!

同じものが2回出てるので、OK。

これで、Observableがファンクターであることが確認できました。長かった…。

ここからさらに、モナドであることの確認をするってんだから、驚きですね!!!!

モナド則の確認

やっとモナドです。ということで、モナド則を確認してみましょうか。

モナド則とは何か、についてはこちらの記事がわかりやすいと思います。
https://qiita.com/7shi/items/547b6137d7a3c482fe68

用語整理

returnバインドをさらっと説明します。

return

値1個をモナド箱に入れる関数です。

モナド箱とは、これまでファンクター箱と呼んできたものと同じです。

いろんなプログラミング言語ではreturnは関数の最終的な出力を返す文として使いますが、それとは全然関係ないです。

Haskellの例だと

return 3 :: Maybe Int -- Just 3
return 3 :: [] Int -- [3]

となります。それぞれ「Maybe箱に3が入ったもの」「リスト箱に3が入ったもの」です。

returnpureと書いている記事や本やコードもありますが、まったく同じ意味なので大丈夫です。

Observableの場合は、ofに一個だけ変数を与える場合がこれにあたります。

昔は、変数一つしか受け付けないreturnとかjustというものがあったみたいなんですが、現行バージョンでは見つからなかったです(執筆日2018年3月20日、npm rxjs --version 5.6.0)

const x$ : Observable<number> = of(3);
x$.subscribe(x => console.log(x)); // 3

x$の時点で、「Observable箱に3が入ったもの」が出来てます。出力する時に中身だけ出してるので、出力は「3」になります。

バインド

モナド箱の中身を取り出して、それを「モナド箱に入っていない値をとって、何か処理をして、モナド箱に入れて返す関数」に与えることです。

わかりにくいですね。例を見ましょう。

xを受け取って、Just (x+1)を返す関数」を用意します。
\x -> Just (x+1)でOKです。

これはまさに「モナド箱に入っていない値をとって、何か処理をして、モナド箱に入れて返す関数」になっています。

普通はこの関数に数値を与えますが、バインドの場合は「モナド箱に入った数値」を与えます。その中身を取り出して関数に渡してくれるのがバインドなのです。

コードはこうなります

(Just 3) >>= (\x -> Just (x+1)) -- Just 4

この>>=がバインドです。

リストの例も見てみましょう。

xを受け取って、[1+x,10*x]を返す関数」に、リストをバインドしてみます。

[4,5,6] >>= (\x -> [1+x,10*x]) -- [5,40,6,50,7,60]

あれ、なんか出力リストの要素が6個もありますね。

これでいいんです。バインド君は、[4,5,6]から順に値を取り出しては、後ろの関数に適用してるわけです。

ここでこう思った人がいるかもしれません。

「順に取り出して関数に渡してるなら、出力は[[5,40],[6,50],[7,60]]にはならないの?」と。

これがならないんですね。それだと出力が二重リストになって、モナド箱が二重になってしまうので、バインド君はわざわざ内側の[]は外して、全体を均一にならしています。
これで正しい動きです。

では今度はObservableの例を見てみましょう。

Observableで>>=にあたる動きをするのはmergeMapという奴です。

以前はflatMapという名前だったそうですが、現行バージョンではmergeMapというそうです。(執筆日2018年3月20日、npm rxjs --version 5.6.0)

ということでコード。

xを受け取って、of(1+x,10*x)で出来るObservableを返す関数」に、Observableを渡してみます。

const y$ = of(4,5,6).mergeMap(x=>of(1+x,10*x));
y$.subscribe(x=>console.log(x));

省略してますが、mergeMapもインポートする必要があります。

で、出力は以下。

5
40
6
50
7
60

リストの時と同じですね!!

今回は、Haskellが値 >>= 関数という順なのに対してTypeScriptが値.mergeMap(関数)で、順番が同じなので、コードの見た目も似ます。

Haskellと、TypeScriptの「一行目のイコールより右」を比べてみると

[4,5,6] >>= (\x -> [1+x,10*x])
of(4,5,6).mergeMap(x => of(1+x,10*x));

ね。同じことをやってるのがよくわかりますね。

TypeScriptの方の「一行目のイコールから左」と「二行目」は、単に結果を出力するために書いてあっただけなので、そこは無視して比較してます。

ということで、これが「バインド」です。用語説明終わり。

左恒等性

ここから、モナド則を一つずつ確認していきます。

モナド則は3つあります。

まずは一つ目、「左恒等性」から見ていきます。

その意味は、

「returnしたものを関数fにバインドすると、ただのfと同じ」

です。

returnは「モナド箱に入れる」で、バインドは「モナド箱の中身を取り出して関数に渡す」だから、なんか当たり前そうな気もしますね。

例で見ていきましょう。Maybeモナドはまた置いといていいかな。

リスト

先ほど使った「xを受け取って[1+x,10*x]を返す関数」をまた使いましょう。

まずは普通にただ「3」を渡してみます。作った関数の後ろに3と書けばOK。

(\x -> [1+x,10*x]) 3 --- [4,30]

次は、3をreturnしたものをバインドしてみましょう。

(return 3) >>= (\x -> [1+x,10*x]) --- [4,30]

まったく同じ出力になりました。一回箱にいれて、また出してるだけなので、そりゃあそう。

Observable

同じようにやっていきましょう。

まずは普通に関数に数値を渡す。作った関数の後ろに(3)と書けばOKです。

const a : Observable<number> = (x=>of(1+x,10*x))(3);//作った関数に3を渡す
a.subscribe(x=>console.log(x));//出力

出力

4
30

続いて、returnしてバインドする。まあこの場合は、ofしてmergeMapするわけだけど。同じことです。

const y$ : Observable<number> = of(3).mergeMap(x=>of(1+x,10*x));
y$.subscribe(x => console.log(x));

出力

4
30

OK!同じ出力がでましたね!

HaskellとTypeScript(一行目のイコールより右)の類似性もさらっと見ておきましょう。

普通に関数↓

(\x -> [1+x,10*x]) 3
(x => of(1+x,10*x))(3);

returnしてバインド↓

(return 3) >>= (\x -> [1+x,10*x])
of(3).mergeMap(x => of(1+x,10*x));

右恒等性

二つ目のモナド則いきます。「右恒等性」です。

「モナド値をreturnにバインドすると元に戻る」という内容です。

今度は、箱に入ってるものを、箱から出して、「値を箱に入れる関数」に渡すわけです。

そしたら、また箱に入りますね。

確認していきましょう。

まずはHaskellのリスト。

[3,2,1] >>= return -- [3,2,1]

えらく短いですが、これで終わりです。

Observable行きましょう。

const a$ = of(3,2,1).mergeMap(x => of(x));
a$.subscribe(x => console.log(x));

出力

3
2
1

はい。良いでしょう。中身を取り出して出力してるので、本当に箱に入ってくれたのか一瞬わかりにくいですが、大丈夫です。

コードの類似性は少し崩れましたね。

[3,2,1] >>= return
of(3,2,1).mergeMap(x => of(x));

returnと同じように「一つだけ引数を受け取ってモナド箱に入れる関数」を書くためにx => of(x)という書き方をしたので、ちょっとだけ似なくなりました。

結合法則

モナド則の3つ目は、数学や算数ではおなじみの結合法則です。

(3+4)+5 = 3+(4+5)みたいな奴です。

ですが、あんまりそう見えないかもしれません。

キレイに「おお結合法則だ!」っていう見え方になるように書き換えることも可能っちゃ可能ですが、普通に書くとそうなりません。

バインドって、複数並べると
値 >>= 関数 >>= 関数という形になりますが、後ろ半分にカッコをつけると

値 >>= (関数 >>= 関数)となって、このカッコ内は型が合わないので計算できないんですね。

(値 >>= 関数) >>= 関数なら、計算できるんですけども。

じゃあ結局何が成立すればいいのか。

説明します。

「モナド箱に入った値」をm
「箱に入ってない値を受け取り、何か処理して、モナド箱に入れて返す関数」をf,gとします。

成立して欲しい式はこうです。

(m >>= f) >>= g = m >>= (\x -> (f x >>= g))

う〜んわかりにくい。

日本語で説明してみると…

「mの中身をfに渡し、その結果から中身を取り出してgに渡したその結果」

「mの中身を『何かを受け取ってfに渡し、その結果から中身を取り出してgに渡した結果』を返す関数に渡したその結果」
が、等しい。

う〜んわかりにい。がんばればわかるかもしれない。でもわかりにくい。

ということで、今回も「コードの類似性」でもって「同じことが成り立つなあ」という確認をしていくことにしましょう。

リスト

f :: Int -> [] Int
f n = [n+1,n*10] -- fを定義

g :: Int -> [] Int
g n = [n-1, n*20] -- gを定義

main = do
  print $ ([0,1] >>= f) >>= g -- 左を先に計算
  print $ [0,1] >>= (\x -> (f x >>= g)) -- 右を先に計算

runghcして、出力は以下。

[0,20,-1,0,1,40,9,200]
[0,20,-1,0,1,40,9,200]

同じ出力が得られましたね!

Observable

function f(n:number):Observable<number>{
  return of(n+1,n*10);
}//fを定義
function g(n:number):Observable<number>{
  return of(n-1,n*20);
}//gを定義

const a$ = (of(0,1).mergeMap(f)).mergeMap(g);//左を先に計算
const b$ = of(0,1).mergeMap(x => f(x).mergeMap(g));//右を先に計算

a$.subscribe(x => console.log(x));//出力
b$.subscribe(x => console.log(x));//出力

出力

0
20
-1
0
1
40
9
200
0
20
-1
0
1
40
9
200

えらく縦長ですが、同じ出力が2回得られてますし、内容はリストの時と同じです!OK!!!

主要な所を並べて比較してみます。

([0,1] >>= f) >>= g -- 左を先に計算
[0,1] >>= (\x -> (f x >>= g)) -- 右を先に計算
(of(0,1).mergeMap(f)).mergeMap(g);//左を先に計算
of(0,1).mergeMap(x => f(x).mergeMap(g));//右を先に計算

>>=.mergeMap[]of()の対応に注意して見てみると、そっくりなのがわかりますね。

これで、全てのモナド則が確認できました!

モナド則まで確認できたので、RxJSのObservableはモナドであることが、十分納得できますね!!!!お疲れ様でした!!!

次回予告

次回は、Observableのモナドらしい側面について調べたり書いたりしていきたいと思います。

現状考えているのは、

  • do記法に相当する書き方について(あるのかどうかも知らないのでこれから調べる)
  • モナドはよく「文脈つきの値」とか言われるけども、Observableはどういう文脈付きの値として解釈できるのか(もちろん知らないのでこれから調べる)

あたりです。それでは。

44
28
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
28