2020の@inamiyさんの発表では、すごく雑に言って関数型なエッセンス(副作用の抽象化やLens/Prismやモナドなど)を含むようなライブラリーとして3つが挙げられた。Archはその3つのライブラリーの中の1つであり、著者が3つともを少し使ってみた限りでは現時点で一番使い勝手が気にいったので入門記事を書くこととした。Archは2020年5月から更新がないので、この記事を読む・読まないに関係なく今から採用するのはリスクがあるかもしれない。Archの方針や設計思想は参考になることがあると思う。UIという概念や、圏論といったこれらのUIライブラリーの背景にある性質にまで踏み込んで解説しており、筆者のようなSwiftはそこまで書かないが関数型プログラミングに多少興味がある人が強く引き付けられた。一方でこの圏論のような抽象的構造は(筆者のように多少は関数型プログラミングに教養があっても)非常に難しいというか、抽象的なのでそれを理解しているからといって具体的なところ(UIを実装するとか)ですぐ役に立つのかどうか?が分かりづらいと思う。したがってこの記事では圏論上の対応などは可能な限り言及を避けて、まずはLens/Prismを利用したUIの状態操作が実際上の役に立つのか?という点をなるべく強調して説明していきたいと思う。この記事を読んで分からないことや改善点、誤りなどを見つけたら気軽にコメントなどで教えてほしい。今回つくるもの今回は下記の画像のように2つのスライダーを動かすと表示が変化するという極めてシンプルなUIを作成する。今回つくるアプリ2つのスライダーはそれぞれコーヒー豆の重量と水の重さを表現している[1]。UIでそのまま書いてOKと思うが、簡単のために小さい例でやってみることにする。全体のソースコードは下記のGitHubリポジトリーにある。UIを素直に利用した場合について少し理解しておく必要があると感じたので、このあたりから説明したい。UIでは次のようなObservableObjectに準拠し、ミュータブル(var)を持つようなクラス(ViewModel)をまず作っておく。0.0}そして、Viewにこのミュータブルを渡して、たとえばスライダーをタップしてときに数値が更新されるようにする。}}こうしてViewModelとView(ContentView)でデータ(この例ではcoffeeBeansWeight)を操作できるようになった。例では単にViewからしかデータを変更できないので、たとえばViewModel側に次のようなdidSetを入れることで、スライダーが変更されたときに他の処理を実行することもできる。}}さて、このようにすれば下記の(1)〜(3)のループによってユーザーの入力へのレスポンスとなる適切なUIが次々と生成されていくはずである。UIをそのまま使うだけであっても十分にUIを作れると思うが、一方でプログラムが複雑になると、次のような理由で保守性が低下する可能性があると考えている。ミュータブルな状態をViewModelとViewの両方から変更することになり、十分に状態の量が多いアプリケーションであれば、プログラマーが予期しない状態が生じてしまい、かつそれがどこでどうやって発生したのかを特定するのが困難となりそうであるViewModelで起動される関数はdidSetの中で呼ばれることから、返り値が利用されないので事実上返り値の型はVoidに固定されることになる。Int?のような引数だけを使って結果が確定する関数gの方が単体テストしやすそうだというのはある程度一般的だと考えている筆者の予想になってしまうが、async/awaitといった平行・並列の強化によってミュータブル状態の変更がよりシビアになるのではないかと思う。Reduxの経験はほとんどないが過去にjQueryなどでDOMを状態としたプログラムを書いていた経験やサーバーサイドプログラミングの経験から言っても、このようなミュータブルの利用は長期間メンテナンスするということに向いていないと思っている。Reactのアプローチ!筆者は普段書いている言語がScalaなので、本当はScalaの似たような例を持ってこようかと思ったが、Scalaも書けるSwiftエンジニアというのが少ないと思ったのでそれよりはマシと思ってReactのコード例を載せている。いうことで筆者がバリバリにReactを使っているというわけではないので注意して欲しいと思う。MVVMとは違ったアプローチとしてReactを紹介する。下記のコードは@kazuma1989さんの記事からReactのコードを引用したものである。)}これはカウントアップボタンを押すと表示された今のカウントが1つずつ増加していくプログラムとなっている。UI風に書くと次のようになる。})}今、useState(0)により初期値として0となるような状態countと、それをアップデートするための関数setCountが与えられた。したがってuseStateはSwiftで次のような型が付くはずである。Void)ジェネリクスで抽象化したが、引数initとして0を与えたため、上記の例は型を明示するとuseState<Int>(0)ということになる。Aという関数になっていることがポイントである。setCountは現在の値を引数として次の状態を得るような関数を引数に取る高階関数となっている。一方でReactのコードではアップデートがこのように古い値から新しい値を新たに生成する方法となった。これによって次のような良いことがある。}にテストを与えて適切なアップデートとなっているか?といったことを検査するのも状態を代入で更新しているコードよりは容易となりそうである更新の際にアクセスする変数が限定されているため他の実装によって挙動が変わるといったこともないReactではこれをこのまま使うためには他にも考えることがあるということでReduxのようなより進んだ仕組みが導入された。ArchはReact/Reduxとは別のアプローチでこのような恩恵を受け取ろうとしている。Archがどのような抽象化をしているのかについて述べていく。LensReactではSwift風に下記のような返り値の型が付くような関数useStateを紹介した。AとなるようなGetterであり、右は現在の状態を使って次の状態を設定するSetterと考えて次のようにラベルをつけると分かりやすくなる。Void)このようにタプルのまま利用することもできるが、わかりやすさのためにこのタプルに名前をつけたものがLensである[2]。Aなため、次のようになる。S}ただし、ここでは型パラメーターが2つ存在している。型パラメーターAは実際に取り扱いたい値の型として、型パラメーターSはいったい何を意味しているかというと、これは型と型の包含関係を意図している。これを説明するために次のような構造体を定義する。FirstBoiledWaterAmountState}このような2つの構造体をフィールドに持つ構造体ContentStateがあるとする。これは図のように依存関係がある。https://www.overleaf.com/project/6037ac9ac6868c3002e38e09そして、今このContentStateとフィールドのCoffeeBeansWeightStateとFirstBoiledWaterAmountStateの間には次のようなことが言える。ContentStateはフィールドcoffeeBeansWeightStateとしてCoffeeBeansWeightState型の値を持つので、ContentStateな値からCoffeeBeansWeightStateとFirstBoiledWaterAmountStateな値が取りだせる(Get)coffeeBeansWeightStateまたはfirstBoiledWaterAmountStateへ変更があった場合には、ContentStateな値への適切な変更が必要である(Set)useStateはあくまでも1つの型をSetしたりGetするのみであったが、多くの場合データ構造はこのContentStateのように他のデータ構造をフィールドに持っている。\texttt{CoffeeBeansWeightState}の依存を示している。CoffeeBeansWeightState>を具体的に実装すると次のようになる。FirstBoiledWaterAmountState>も同様に書くことができる。useStateのような1つの型に対する更新処理ではなくて、このような2つ型の間にある関係を記述することによって次のようなメリットがある。UIでは全ての画面を更新せずに狙った部分だけを適切に差分更新することでUXが良くなると考えられる。Archの改造次第では(アプリのコードを一切いじることなく)差分更新になる可能性があるというような話となる構造体ContentStateをフィールドに持つような型が今後表れた場合に、既存のLensを再利用することができるもし型ごとに更新の木構造を記述しなければならないとすると、同じようなコードが増えてしまうこのような差分更新のテクニックはReactにも存在する。ArchではGetter/Setterに差分更新に将来使えるような情報も組み込んだというふうに考えてよいと思う。PrismさてGetter/Setterを得たので、次は「どのような時にSetterを起動するか?」というような処理を記述する必要がある。Lensに近い概念としてReactのuseStateがあったが、ReactにあるuseReducerに近い概念としてPrismがある。useReducerはSwift風に書くと次のようなインターフェースを持つ関数である。Void)これまでのLensでは取得や変更の方法を与えることはできたが、どういうときにどう変更すればよいかというのはLensの範囲外である。このあたりはReactとは別の抽象化となっているので、まずは概念を図で整理する[3]。UIのLens/Prismを配置して、それらが下側にあるuseStateやuseReducerとどう対応づくのかを示している。Prismに関してはLensのように、Reactにある「機能を単純に型の間にある関係」という点で抽象化したというものではなく、Reactのよく知られた関数でPrismに相当するものはないと思う。直感的な説明をすると「useReducerからuseStateに相当する機能を取り去った」ような機能となっている。Archではこのように1からuseReducerに相当する機能を書くのではなくて、さきほど実装した型間のGetter/SetterとなるLensとこれから紹介するPrismの2つからほぼ自動的にuseReducerを作るというアプローチが採用されている。この「どういう時に状態を変更するか」というアクションをここでは定義していく。このときLensのときと同様にアクションにも木のような依存関係を定義できる。decrease(Double)}ほぼLensのときと同様ではあるが、たとえばこのように作ることができる。あとはこれらの間の関係をPrismのextensionとして与えるが、ここでPrism.swiftはこのようなコンストラクターを持っている。}}2つの引数はそれぞれ次のような意味となる。CoffeeBeansWeightState>と同様に画面全体の変更とコーヒー豆の重量状態の関係からいって、次のようになる。)}StateDispatcherここまでで作ってきたLens/Prismを利用して、さきほどの図のStateDispatcherをつくることができる。いままでのLens/Prismは、たしかにSwiftコードで書いたもののほとんど複数のデータ構造間の性質(上位のデータが変更されたら、下位のデータを変更するなど)を記述していた感が強く、実際に状態をどう変更するか?を直接記述していたというと違うかもしれない。アクションを受けとったときに一体なにをするか?そういった具体的な挙動を記述する場所がStateDispatcherとなり、これがuseReducerに対応している。具体的にコーヒー豆の重量(CoffeeBeansWeightState)を変更するアクション(enum)であるCoffeeBeansWeightInputを受けとった場合のCoffeeBeansWeightDispatcherは次のようになる。}}あまりそういう人は少ないかもしれないが、これはインタープリターのように見えるかもしれない。CoffeeBeansWeightInputがプログラム言語のASTであり、coffeeBeansWeightDispatcherにそれぞれのASTに対する振る舞いが定義されていて、最終的にそのプログラムの出力(状態)としてCoffeeBeansWeightStateが出力されるといった感じである。ここまで来たらやることは明らかかもしれないが、Lens/Prismと同様にこれをContentState側へ次のようにwiden[4]を使って反映させる必要がある。)水の重量に対するStateDispatcherも同様に作れ[5]ば画面全体を制御するContentDispatcherの完成となる。Archがどのように状態を管理しているのかということが明らかとなったと思う。Archだとここまで大変ということで、利用することのメリットが謎になったかもしれない。ここでは筆者が思うメリットをあげてみる。状態と振る舞いの分離StateDispatcherの節で述べたとおり、これらはUIに対する次のようなプログラム言語と処理系となっている。インタープリターが操作した結果このようになっていると、アクションに対するUIの振る舞いを変更したい場合に修正箇所を局所的にできる。同じアクションに対して複数のインタープリター(StateDispatcher)を定義できるため、たとえばiOSにエディターのVimを実装したいとなったとき、同じキーイベント(アクション)に対してモードによっては別の振る舞いをしたくなる。現在の状態をキー入力のたびにチェックして場合わけするという手もあるが、このようにアクションとインタープリターが分離されていれば、モードの変更と同時にインタープリターを切り替えるという方法で対処できる。状態とUIの分離この記事ではあくまでもLens/Prismなどを使った「状態操作」についてしか述べていない。したがって、ここからiOSの画面といった本物のUIに反映しなければならない。その部分が@inamiyさんの発表のモナドやコモナドなどを使うところになる。ここまで見てきたように、状態とアクションによってどう振る舞うか?という部分を実際のUIとは全く関係なくプログラミングしていることになる。実際上は本物のUIの都合で状態が追加されるとか、ここはタップできるからこういうアクションが必要だというような相互の関係があるとは思うが、たとえばここまでのLens/PrismやStateDispatcherはUIの知識が全くなくてもプログラミングができるということになる。状態とアクションから次にどうなるか振る舞いを定義する状態から適切なUIを生成するこのように2つの部門に分けることができると思われる。(1)をドメインエキスパートのようなエンジニアがプログラムして、そして(2)をデザイナーやUIエンジニアが担当するといった分業が達成できる可能性がある。まとめ本当はComonadを利用したUIについても説明したかったが、Lens/Prismの解説で文章量が多くなってしまったので、いったんこの記事はここまでにすることにした。この記事で例として出したReactが出現したから十分に時間が経った現在であっても、useReducerなどはまだ十分に難解であるから仕方ないとは思う。気合が残っていたら次はこのようにして作った状態をどうやって本物のUIへと反映させるのか?の部分を解説したいと思う。謝辞ドラフト版の記事を読んで感想をくれた@7_6_さんと@kyu_uriさんに感謝したい。ほうが大抵勝っている脚注これはコーヒーを抽出するときのパラメーターであるが、この記事を読むうえではコーヒーに関する知識は「コーヒー豆にお湯を注ぐとコーヒーが出る」くらい分かっていれば十分である。型パラメーターがたくさんあることについてはあとで解説する。↩︎StateDispatcherについてはまだ説明していないので、分からなくても大丈夫である。余談となるがScalaの関数型ライブラリーにおいてwidenは、型AとそのサブタイプであるBがあるときに、BをAとするアップキャストのような操作に対して慣用的に与えられる名前である。詳細はこちらを参照するとよい。↩︎このようなASTに対する振る舞いのことを意味論と言うこともある。yyu@mental.pokerバッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。Proよくある質問LinksX(Twitter)GitHubメディアキットLegal利用規約プライバシーポリシ。