{ 関数型弱者のためのイメージ化fp-ts : [ TypeScriptで関数型もどき ] }
19歳です。TypeScript歴は2週間くらいです。関数型とは無縁のプログラミングを1年半やってきました。
クラスという入れ物はとても便利だが、視点がマクロすぎる気がする。オブジェクト指向デザインパターンはクラスの組み合わせ方の良きカタログではあるが、クラスの中の各メソッドがそもそも綺麗に書けない問題が浮上。
何をどんな変数として保持するか、流れをどう接続するか、そういうもっとミクロな視点でコードデザインを研究したくて、関数型という流派を覗き見しています。
ちなみにfp-tsに出会うまで
TypeScriptで関数型プログラミングを導入する選択肢としては、
- lodash(Underscore.js)
- Ramda
- fp-ts
あたりが主流な気がしている。個人の見解だが上から易しい順。
そんなに見知っているわけでもないけど印象をまとめておく。
lodash(難易度★☆☆)
for文とか駆使してゴリゴリ書いていた処理をサクッと一言でまとめられるような関数が集まっている。
プログラムを手順書から宣言書に濃縮できる感じ。どこで何をやっているのかが抜群にわかりやすくなる。
関数型プログラミングの知識がなくても普通に使えるし便利。
Ramda(難易度★★☆)
あんまり使ったことないけど、関数型特有の単語がメソッド名にバンバン出てくるあたり、難易度はちょっと上がる印象。
lodashは普通のプログラムを宣言型に改良するレベルでも使えるが、Ramdaは関数型プログラムを書く上でのヘルパーツールという域なんじゃないだろうか。使いこなせば便利なんだろうなあ。
そしてfp-tsと出会う
とはいえ私は関数型を書きたいというよりはデザインの参考にしたいな〜という目的だったので、fp-tsがちょうどよかった。
言っておくがlodashやramdaと比べ物にならないくらい難しい。型定義のみのドキュメントなのでそもそも何言ってるかわかんないしわからないとちょこっと使い始めることすらできない。
fp-tsはコードデザインの宝石箱
上記2冊の本を読んでも、fp-tsのドキュメントにはまだまだ未知のWordが大量に出てくる。
ScalaとかHaskellとかもっと勉強すれば厳密な扱い方がわかるのだろう。
でもそこまで関数型を極めるつもりがなくても、
「こんな形のデータをこう名づけましょう。するとこの子はこんなことができますよ。」
という画集としてふんわり捉えるだけでも、十分にコード設計の参考にはなる。今はそんな感じで戯れている。これから少しずつね。
Store
英単語解説
そもそもStoreってなんだろうねって話。
一般的には蓄えることを意味する。アセンブラではレジスターの値をメモリー領域に格納することを指す。プログラミングでは、格納の意味でよく使われ、「データを変数にストアする」といった表現をする。また、レジスターやメモリーのデータを一時的に退避させる記憶領域をスタックという。たとえば、一時的に値を記憶させることを「スタックにストアする」という。 by コトバンク
ややこしいのが、Storeという単語はデータの保存期間に問わず使われるということ。
- ReduxのStore -> 永続的なデータ保存
- アセンブラとか -> 一時的なデータ保存
どうやらfp-tsのStoreは後者に近いらしい。modelの定義を見ればそんな気がする。↓
model
- pos ....現在調べている場所
- peek ....現在調べている場所を覗いた結果
interface Store<S, A> {
readonly peek: (s: S) => A
readonly pos: S
}
peekはどうやら「覗き見」とかそんな意味らしく。イメージとしてはこう読めるのかも。あくまでイメージね。
実例とか考えてみる
今はちょっとした言語の構文解析器を書いているから、その中ではこんな感じで使えそう。(実用性皆無)
import * as STORE from 'fp-ts/Store'
interface Token {
type: string
value: string
}
type ParserPointer = Store<number, Token>
interface ParserPointer {
// 現在の位置のトークンを取得
readonly peek: (pos: number) => Token
// 現在どこまで読んだか
readonly pos: number
}
そこはかとないTS初心者感。
…てか定義そのまんまじゃん。想像力なくてごめんね。
メリット
まさに一時退避の役割を担ってくれることで、tmp
とかpos
とかcount
とかいう悪名高い一時変数を用意する必要がなくなるわけだ。
一時変数をその辺に単体で置いておくのではなく、オブジェクトとして包んであげることで、グローバル変数を撲滅できる。そして役割が明確になるし、変数間の依存関係もわかりやすくなる。
peeks
現在の位置に依存する位置から値を抽出
peeks<S>(f: Endomorphism<S>): <A>(wa: Store<S, A>) => A
peekの相対位置バージョン。
(fp-ts特有の命名規則かは知らないが、相対位置バージョンになると末尾にsがつくっぽい)
seek
指定した位置にフォーカスを再配置
seek<S>(s: S): <A>(wa: Store<S, A>) => Store<S, A>
seeks
現在の位置からの相対位置にフォーカスを再配置
seeks<S>(f: Endomorphism<S>): <A>(wa: Store<S, A>) => Store<S, A>
Traced
model
- P ....位置
- A ....位置から得られた情報
interface Traced<P, A> {
(p: P): A
}
Tracedは「辿った」「突き止めた」という事実だったり、その痕跡。
Storeとの違い
Storeの直後に書いているのはわざとだ。だってイメージが似ている気がするから。
でも関心が微妙に違う。
- Storeは位置の移動に関心がある(移動するために現在地を記録している)
- Tracedは今いる位置にしか関心がない
だって突き止めたんだもの。Tracedの場合、もう目的地には着いているのだから移動する必要がない。
censor
現在の位置に関数適用
censor<P>(f: (p: P) => P): <A>(wa: Traced<P, A>) => Traced<P, A>
listen
現在の位置を取得
listen<P, A>(wa: Traced<P, A>): Traced<P, [A, P]>
listens
現在の位置に依存する値を取得
listens<P, B>(f: (p: P) => B): <A>(wa: Traced<P, A>) => Traced<P, [A, B]>
track
現在の位置に依存する相対値で値を取得
tracks<P, A>(M: Monoid<P>, f: (a: A) => P): (wa: Traced<P, A>) => A
State
model
ある状態から次の状態を返す
- S ....状態
- A ....状態変化の結果
例)計測待ち => [経過時間, 計測中]
interface State<S, A> {
(s: S): [A, S]
}
StoreとTracedから意味を探る
これはあくまでも想像ね。
interface Store<P, A> {
// 現在調べている場所を覗いた結果
readonly peek: (p: P) => A
// 現在調べている場所
readonly pos: P
}
interface Traced<P, A> {
// Pは位置
// Aは位置から得られた情報
(p: P): A // (結局はこれ、Storeのpeekと同じ)
}
StoreもTracedも、結局は現在の情報しか保有できないのだ。
でも位置は変化する。
「今ここにいる」という状態と、「一歩進んだ」という状態は明らかに違う。
そして、状態というラベリングをすれば、各状態に具体的な値が伴う。
覗いた結果(事実)は位置(状態)ごとに異なるからだ。
Storeは状態
を記述し、Tracedは状態 -> 結果
という関係を記述する。
そこに状態1 -> 状態2
という関係の表現も書き加えたものが、Stateなんじゃないかって話。
interface State<P, A> {
// 位置P1 => [結果A2, 位置P2]
(p: P): [A, P]
}
状態の中に状態を包んでいるのはちょっと変な感じがするけどね。
to be continued ...
しかしこれが私のような凡人が何もわからん世界で唯一できるアプローチだった。
…さてコードを書いてこよう。
そのうちまた書き足します
EOF
Discussion