FP:データ分離、関数はバケツリレーできる変数の1つ
このワークシートはMath by Codeの一部です。
今回からOOPからFPへの移行を試みましょう。
継承VS移譲のところで、
テンプレートメソッドとストラテジーパタンを対比しましたね。
その発想と動きを思い起こしをします。
1. データと処理の分離で、OOPをFPに移植しよう
<継承のTemplateパタン>
# ==========================================
# 【クラスA】ツール(イベント処理で数の画面追加)
# ==========================================
# ただの文字列の画面表示と割り切れば、どんな数でも文字列のdata
# だから、数のインスタンス化は不要
class CanvasTemplate:
def __init__(self, data):
self.data = data
def on_mouse_click(self):
print("[CanvasTemplate],clicked ", end="")
print (self.draw_number())
def draw_number(self):
raise NotImplementedError("子クラスで、自分が作る数を指定してください")
# ==========================================
# 【カスタマイズ】利用者が作ったコード
# ==========================================
# サブクラスを作る
class Natural(CanvasTemplate):
def draw_number(self):
return f"自然数 {self.data}"
class Fraction(CanvasTemplate):
def draw_number(self):
return f"分数 {self.data}"
Aクラスを継承(Is a)した子クラスNatural、Fractionによって、出力を変えました。
# ==========================================
# 実行
# ==========================================
nat=Natural("2027")
nat.on_mouse_click()
fr=Fraction("20/27")
fr.on_mouse_click()
[OUT]
[CanvasTemplate],clicked 自然数 2027
[CanvasTemplate],clicked 分数 20/27
<関数型プログラミング(FP)ってなんだっけ>
関数はできるだけ「純粋関数」にします。
数学といっしょで、いつ実行しても、同じ入力では決まった出力があるものですね。
関数の作用で変数の値が変わる(副作用)ことは避けましょう。
つまり、値の「不変性(イミュータブル)」を目指すです。
<OOPをFPにする>
まず、処理(動詞)とデータ(名詞)に分けて、データ(名詞、状態)中心に逆転します。
そのデータを処理(動詞)していたクラスを、関数群にバラします。
入れるデータ(名詞)の方は、クラスやリストなどにして扱いやすくします。
関数には内部データがないので、引数として入れましょう。
内部データAに処理をしていたときは、Aを引数にして、新しい変数AAとかに関数の戻り値を入れます。
これで、Aは処理の副作用がなく、不変ですね。
テンプレートメソッドは、入り口を決めて処理するパターンだった。
でも、入れる数に応じて出し方を変えるために数の種類や数だけ子クラスが必要になってしまってたね。
これを、入れる数の種類で処理の子クラスを作るかわりに、
入れるデータクラスのインスタンス化で対応しましょう。
クラスは増やさず、入れたいだけ関数に入れていくだけだ。
すごくしくみがカンタンになった。
# Template FP
#データ部分
class Number:
def __init__(self, type_name:str, value_str:str):
self.type_name = type_name
self.value_str = value_str
#処理部分
def draw_number(num:Number):
return f"{num.type_name} {num.value_str}"
def on_mouse_click(strings:str):
print("[CanvasTemplate],clicked ", end="")
print(strings)
# ==========================================
# 実行
# ==========================================
natural = draw_number(Number("自然数", "2027"))
fraction = draw_number(Number("分数", "20/27"))
on_mouse_click(natural)
on_mouse_click(fraction)
[OUT]
[CanvasTemplate],clicked 自然数 2027
[CanvasTemplate],clicked 分数 20/27
上のやり方では、draw_numberの返り値の文字列を変数にしてから、on_mouse_clickに渡していた。
しかし、
on_mouse_click(draw_number(Number("自然数", "2027"))
のようにすると、この一行がテンプレートそのものなるね。
デザインパタンの見える化だ。
2. ストラテジーパタン(移譲)を簡単にしよう
<移譲のStrategyPattern>
データの入り口はTemplateと同じだったが、数の種類で職人的に加工して表示する処理をしたいときに
ストラテジーパタンがあったね。
# Strategy.py
# ==========================================
# 【クラスN】職人クラスとサブクラス
# ==========================================
class NotationStrategy:
"""数式表現の『脳みそ』の契約。全員が同じ地平(水平)に並ぶ"""
def format_data(self, data):
raise NotImplementedError()
class NaturalStrategy(NotationStrategy):
def format_data(self, data): return f"自然数 {data}"
class FractionStrategy(NotationStrategy):
def format_data(self, data): return f"分数 {data}"
class BinaryStrategy(NotationStrategy):
def format_data(self, data): return f"2進数 {bin(int(data))}"
# ==========================================
# 【クラスC】外注するだけ(職人に丸投げ)
# ==========================================
class CanvasContext:
def __init__(self, strategy: NotationStrategy):
self.strategy = strategy # 水平に職人を雇う(委譲)
def change_strategy(self, new_strategy: NotationStrategy):
"""実行中に、いつでも外注先をカチッと切り替えられる"""
self.strategy = new_strategy
def on_mouse_click(self, data):
print("[CanvasContext] クリックされました -> ", end="")
print(self.strategy.format_data(data))
# 1. 職人をあらかじめスカウトしておく
natural_brain = NaturalStrategy()
fraction_brain = FractionStrategy()
binary_brain = BinaryStrategy()
# ==========================================
# 【カスタマイズ】利用者が作ったコード
# ==========================================
# クラスNのサブクラスなら自前の職人も使えるが、
# 基本、メニューの職人(戦略)を雇って丸投げする。
# 2. 最初は自然数職人で画面を起動
canvas = CanvasContext(natural_brain)
canvas.on_mouse_click("2026")
# 3. 画面はそのままで職人をスイッチ!
canvas.change_strategy(fraction_brain)
canvas.on_mouse_click("2026/05")
# 4. 職人をさらにスイッチ!
canvas.change_strategy(binary_brain)
canvas.on_mouse_click("42")
[OUT]
[CanvasContext] クリックされました -> 自然数 2026
[CanvasContext] クリックされました -> 分数 2026/05
[CanvasContext] クリックされました -> 2進数 0b101010
<OOPをFPにする>
内部での処理が必要になりそうだ。ということは、描画関数の職人を3つ作り、
クリックされたときに、職人ごと渡すという手があるね。
これは「高階関数」というやり方で、
関数も数と同じくやりとりできる言語には共通に使える方法だ。
まあ、関数名を変数のように渡すだけだけどね。
# Strategy FP
#データ部分
#描画職人
def draw_natural(str):
return f"自然数 {str}"
def draw_fraction(str):
return f"分数 {str}"
def draw_binary(str):
return f"2進数 {bin(int(str))}"
def on_mouse_click(strategy, str):
print("[CanvasTemplate],クリックされました -> ", end="")
print(strategy(str))
# ==========================================
# 実行 共通関数(職人関数名、わたすデータ)という形
# ==========================================
on_mouse_click(draw_natural,"2027")
on_mouse_click(draw_fraction,"2026/05")
on_mouse_click(draw_binary,"42")
[OUT]
[CanvasTemplate],クリックされました -> 自然数 2027
[CanvasTemplate],クリックされました -> 分数 2026/05
[CanvasTemplate],クリックされました -> 2進数 0b101010
職人とデータを直接入れるだけだから、データのクラスすら不要になった。
カンタンすぎて、これでいいのかという感じだね。
テンプレートにしろ、ストラテジーにしろ、
関数を入れ子で使える。つまり、バケツリレーができるというのが
関数型の実用的なメリットの一つだね。
課題:Geogebraで関数型はできますか。
関数型は数値処理なら可能です。
上にあるような文字列処理ではうまくいきません。
文字列は貼り付けるべきオブジェクトとなり、自動でtext1,text2のように名前がつくからです。
たとえば、数式に”1234"と入力すると、1234とだけ表示されますが、
裏でtext1と識別子がつけられてます。
ツールボックスのABCアイコンで上級タブを選び、Geogebraアイコンを選ぶとtext1があります。
これを選び、カーソルを先頭に移動して、自然数と入力してOKを押すと、
text2="自然数"+text1+""ができます。
同様にして、
text3="分数"+text1+""を作れますね。
deci=123
toBase(deci,2)とすると、"1111011"と
表示されます。
画面反応式の関数、つまり、画面で入力したものにどう反応するかを画面でみるもの
を作ることはできます。
たとえば、
InputBox(text1)
InputBox(deci)
という数式で、入力用のボックスができるので、これが引数の関数が即席でできますね。
「関数」を作ることもできます。
ここで、全体メニュー「≡」からツール>新規ツールの作成
ダイアログが開きます。
入力オブジェクトは、deciをドロップダウンリストから選び、
出力オブジェクトは、をドロップダウンリストから選び、
名前とアイコンは、ツール名を2進、コマンド名をbinで終了。
ツールのアイコンの最後のスパナボタンを選び、画面をクリックすると
数値入力が促されます。そこに10進数を入れると、
2進数がクリック場所に貼り付けられてますね。
画面を2進数だらけにすることができます。
bin関数という画面にオブジェクトを貼り付ける関数ができあがりました。
もちろん貼り付けをせずに、計算を返す関数も作れます。純粋関数です。
計算ブロックをまとめて、関数に格上げすることも可能です。
でも、ダイアログボックスで関数を作るという発想がふつうはないでしょうし、
作ったあとに直すのも大変そうです。
知る人ぞ知るの世界ですね。
一応できるということの確認でした。