圏論の視点で計算科学(プログラミング)を見よう

このページは[url=https://www.geogebra.org/m/ddm5798j]マス旅[/url]の一部です。[br][br]今まで、圏論をターゲットにして、キーワードを軸にしていろんなツアーをやってきた。[br][br]今回からは、圏論のメガネをかけて、いろんなビルをじっくり見つめてみよう。[br]つまり、圏論はわき役にして、主役を立てるということです。[br][br]今回の主役は計算科学(プログラミング)です。[br][br][url=https://www.geogebra.org/m/ddm5798j#material/zffeqkgp]「自然変換」によって、圏は3階建てになる[/url]で見たように、計算科学のビルはこうなってたね。[br][br][*][b]1階:圏 C(点a,b,...と矢f,g,...)[/b][/*][*] 点が型(Type)で、つなぐ矢が関数(Function)が住む。[/*][*][b]2階:ファンクター F: C→D (圏の間の構造を保つ写像) [/b][/*][*] 型をコンテナに入れて持ち上げて、矢もこわさずに持ち上げる力持ち。[/*][*] このコンテナがファンクターという住人だった。[/*][*][b]3階:自然変換 Nat: F→G(ファンクターの間の構造を保つ写像)[/b][/*][*] 公平にファンクターたちを見下ろして、通訳したり、結婚相談を自然に行うマネージャーがいる。[/*][*][br][/*]さあ、このビルで起きることをくわしく観察してみよう。[br][br]観察の視点は3つだ。[br][br]○ 中身ではなく、見える振る舞いから決めるという「圏論」の視点[br][br]○ 一番無駄のない作りにするという「普遍性」の視点[br][br]○ 逆の個性(Xとco-X)が表裏一体で共存する「対称性」の視点[br][br]さあ、出かけよう。
1.各階を観察しよう。
[b][size=150]<1F圏>[/size][/b][br]プログラミングの圏のメンバーは型とその変換関数でした。[br][br][color=#0000ff]「圏論」の視点[/color][br]オブジェクト指向プログラミング([b]OOP[/b])には、[br]クラス型、[b]インターフェース[/b]型というのがあったね。[br]データの中身を外から隠して、外側でやれる操作と外側から見える振る舞いだけで[br]安全で柔軟なデータ活用ができるという思想だった。これって圏論そのものだね。[br]そして、「[b]米田の補題[/b]」は、[br]「君の内部構造なんて興味ないよ。君の他人への振る舞いの情報だけで、君の正体(型)は決まるからね」ということになる。[br][br][br][b][size=150]<2Fファンクター>[/size][/b][br]型Tのものに文脈をつけてくるむコンテナがファンクターだった。[br][br][color=#0000ff]「圏論」の視点[/color][br][b]List も Option も Future [/b]も、中身のロジックは別々だ。[br]でも、外から見た振る舞いは、「中の要素に関数を適用して移す([b][color=#0000ff][size=200]map ができる[/size][/color][/b])」と[br]いう点では同じ仲間だね。[br][br][color=#0000ff]「対称性」の視点[/color][br]計算結果の貯蔵庫として常用されるのがリストだった。これもファンクターだ。[br]生のデータではなくそれを格納する[b]List(律儀にコツコツため込む君)[/b]に、[br]放り込んだり、取り出したりできたね。[br]でもこれらはすべて、その都度、過去を見ている。[br]だから、貯蔵庫のスペースの確保とデータ評価が必要になる。[br]データを溜め込むファンクター(Listなど)は過去の計算結果の集まり([b]データ[/b])。[br]それと対照的なのが[b]遅延評価(いわれた初めて動き出すLazy君)[/b]という、[br]未来に向けたデータの持ち方がったね。[br]要求されたら計算するよ。という方式だから、無限の個数のある整数全体?も計算対象にできた。[br]くわしくは[url=https://www.geogebra.org/m/twxxx3yq#material/nwyj8w4m]こちら[/url]。[br]データを生み出すファンクター(generatorなど)は、[br]未来の要求に応じて値を吐き出すプロセス([b]コ・データ[/b])。[br][br][b][size=150]<3F自然変換>[/size][br][/b]ファンクターを自然につなぎます。[br][br][color=#0000ff]「普遍性」の視点[br][/color]Listの中のデータを、Optionalへと安全に写す共通処理(例えば、リストの先頭だけを安全に取り出す safe_head 関数)を考えよう。[br]この処理によって、中身のデータが int であれ string であれ、「リスト全体に関数を適用してから先頭を取る」のと「先頭を取ってからその関数を適用する」結果は必ず一致するので、[br][b][color=#0000ff][size=200]ジェネリクス(型多相性)の安全性[br][/size][/color][/b]が保証されます。自然変換があるおかげですね。[br][br][color=#0000ff]「対称性」の視点[/color][br] 「引数を2つ同時に受け取る関数[b] (A, B) -> C[/b]」と、「1つずつ受け取る関数[b] A -> B -> C[/b]」の間には、[br]完璧な1対1の対応([b]随伴関係[/b])を保証する自然変換があります。[br][b][size=200][color=#0000ff]カリー化(Currying)[/color][/size][/b]ができるのは自然変換のおかげですね。
2.(パック/アンパック)×(データ/コ・データ)
最近、[b]スプレッド構文[/b]や[b][color=#0000ff]アンパック演算子[/color][/b]など、[br][b]リスト化[/b]と反対の[b]コ・リスト化[/b]が日常化しているようですね。そこを深堀してみましょう。[br][br][b][size=150]<1Fでの使い方>[br][/size][/b]たとえば、Pythonではリストやタプル[b]変数名の先頭に「*」をつけるだけ[/b]で、ごっそり取り出せます。[br]# 箱のまま扱う[br]>>>numbers = (3, 4, 12)[br]>>>print(numbers)[br](3, 4, 12) [br]# アンパック(*)で箱を消し去る[br]>>>print(*numbers)[br]3 4 12 [br]これだけだと、何のありがたみもわかりませんね。[br]でも、関数の引数のデータ渡しで使うとスゴ~く便利です。[br]numbersが3次元ベクトルの成分だとして、そのベクトルの長さを出したいとしましょう。[br][b]def Len(x,y,z):[br] return (x**2+y**2+z**2)**0.5[br][/b][br]すぐ思いつくのは、[br][b][color=#0000ff][size=200]>>>Len(numbers[0],numbers[1],numbers[2])[br][/size][/color][/b]13.0[br]と答えがでます。[br]ここで、アンパック演算子の登場。[br][color=#0000ff][b][size=200]>>>Len(*numbers)[br][/size][/b][/color]たったこれだけです。[br]タプル型でもリスト型でも同じように使えますね。[br]辞書型の場合は**と2つになりますが、使い方は同じです。[br]べきみたいですが、先頭につけるので誤解はないですよね。[br]pointDic = {'x':3,'y':4,'z':12}[br]print(Len(**pointDic))[br][br][b][size=150]<2Fでの使い方>[/size][br]パックとアンパックが共存[/b]します。[br]list1 = [1, 2, 3][br]list2 = [4, 5, 6][br]# 2つのリストからデータを取り出し、1つの新リストへ平らにつなぎます。[br]>>> combined = [*list1, *list2] [br][1, 2, 3, 4, 5, 6][br]#辞書でもフラット化して要素を展開して、大きな辞書が作れますね。[br]>>> dict(**{'x': 1}, y=2, **{'z': 3})[br]{'x': 1, 'y': 2, 'z': 3}[br][br]アンパック演算子は、キーがかぶってないと、その順にただ展開されるだけです。[br]>>> {'x': 1, **{'y': 2}}[br]{'x': 1, 'y': 2}[br]しかし、[br]キーがかぶると、右(あと)が優先されるので、このロジック無傷の更新ができるわけです。[br]>>> {'x': 1, **{'x': 2}}[br]{'x': 2}[br]>>> {**{'x': 2}, 'x': 1}[br]{'x': 1}[br]そうすると、bigdataという10次元の点の座標が入った辞書があるとして、'x0'の座標だけ0にしたかったら、[br][b][color=#0000ff][size=200]newbig={**bigdata, 'x0':1}[br][/size][/color][/b]とするだけです。[br][br]この省エネのデータ更新方法は、TypeScriptではスプレッド構文として、嵐のように多用されます。[br]2辞書dとeの連結はd|eとかけることも使うと、辞書の更新がカンタンにかけますね。[br]telenum = {'sasaki': '09000900990', 'yamamoto': '09011231234', 'suzuki':'09056569898' }[br]tel2 = {**telenum | {'sasaki':'08012345678'}} #1キー更新[br]くわりくは[url=https://www.geogebra.org/m/twxxx3yq#material/wnzb7e2u]こちら[/url]。[br][br][size=150][b]<3Fでの使い方>[/b][br][/size]アンパックもジェネレータもファンクターでした。[br]これをつないで使うことで、面白ことができます。[br][br]要求されたら計算するという遅延評価(ジェネレータ)とリストのアンパックを使うのです。[br]すると、geogebraのスライダーのような仕組みがつくれますよ。[br][br]# 【コ・データ】GeoGebraのスライダー t のように、[b]要求に応じて動的に座標を吐き出すジェネレータ[/b][br]def cycloid_stream(steps=3):[br] import math[br] for i in range(steps):[br] t = i * (math.pi / 2)[br] # サイクロイドの媒介変数表示 (x(t), y(t))[br] yield (t - math.sin(t), 1 - math.cos(t))[br][br]# ジェネレータ(未来の計算ルール)を、[b]* で一気にメモリ上のリスト(確定した過去)へ展開する[/b][br]points = [*cycloid_stream()][br]print(points)[br]# [(0.0, 0.0), (0.5707..., 1.0), (3.1415..., 2.0)][br][br]ここまで読んで、もうお気づきの人もいるでしょう。[br]Pythonで関数を学ぶと、関数に渡す引数で、位置、パラメータ、可変長という区分があることを知りますね。そのときに、可変長データは*を先頭につけました。[br]*/**は関数定義のときはいくつもある要素を1つの変数にパッケージ化する形の定義です。[br]def pack_all(*args)のように引数の場所で[b]*args 、**kwargsとアンパック演算子を使って関数定義[/b]をします。[br]*/**は変数を使うときにつけると、1つの変数のパックをひらくことができました。[br]関数にpack_allに変数を放り込まれると、アンパックして参照されます。[br][b]同じ記号が「[color=#0000ff]双対の原理[/color]」で使われている[/b]ことに気づきますね。[br]
3.モナドとコ・モナド
[b][size=150]<モナド>[/size][/b][br][br]モナドは2Fのいろんな文脈のファンクターをつないでつかう自然変換です。[br]要するにファンクターパイプラインですね。[url=https://www.geogebra.org/m/twxxx3yq#material/huczjcdf]こちら[/url]にあるように、[br][br]from pymonad.maybe import Just, Nothing[br]# 各ステップ:結果をJustかNothingのカプセルに入れて返す[br]def extract_profile(d):[br] return Just(d["profile"]) if "profile" in d else Nothing[br][br]def extract_address(p):[br] return Just(p["address"]) if "address" in p and p["address"] is not None else Nothing[br][br]def extract_zip(a):[br] return Just(a[:3]) if len(a) >= 3 else Nothing[br][br][b]def get_zip_code_monad(raw_data):[br] return ([br] Just(raw_data) # Justでくるむ[br] .then(extract_profile) # プロフィール抽出[br] .then(extract_address) # 住所抽出[br] .then(extract_zip) # 郵便番号抽出[br] )[br][/b][br]この[br][b]get_zip_code_monad[br][/b]がモナドです。[br]ファンクターをくるんで1つにパックしているからです。[br][br]次のリスト内包表記もモナドですね。[br][br]# 1つの要素から複数の要素(リスト)を返す関数をチェーンし、結果を1つのリストに平坦化する[br]result = [y for x in [1, 2, 3] for y in [x, -x]][br]print(result)# 結果: [1, -1, 2, -2, 3, -3] (二重リスト[[1,-1], [2,-2], [3,-3]]が平坦化されている。)[br][br]モナドは「[b][color=#0000ff][size=150]1つにまとめる[/size][/color][/b]」作用がありますね。[br][br]○ [b]複数のデータを1つにパックします。[code]unit : A → M[A][/code](包む)[/b][br]○ [b]平坦にして1つの中に入れます。[/b][b][code]bind : M[A] → (A → M[B]) → M[B][/code](平坦化して連結)[/b][br][b][br]<コモナド>[/b][br][br][b]コ・モナドは名前の通り、モナドと双対です[/b]。[br][br]モナドを大量のデータで処理することで、エラーを起こさずに安全に処理をして、欠損データログを残すという処理ができます。これは全体をまとめて実行して、結果のまとめという静止した安全な世界です。[br][br]コモナドは「[b][size=150][color=#0000ff]1つにまとめるの反対[/color][/size][/b]」の作用があります。[br][br]○ [b]パックから1点とりだします。[code]extract : W[A] → A[/code](コンテキストから値を抽出)[/b][br][b]○ 1つの中から文脈全体に広げます。[code]extend : (W[A] → B) → W[B][/code](コンテキスト全体に拡散)[/b][br][br]たとえば、[b]ライフゲーム(Cellular Automaton)[/b]や[b]画像の平滑化(ぼかし)フィルタ[/b]のロジックは共通点がありますね。[br][br]1点の情報を取り出して、それを近傍に影響させるというルールの波を平面の各点で起こします。その波の効果が次々と起きて、全体が変わっていきます。局所の影響が全体に波及しますね。[br][br]これがコモナドです。
[size=150][u][color=#9900ff][b]課題:10セル×10セルのライフゲームは、コモナドの発想で作ろう。[br][br][/b][/color][/u][size=100][b]count_neighbors[/b] が「extract(局所観測)」のコモナドです。[br][b]next_generation[/b] が「extend(全体への波及)」のコモナドです。[br]グリッド全体を「文脈(環境)」として扱い、[br]各セルのローカルルールを全体に広げて新しい世界を作る。[br]まさに [b]コモナドの典型例[/b] ですね。[br][br]「ライフゲームのルール」は単純で、次の2つで生存か死になる。[br]# 生存ルール: 自分が「生(1)」かつ「周囲が2または3」[br][/size][/size]# 誕生ルール: 自分が「死(0)」かつ「周囲が3」[br]これを[b]next_generation[/b] に組み込もう。[br][br][IN]Python[br][br]import os[br]import random[br]import time[br][br]# 画面サイズ(5×5)[br]WIDTH = 5[br]HEIGHT = 5[br][br]ALIVE = "◙"[br]DEAD = "○"[br][br]def [b]create_initial_grid[/b]():[br] """初期盤面をランダム生成"""[br] return [[br] [random.choice([1, 0]) for _ in range(WIDTH)][br] for _ in range(HEIGHT)[br] ][br][br]def [b]count_neighbors[/b](grid, r, c):[br][b][color=#0000ff] """周囲8マスの生存セル数を数える(トーラス構造)"""[br][/color][/b] count = 0[br][color=#0000ff][b] for dr in (-1, 0, 1):[br] for dc in (-1, 0, 1):[br] if dr == 0 and dc == 0:[br] continue[br] nr = (r + dr) % HEIGHT[br] nc = (c + dc) % WIDTH[br] count += grid[nr][nc][br][/b][/color] return count[br][br]def [b]next_generation[/b](grid):[br][color=#0000ff][b] """コモナドの extend:全セルにローカルルールを適用"""[br][/b][/color] new_grid = [[0] * WIDTH for _ in range(HEIGHT)][br][br] for r in range(HEIGHT):[br] for c in range(WIDTH):[br] neighbors = count_neighbors(grid, r, c)[br] state = grid[r][c][br][b][color=#0000ff] if state == 1 and neighbors in (2, 3):[br] new_grid[r][c] = 1[br] elif state == 0 and neighbors == 3:[br] new_grid[r][c] = 1[br] else:[br] new_grid[r][c] = 0[br][/color][/b][br] return new_grid[br][br]def draw(grid, generation):[br] os.system("cls" if os.name == "nt" else "clear")[br] print(f"=== LIFE GAME (Generation: {generation}) ===")[br] print("+" + "-" * WIDTH + "+")[br] for row in grid:[br] line = "".join(ALIVE if cell == 1 else DEAD for cell in row)[br] print(f"|{line}|")[br] print("+" + "-" * WIDTH + "+")[br] time.sleep(0.3)[br][br]def [b]main[/b]():[br] grid = create_initial_grid()[br][br][b] for generation in range(5): # ★ 5世代だけ実行[br][/b] draw(grid, generation)[br] grid = next_generation(grid)[br][br] print("\n10世代のシミュレーションが終了しました。")[br][br]if __name__ == "__main__":[br] main()[br]
実行例[br][OUT][br]=== LIFE GAME (Generation: 0) ===[br]+-----+[br]|○○○◙○|[br]|○◙○○◙|[br]|○○○○◙|[br]|◙◙○◙○|[br]|◙◙○○○|[br]+-----+[br]=== LIFE GAME (Generation: 1) ===[br]+-----+[br]|○◙◙○◙|[br]|◙○○◙◙|[br]|○◙◙◙◙|[br]|○◙◙○○|[br]|◙◙○○○|[br]+-----+[br]=== LIFE GAME (Generation: 2) ===[br]+-----+[br]|○○◙○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○◙|[br]|○○○◙○|[br]+-----+[br]=== LIFE GAME (Generation: 3) ===[br]+-----+[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○◙○|[br]+-----+[br]=== LIFE GAME (Generation: 4) ===[br]+-----+[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]+-----+[br]=== LIFE GAME (Generation: 5) ===[br]+-----+[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]|○○○○○|[br]+-----+
[color=#0000ff][u][size=150]課題:geogbraでもライフゲームを作ろう。[br][/size][/u][/color][br]geogebraは行列が得意なので、setValueというスクリプトと行列の積を使って、次の状態を計算しよう。[br][br]#5×5のセルの1つ1つに隣接した位置があります。[br]#そこだけ抜き出すために、[br]#25個のセル1つ1つに対して、どのセルがとなりかの情報が必要です。隣接してれば1をしてなければ0を入れたリストをならべます。[br]#サイズ25のリストが25個格納された隣接行列がAです。[br]#隣接行列 A [br]A = {{0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},{1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},{0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},{0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},{0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},{1,1,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0},{1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0},{0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0},{0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0},{0,0,0,1,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0},{0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0},{0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0,0},{0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0,0},{0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0,0,0,0},{0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0},{0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,1,1,0,0,0},{0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0,0},{0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1,0},{0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0,1,1,1},{0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,1},{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0},{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0,0},{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1,0},{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,0,1},{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0}}[br][br]# 状態ベクトルvの初期値[br]v = {0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}[br][br]# 世代カウンター[br]g = 0[br]count = {0}[br][br]# ツールバーからボタンを選び画面にはりつけます。見出しは「次の世代」[br]# 世代を1つ進め、A.dot(v) を行い結果をvと同サイズのベクトルcountにセットします。[br]# ライフゲームのルール[br]# 生存ルール: 自分が「生(1)」かつ「周囲が2または3」[br]# next_v[(v == 1) & ((neighbor_counts == 2) | (neighbor_counts == 3))] = 1[br]# 誕生ルール: 自分が「死(0)」かつ「周囲が3」[br]# next_v[(v == 0) & (neighbor_counts == 3)] = 1[br]# によって、v状態ベクトルを更新しましょう。次のスクリプトを貼り付けましょう。[br][br]SetValue(g, g + 1)[br]Execute({"count = A * Transpose({v})"})[br]SetValue(v, Sequence(If((Element(v, i) == 1 && (Element(count, i, 1) == 2 || Element(count, i, 1) == 3)) || (Element(v, i) == 0 && Element(count, i, 1) == 3), 1, 0), i, 1, 25))[br][br][br]# ツールバーからボタンを選び画面にはりつけます。見出しは「リセット」、スクリプトは以下の通り[br]SetValue(g, 0)[br]SetValue(v, {0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})[br][br]#表示用のグリッドを作ります。[br]#不透明度の設定をいじるのではなく、「v の中身が 1 の場所だけに自動で四角形を描画する(0 の場所は描画しない)」にします。[br]# vはr行c列の行列を1次元のベクトルにしているので、読み取った結果をvの(r - 1) * 5 + c番目とします。そこが1のときだけ描画します。 [br][br]Sequence(Sequence(If(Element(v, (r - 1) * 5 + c) == 1, Polygon((c, -r), (c + 1, -r), (c + 1, -r - 1), (c, -r - 1))), c, 1, 5), r, 1, 5)[br][br]# ツールバーからボタンを選び画面にはりつけます。見出しは「ランダムな初期値」、[br]スクリプトは以下の通り[br]SetValue(g, 0)[br]SetValue(v, Sequence(RandomBetween(0,1),k,1,25))[br]#数式に次の行をはりつける。できたテキストラベルを適当に移動してください。[br]""+g +"世代"
ライフゲーム

Bilgi: 圏論の視点で計算科学(プログラミング)を見よう