Google ClassroomGoogle Classroom
GeoGebraClasse GeoGebra

FP:クラスは消え、「ラムダ大作戦」が始まる。

このワークシートはMath by Codeの一部です。 今回はオブジェクトの生成をFP化してみよう。 インスタンスの生成には、 下請け工場VSコピー屋さん抽象契約でWInWin で扱ったように、 Factory, ProtoType, AbstractFactoryなどの方法があったね。 それぞれの特徴を残しながら、関数化してみよう。

1.重厚なOOP工場建設を、柔軟なFP即席工場にしてしまう

<OOP:FactoryMethod> Factory工場法では 数の種類を子クラスでカスタマイズできるけれど、それを表示するための工場 も1対1で子クラスをつくらないといけなかったね。 # ========================================== #  ★「フレームワーク」はクラス集。A,Bからなる。 # ========================================== # 【クラスA】ツール(イベント処理で数の画面追加) # ========================================== class GraphicTool:     def on_mouse_click(self, editor_canvas):         print("[GraphicTool],clicked ", end="")         # 自分で new しようとするが、何を作ればいいか分からないので         # 「子クラスに丸投げするメソッド」を用意する。         new_element = self.create_number()           editor_canvas.add(new_element)     def create_number(self):         raise NotImplementedError("子クラスで、自分が作る数を指定してください") # ========================================== # 【クラスB】数の超抽象クラス(自分を画面にかく) # ========================================== # 数の超抽象クラス class AbstractNumber:     def draw(self):         raise NotImplementedError() # キャンバス class Canvas:     def add(self, element):         print(f"画面に追加されました: ", end="")         element.draw() # ========================================== # 【カスタマイズ】利用者が作ったコード # ========================================== # 数のサブクラスを作る class Natural(AbstractNumber):     def __init__(self, numera):         self.numera = numera     def draw(self):         print(f"自然数 {self.numera})") class Fraction(AbstractNumber):     def __init__(self, numera, denomi):         self.numera = numera         self.denomi = denomi     def draw(self):         print(f"分数 {self.numera}/{self.denomi})") #====サブクラスのインスタンスごとに工場クラスが必要になる。 class NaturalGraphicTool(GraphicTool):     def create_number(self):         # ツールの中に具体的な「Natural」というクラス名が書かれてしまっている         return Natural(numera="2027") class FractionGraphicTool(GraphicTool):     def create_number(self):         # ツールの中に具体的な「Fraction」というクラス名が書かれてしまっている         return Fraction(numera="20", denomi="27") # ========================================== # 実行 # ========================================== canvas = Canvas() print("\n--- 2027『専用ツール』をnewする ---") # パラメータ化できないので、専用のツールクラスを毎回newする music_tool_N = NaturalGraphicTool() music_tool_N.on_mouse_click(canvas) print("\n--- 分数20/27『専用ツール』をnewする ---") # 別の数を扱いたくなったら、ツールごと切り替える必要がある music_tool_F = FractionGraphicTool() music_tool_F.on_mouse_click(canvas)
<FP:FactoryMethod> OOPの「AbstractNumber」はクラスを使わずに、ただのデータまたはデータ生成関数で実現できます。 たとえば、関数を使って、辞書形式でデータの構造を決められます。 create_natural(numera)、create_fraction(numera, denomi)の2タイプつです。 OOPの AbstractNumber.drawはクラスを使わずに、draw部分の挙動を子クラスで決めるのではなく、 引数に持たせたいので、draw_element(element)として数タイプにあわせて文字列をかくようします。 キャンバスもクラスは使わずには画面に追加されたとプリントする関数canvas_add(element)だけにします。中でdraw_elementをすればよいですね。 処理の中心であるon_mouse_click(factory_func, canvas_add_func)は高階関数にします。 factory_func に lambda 式(引数なしでデータを返す関数)を渡すことで、通常の関数渡しとちがい、 渡した関数が使うはずの引数をコンパイル時に渡す必要がありません。使うところで、lamda式の 中でdraw_elementで使う引数を渡してやればよいのです。 # Factory FP # ========================================== # 1. データ構造(名詞:辞書を返す関数) # ========================================== def create_natural(numera): return {"type": "Natural", "numera": numera} def create_fraction(numera, denomi): return {"type": "Fraction", "numera": numera, "denomi": denomi} # ========================================== # 2. フレームワークも関数だけ # ========================================== def draw_element(element): if element["type"] == "Natural": print(f"自然数 {element['numera']})") elif element["type"] == "Fraction": print(f"分数 {element['numera']}/{element['denomi']})") def canvas_add(element): print("画面に追加されました: ", end="") draw_element(element) # factory_func` に lambda 式(引数なしでデータを返す関数)を渡します def on_mouse_click(factory_func, canvas_add_func): print("[GraphicTool],clicked ", end="") new_element = factory_func() # lambdaに応じた canvas_add_func(new_element) # ========================================== # 3. 実行(lambda を使ってその場で工場を作る) # ========================================== print("\n--- 2027『専用ツール』を関数で表現 ---") natural_factory = lambda: create_natural(numera="2027") on_mouse_click(natural_factory, canvas_add) print("\n--- 分数20/27『専用ツール』を関数で表現 ---") fraction_factory = lambda: create_fraction(numera="20", denomi="27") on_mouse_click(fraction_factory, canvas_add) [OUT] --- 2027『専用ツール』を関数で表現 --- [GraphicTool],clicked 画面に追加されました: 自然数 2027) --- 分数20/27『専用ツール』を関数で表現 --- [GraphicTool],clicked 画面に追加されました: 分数 20/27) すべて、関数で完結しましたね。 なお、on_mouse_click関数定義の引数のcanvas_add_funcというのは 実行時に on_mouse_click(natural_factory, canvas_add) on_mouse_click(natural_factory, canvas_add) で呼ばれたcanvas_addという定義済の関数を入れる引数の名前です。 lambda x: None(何もしない関数)を渡すと、 on_mouse_clickは何も実行しない、テストで使える関数になりますね。 FPの武器である lambda 式を使うと、 「使うはずの引数を、その場で事前に包み込んで(カプセル化して)関数ごとバケツリレーする」 という裏技が可能になり、 受け手側(on_mouse_click)のコードは1文字も変えることなく、 送り手側、ユーザーがその場で自由自在に、何個でも、どんな形でも「即席工場」を作って送り込めるようになりましたね。

2.不変だからクローンは不要となりコピー屋さんが消滅する

このサブクラスの対が増加するのをさけるために、プロトタイプ法が使えたね。 数のデータクラス自体に自己複製機能を持たせて、仲介となるコピー屋さんを作ったことを 思い出そう。 <OOP:Prototype法> import copy # ========================================== #  ★「フレームワーク」はクラス集。A,B,Cからなる。 # ========================================== # 【クラスA】ツール(イベント処理で数の画面追加) # ========================================== class GraphicTool:     def __init__(self, creator_component):         # ★【AがBの生成の仕方を「知らない」例】         # 自身で new できないため、生成を代行してくれるコピー屋さん(クラスC)に来てもらう。         self.creator = creator_component     def on_mouse_click(self, editor_canvas):         print("[GraphicTool],clicked ",end="")         # Aは、これから作るのBのサブクラスの生成の仕方を「知らない」。         # ただ、外から渡された creator に「1つ作って」と頼むだけ(カプセル化)。         new_element = self.creator.create()                 editor_canvas.add(new_element) # ========================================== # 【クラスB】数の超抽象クラス(自分を画面にかく、複製できる) # ========================================== class AbstractNumber:     def draw(self):         raise NotImplementedError("サブクラスで描画ロジックを実装してください")             def clone(self):         # Prototype パターンの肝となる複製処理         # 内部が単純な値だけなら、deepcopyではなくcopyで十分高速になる         return copy.copy(self) # ========================================== # 【クラスC】生成器(インスタンスをパラメータ化する対象) # ========================================== class C:     # コピー対象のインスタンスを変数(パラメータ)で受け取る。     def __init__(self, prototype_instance: AbstractNumber):         self.prototype = prototype_instance     def create(self):         # 保持しているインスタンスが自己複製したものを返す。         return self.prototype.clone() # エディタのキャンバス(擬似的な画面) class Canvas:     def add(self, element):         print(f"画面に追加されました: ", end="")         element.draw() # ========================================== # ★ここでは「カスタマイズ」はクラスBのサブクラス「自然数」と「分数」を作ること # ========================================== class Natural(AbstractNumber):     def __init__(self, numera):         #自然数のコンストラクター         self.numera = numera    # 数の絶対値     def draw(self):         #自然数を画面にかくメソッド         print(f"自然数 {self.numera})") class Fraction(AbstractNumber):     def __init__(self, numera, denomi):         #分数のコンストラクター         self.numera = numera    # 数の分子         self.denomi = denomi    # 数の分母     def draw(self):         #分数を画面にかくメソッド         print(f"分数 {self.numera}/{self.denomi})") # 模擬的なエディタ画面の用意 canvas = Canvas() print("\n--- 2027のインスタンスでパラメータ化して複製する ---") N = Natural(numera="2027") #NはBのサブクラスのインスタンス C_N = C(prototype_instance = N) #コピー屋さんCに原型をパラメータとしてNをわたす。 music_tool_N = GraphicTool(creator_component = C_N) #コピー屋さんCのデータを画面に渡すデータ music_tool_N.on_mouse_click(canvas) #画面に追加されるとインスタンスはそれぞれの流儀で自分を描画する print("\n--- 分数20/27のインスタンスで再パラメータ化 ---") C_F = C(prototype_instance = Fraction(numera="20", denomi="27")) GraphicTool(creator_component = C_F).on_mouse_click(canvas) print("\n--- 分数1998/1999のインスタンスで再パラメータ化 ---") C_F = C(prototype_instance = Fraction(numera="1998", denomi="1999")) GraphicTool(creator_component = C_F).on_mouse_click(canvas) for i in range(100):     N = C(prototype_instance = Natural(numera=i))     GraphicTool(creator_component = N).on_mouse_click(canvas)
  • <FP:Prototype法>
  • OOPでプロトタイプ(複製)が必要な理由は、
  • 同じインスタンスを使い回すと、
  • 誰かが中身を書き換えたときに他の場所にも影響が出てしまう副作用があるからです。
  • FPでは、create_natural が返すのはただの新しい辞書データ(あるいはタプル)です。
  • 新しいデータを不変で作るのがルールなので、
  • そもそも元のデータを守るためのクローン(複製)行為自体意味がありません
  • だから、クラスCの生成器クラス、コピー屋さんの仕事はなくなります。
  • Factoryメソッドの数の定義と処理関数の定義が流用できます。
  • すでに、数の種類に応じた工場を事前に作る必要がなくなっているので、 工場建設をなくすためのコピー屋さんも不要になっているわけですね。 # Prototype FP # # Factory FPの1.2の関数をそのまま流用する。 # ========================================== # 3. 実行(利用者が作ったコード) # ========================================== # 模擬的なエディタ画面の用意(関数) canvas = canvas_add print("\n--- 2027のインスタンス(を模した関数)でパラメータ化して複製する ---") creator_N = lambda: create_natural(numera="2027") on_mouse_click(creator_N, canvas) print("\n--- 分数20/27のインスタンスで再パラメータ化 ---") creator_F1 = lambda: create_fraction(numera="20", denomi="27") on_mouse_click(creator_F1, canvas) print("\n--- 分数1998/1999のインスタンスで再パラメータ化 ---") creator_F2 = lambda: create_fraction(numera="1998", denomi="1999") on_mouse_click(creator_F2, canvas) print("\n--- ループ処理 ---") # OOPでは100回 C クラスを new していましたが、FPではその場で lambda を渡すだけです。 for i in range(100): on_mouse_click(lambda: create_natural(numera=i), canvas) [OUT] --- 2027のインスタンス(を模した関数)でパラメータ化して複製する --- [GraphicTool],clicked 画面に追加されました: 自然数 2027) --- 分数20/27のインスタンスで再パラメータ化 --- [GraphicTool],clicked 画面に追加されました: 分数 20/27) --- 分数1998/1999のインスタンスで再パラメータ化 --- [GraphicTool],clicked 画面に追加されました: 分数 1998/1999) --- ループ処理 --- [GraphicTool],clicked 画面に追加されました: 自然数 0) [GraphicTool],clicked 画面に追加されました: 自然数 1) [GraphicTool],clicked 画面に追加されました: 自然数 2) [GraphicTool],clicked 画面に追加されました: 自然数 3) [GraphicTool],clicked 画面に追加されました: 自然数 4) …… 。 イミュータブルな世界ではクローンもコピー屋さんも不要になるということですね。

    3.WinWinの世界はどうなった?

    <OOP:AbstractFactory> 抽象工場というより、抽象的なただの契約でしかない、抽象契約となり、 フレームワークは超身軽になったことを思い出そう。 # ========================================== # 【クラスC】抽象契約(部品メニュー契約) # ========================================== class AbstractExpressionContract:     """【抽象契約】具体的な表示モードには関与せず、         必要な部品のラインナップだけを規定する。"""     def create_number(self, value):         raise NotImplementedError("契約に従って、数を返す仕組みを作ってください")     def create_operator(self, symbol):         raise NotImplementedError("契約に従って、演算子を返す仕組みを作ってください") # ========================================== # 【クラスA】画面工作員(部品契約画面に情報を入力して実行ボタンを押すだけ) # ========================================== class GraphicTool:     def __init__(self, contract_component: AbstractExpressionContract):         # 外の工場から届く「部品契約画面」         self.contract = contract_component     def on_mouse_click(self, editor_canvas):         print("[GraphicTool],clicked", end="")         # 契約書に情報を入力して実行ボタンを押すだけ(カプセル化)。         editor_canvas.add(self.contract.create_number("20"))         editor_canvas.add(self.contract.create_operator("+"))         editor_canvas.add(self.contract.create_number("80"))         editor_canvas.add(self.contract.create_operator("="))         editor_canvas.add(self.contract.create_number("100")) # エディタのキャンバス(擬似的な画面) class Canvas:     def add(self, element):         print(f"", end="")         element.draw() # ========================================== # 【カスタマイズ】利用者が作ったコード # ========================================== #テキスト表示用契約工場 --- class TextNumber:     def __init__(self, v): self.v = v     def draw(self): print(f" {self.v}", end="") class TextOperator:     def __init__(self, s): self.s = s     def draw(self): print(f"{self.s}", end="") class TextModeFactory(AbstractExpressionContract):     """契約を守る工場"""     def create_number(self, value): return TextNumber(value)     def create_operator(self, symbol): return TextOperator(symbol) # --- :TeX表示用の部品と、その契約を履行する工場 --- class TexNumber:     def __init__(self, v): self.v = v     def draw(self): print(f"${self.v}$ ", end="") class TexOperator:     def __init__(self, s): self.s = s     def draw(self): print(f"$\\pm {self.s}$", end="") class TexModeFactory(AbstractExpressionContract):     """TeXモードの契約を果たす工場"""     def create_number(self, value): return TexNumber(value)     def create_operator(self, symbol): return TexOperator(symbol) canvas = Canvas() print("\n--- 1. 【テキストモード契約】で画面工作員を動かす ---") text_contract = TextModeFactory() # テキスト用の工場(契約の具現化) music_tool_text = GraphicTool(contract_component = text_contract) music_tool_text.on_mouse_click(canvas) print("\n--- 2. 【TeXモード契約】へ一発切り替え ---") tex_contract = TexModeFactory() # TeX用の工場へ差し替え music_tool_tex = GraphicTool(contract_component = tex_contract) music_tool_tex.on_mouse_click(canvas) # 出力:画面に追加されました: TeX数 $2026$) 画面に追加されました: TeX記号 $\pm +$) [OUT] --- 1. 【テキストモード契約】で画面工作員を動かす --- [GraphicTool],clicked 20+ 80= 100 --- 2. 【TeXモード契約】へ一発切り替え --- [GraphicTool],clicked$20$ $\pm +$$80$ $\pm =$$100$
    <FP:AbstractFactory> 抽象工場は不要となり、 抽象工場で定義してい関数がcreate_number,create_operatorデータ定義として外に出ます。 カスタマイズして作っていた部品と工場もただの描画関数draw_text、draw_texにしましょう。 処理の中核となるon_mouse_click(env: dict, canvas_add_fn):には、 envで関数をパッケージにした辞書を、canvas_add_fnで、描画関数を渡します。 OOPの「contract(抽象工場クラス)」の代わりがenvです。 描画関数も関数をパッケージにした辞書もon_mouse_clickを使うときに変えて渡すことで、 抽象工場がなくても対応できるのです。 canvas_text = lambda element: draw_text(element) on_mouse_click(text_env, canvas_text) canvas_tex = lambda element: draw_tex(element) on_mouse_click(tex_env, canvas_tex) # AbstractFactory FP # AbstractFactory FP # ========================================== # 1. データ構造の定義(イミュータブルな辞書) # ========================================== # 共通のデータ生成関数 def create_number(value): return {"type": "Number", "value": value} def create_operator(symbol): return {"type": "Operator", "symbol": symbol} # ========================================== # 2. モード別の描画関数(カスタマイズ部分) # ========================================== # 【Textモード用】の描画ロジック def draw_text(element): if element["type"] == "Number": print(f" {element['value']}", end="") elif element["type"] == "Operator": print(f"{element['symbol']}", end="") # 【TeXモード用】の描画ロジック def draw_tex(element): if element["type"] == "Number": print(f"${element['value']}$ ", end="") elif element["type"] == "Operator": print(f"$\\pm {element['symbol']}$", end="") # ========================================== # 3. フレームワーク側(純粋関数と高階関数) # ========================================== # 【グラフィックツール】★核心部分 # OOPの「contract(抽象工場クラス)」の代わりに、 # 「関数をまとめた辞書(環境パッケージ)」を引数で受け取ります。 def on_mouse_click(env: dict, canvas_add_fn): print("[GraphicTool],clicked", end="") # 辞書から「数を作る関数」と「演算子を作る関数」を取り出して実行するだけ create_num = env["create_number"] create_op = env["create_operator"] canvas_add_fn(create_num("20")) canvas_add_fn(create_op("+")) canvas_add_fn(create_num("80")) canvas_add_fn(create_op("=")) canvas_add_fn(create_num("100")) # ========================================== # 4. 実行(利用者が自由にモードを組み立てて使う) # ========================================== print("\n--- 1. 【テキストモード契約】で画面工作員を動かす ---") # テキスト用の「環境パッケージ(関数をまとめた辞書)」を作る text_env = { "create_number": create_number, "create_operator": create_operator } # キャンバス(描画関数)もモードに合わせて部分適用(lambda)で切り替える canvas_text = lambda element: draw_text(element) on_mouse_click(text_env, canvas_text) print("\n--- 2. 【TeXモード契約】へ一発切り替え ---") # TeX用の「環境パッケージ」を作る(データ構造自体は使い回せる) tex_env = { "create_number": create_number, "create_operator": create_operator } # 描画関数をTeX用に切り替えるだけ canvas_tex = lambda element: draw_tex(element) on_mouse_click(tex_env, canvas_tex) [OUT] --- 1. 【テキストモード契約】で画面工作員を動かす --- [GraphicTool],clicked 20+ 80= 100 --- 2. 【TeXモード契約】へ一発切り替え --- [GraphicTool],clicked$20$ $\pm +$$80$ $\pm =$$100$ 抽象工場もデータ定義の中に消えました。 結局OOPの3パタンを通して、 生成のためのクラスは具体も抽象もすべて消えました。 コピー屋さんも消えました。 しかし、FPでは、 柔軟にその場で工場を作るラムダ式という軽く便利なロジックがとって代わりました。 ラムダ式は、 「ラムダを使用して手続き/関数を導入するのは、奇妙に思えるかもしれません。この表記法は、1930年代に「ハット」記号から始めたアロンゾ・チャーチに遡ります。彼は平方関数を「ŷ . y × y」と書きました。しかし、不満を抱いた印刷業者がハットをパラメータの左側に移動させ、大文字のラムダに変更しました。「Λy . y × y」。そこから大文字のラムダは小文字に変更され、現在では数学の本で「λy . y × y」、 Lispで「(lambda (y) (* yy))」を目にします。—ピーター・ノーヴィグ (norvig.com/lispy2.html)」とあります。 こんなに広まるとはチャーチさんは思ってなかったかもしれませんね。 無名関数と訳すとその訳にも違和感があります。 関数を使うには変数に代入する式を使います。 その変数を関数として呼び出しているのに、関数の引数を渡さなくても、 もう中がフィックスしているかのような、たんなる無名関数の戻り値を渡しているような書き方、 これが不思議だけど超便利だね。 canvas_tex = lambda element: draw_tex(element) on_mouse_click(tex_env, canvas_tex) この式のcanvas_texは無名関数をパックした名前なので、その名前を渡すだけで、 on_mouse_clickにすっと入り無事に動く。 無名の人がcanvas_texという仮の名前で安全に入り込む。不思議だ。
    <ラムダ式の基本の確認> ラムダ式 lambda x : f(g(x))とは、「xを入力としてf(g(x))を返す無名関数式」です。 ラムダ式の計算結果はラムダ関数と呼ばれますが仮の適当な名前sとすると、 sで堂々と通ってしまいます。 たとえば、 >>> s = lambda x : x * x >>> s at 0xf3f490> >>> s ( 12 ) 144 ラムダ式の引数は1つとは限りません。 lambdaのあとから:までの間に並ぶ文字が引数であり、:のあとに処理をかきます。 たとえば、 f = lambda a,x,b: -a*x**2 - b*x >>> f = lambda a,x,b: -a*x**2 - b*x >>> f(1,5,7) -60 >>> f(10,2,100) -240 ラムダ式は仮の名前がなくても動きます。 >>> (lambda x,y: x**2 - y**2)(16,12) 112 課題:Geogebraで無名関数が可能かの実験をしよう。 a=slider(-5,5,1) b=slider(-5,5,1) x^2 -a x^2 -b x x^2-y^2 と5行入力します。 x,yは予約語なので、変数として認識されます。 x^2、-a x^2 -b x、x^2-y^2は変数x、yの関数として認識されて、名前が自動でつきます。 無名関数は作りようがありません。 a,bはたまたま動かせるけれど定数だと決められてしまいます。 座標空間ではこれでいいのですが、自由がないですね。 でも、関数のグラフが即座にかかれるのは素晴らしいです。一長一短ですね。 関数の名前はあとで変える自由はあります。 でも裏でID登録がされていて、不審者の入り込めない世界でしょう。 戸籍のあるオブジェクトが座標空間の枠組みの中で動き回れる、 由緒正しい者たちの美しい世界だったのですね。

    Geogebraは無名を許さない