FP:ビルダーのラムダ大作戦:処理自体を辞書化せよ!
このワークシートはMath by Codeの一部です。
AbstractFactoryでは、データ中心にして、抽象クラスも処理もすべて関数にしてOOPをFPにしましたね。
今回はビルダーパタンです。
ビルドするデータ要素のタイプが数と演算だったが種別が増えるだけです
まず、OOPの思い起こしから始めましょう。
<OOP:Builder>
[IN]build.py
# ==========================================
# 【クラスC:名誉職人】ビルドさん(抽象契約)
# ==========================================
class AbstractDocBuilder:
"""【名誉職人】一切手は汚さない。
ただ、文書を構成するためのキーワード(インターフェース)を提示するだけ。"""
def add_title(self, text): raise NotImplementedError()
def add_paragraph(self, text): raise NotImplementedError()
def add_bullet_point(self, item): raise NotImplementedError()
def add_formula(self, expr): raise NotImplementedError()
def get_result(self): raise NotImplementedError()
# ==========================================
# 【クラスA:ディレクター】編集長
# ==========================================
class DocumentDirector:
"""【ディレクター】文書の構成(順番)と内容(文字)を決定する。
ただし、最終的にそれがどんな形式(表現)になるかは『知らない』。"""
def __init__(self, builder: AbstractDocBuilder):
self.builder = builder # ユーザーが連れてきた具象職人をセットする
def construct_page(self):
# 名誉職人のキーワードに沿って、上から順番に原稿を組み立てる
self.builder.add_title("デザインパターンを学ぼう")
self.builder.add_paragraph("二元論で語ることは簡単だ。")
self.builder.add_bullet_point("下請け工場パタン")
self.builder.add_bullet_point("コピー屋さんパタン")
self.builder.add_formula("E = mc^2")
# ==========================================
# 【カスタマイズ1】テキストファイル担当の職人
# ==========================================
class TextDocBuilder(AbstractDocBuilder):
def __init__(self):
self.lines = [] # ここにテキストを書き溜めていく(手を汚す場所)
def add_title(self, text):
self.lines.append(f"【★ {text} ★】\n")
def add_paragraph(self, text):
self.lines.append(f" {text}\n")
def add_bullet_point(self, item):
self.lines.append(f" ・ {item}\n")
def add_formula(self, expr):
self.lines.append(f" [式: {expr}]\n")
def get_result(self):
# 最後に、溜まった文字列をガッチャンコして完成品を渡す
return "".join(self.lines)
# ==========================================
# 【カスタマイズ2】HTMLファイル担当の職人
# ==========================================
class HtmlDocBuilder(AbstractDocBuilder):
def __init__(self):
self.tags = [] # ここにHTMLタグを書き溜めていく(手を汚す場所)
def add_title(self, text):
self.tags.append(f"{text}\n")
def add_paragraph(self, text):
self.tags.append(f"{text}\n")
def add_bullet_point(self, item):
self.tags.append(f"{item} \n")
def add_formula(self, expr):
self.tags.append(f"${expr}$\n")
def get_result(self):
# タグをすべて結合して、一つのHTMLドキュメントとして完成させる
return "\n\n" + "".join(self.tags) + "\n"
print("--- 1. 【テキスト職人】を配属して構築する ---")
text_worker = TextDocBuilder() # 現場のテキスト職人
director_T = DocumentDirector(builder = text_worker)
director_T.construct_page() # 編集長が原稿を読み上げる
# 最後に職人から完成品を受け取る
text_result = text_worker.get_result()
print(text_result)
print("--- 2. 【HTML職人】へ一発チェンジして構築する ---")
html_worker = HtmlDocBuilder() # 現場のHTML職人
director_H = DocumentDirector(builder = html_worker) # 編集長はそのまま
director_H.construct_page() # まったく同じ原稿をもう一度読み上げる
# 最後に職人から完成品を受け取る
html_result = html_worker.get_result()
print(html_result)
[OUT]
--- 1. 【テキスト職人】を配属して構築する ---
【★ デザインパターンを学ぼう ★】
二元論で語ることは簡単だ。
・ 下請け工場パタン
・ コピー屋さんパタン
[式: E = mc^2]
--- 2. 【HTML職人】へ一発チェンジして構築する ---
デザインパターンを学ぼう
二元論で語ることは簡単だ。
下請け工場パタン
コピー屋さんパタン
$E = mc^2$
<FP:ビルダーパタン>
データ構造の辞書を作って、
要素タイプ別にどのように表示するかの関数に渡すという方法がおもいつきますね。
これは、AbstractFactoryと同じ発想です。
# ==========================================
# 1. データ構造の定義(文書の構成要素を表すイミュータブルな辞書)
# ==========================================
def create_title(text):
return {"type": "Title", "text": text}
def create_paragraph(text):
return {"type": "Paragraph", "text": text}
def create_bullet_point(item):
return {"type": "Bullet", "item": item}
def create_formula(expr):
return {"type": "Formula", "expr": expr}
# ==========================================
# 2. ディレクター側(構造の決定:純粋関数)
# ==========================================
# OOPの DocumentDirector.construct_page に相当します。
# 状態を持たず、文書の構造を表すデータのリストを生成して返します。
def construct_page():
return [
create_title("デザインパターンを学ぼう"),
create_paragraph("二元論で語ることは簡単だ。"),
create_bullet_point("下請け工場パタン"),
create_bullet_point("コピー屋さんパタン"),
create_formula("E = mc^2"),
]
# ==========================================
# 3. 変換側(各フォーマットへのレンダリング:純粋関数)
# ==========================================
# 【テキスト担当】要素をテキスト文字列に変換する関数群
def render_text_element(element):
el_type = element["type"]
if el_type == "Title":
return f"【★ {element['text']} ★】\n"
elif el_type == "Paragraph":
return f" {element['text']}\n"
elif el_type == "Bullet":
return f" ・ {element['item']}\n"
elif el_type == "Formula":
return f" [式: {element['expr']}]\n"
return ""
def generate_text_document(elements):
# 各要素を変換して結合する
return "".join(render_text_element(e) for e in elements)
# 【HTML担当】要素をHTML文字列に変換する関数群
def render_html_element(element):
el_type = element["type"]
if el_type == "Title":
。。。。。
でも、冗長ですね。
render_text_elementとrender_html_elementが
データの要素で分岐しているif…elif文になったからです。
そこで、辞書を見ながら処理するのではなく、処理自体を辞書化してみましょう。
ただし、以下のコードはgeogebraの<>が予期せぬ反応を示すので、HTMLタグなのに全角に
しています。ご使用のさいは、半角に直してください。
# Builder FP
# ==========================================
# 1. 各モードの「組み立てルール(ラムダ関数の辞書)」
# ==========================================
# 【Textモード用】前の文字列(c)に、テキスト用の装飾を加えてリレーするルール
text_builder_env = {
"add_title": lambda c, text: f"{c}【★ {text} ★】\n",
"add_paragraph": lambda c, text: f"{c} {text}\n",
"add_bullet_point": lambda c, item: f"{c} ・ {item}\n",
"add_formula": lambda c, expr: f"{c} [式: {expr}]\n"
}
# 【HTMLモード用】前の文字列(c)に、HTMLタグを巻き込んでリレーするルール
html_builder_env = {
"add_title": lambda c, text: f"{c}<h1>{text}</h1>\n",
"add_paragraph": lambda c, text: f"{c}<p>{text}</p>\n",
"add_bullet_point": lambda c, item: f"{c}<li>{item}</li>\n",
"add_formula": lambda c, expr: f"{c}${expr}$\n"
}
# ==========================================
# 2. フレームワーク側:編集長(高階関数)
# ==========================================
def construct_page(builder_env: dict):
"""
【Director関数】
引数として「4つの種別ルールが入った辞書」を受け取る。
初期状態の空文字 "" からスタートし、戻り値を次の引数へと流し込む
『不変(イミュータブル)なバケツリレー』で原稿を組み立てる。
同名の新規変数なので、同じメモリのデータの変更・追加ではありません。
上書きの連続をしているのですが、別物にたまたま同じ名前をつけているのです。
なんなら、名前をつけずに関数を入れ子にしても同じことです。
"""
res = ""
# 編集長が構成に沿って、上から順番にラムダ式へリレーしていく
res = builder_env["add_title"](res, "デザインパターンを学ぼう")
res = builder_env["add_paragraph"](res, "二元論で語ることは簡単だ。")
res = builder_env["add_bullet_point"](res, "下請け工場パタン")
res = builder_env["add_bullet_point"](res, "コピー屋さんパタン")
res = builder_env["add_formula"](res, "E = mc^2")
return res
# ==========================================
# 3. 実行(Jupyterのセルで Shift + Enter !)
# ==========================================
print("--- 1. 【テキスト職人(辞書)】を配属して構築する ---")
text_result = construct_page(text_builder_env)
print(text_result)
print("--- 2. 【HTML職人(辞書)】へ一発チェンジして構築する ---")
# HTMLは最後に挟む外枠のルールだけ、受け取った後に関数で添えればよい
html_raw = construct_page(html_builder_env)
html_result = f"\n\n{html_raw}\n"
print(html_result)
<振り返り>
いやあ、色々しびれますね。
関数自体を辞書化して、使うときにラムダ関数につけた仮の名前で使えてます。
また、別物に同名をつけて、違う物にやあ[res]と同じ名前で呼び込むのですから。
気持ち悪ければ、多重カッコで、出して変わったものを次にリレーするのが視覚化してもよいですが、
メモリの「名前」って本当何なんでしょうね。
区別できればなんでもよいラムダ関数の「名前」。
同姓同名の別人はたくさんいるように、
「同名」でnewして何が悪い!
ということか。
戸籍があり同姓同名はゆるさないGeogebraでは、
オブジェクトaがあるのに、またaの名前を使おうとすると、
a_1, a_2のように必ず区別されてしまいます。
リスト内包式やZipが使えたりして、PythonとGeogebraは似ているところがあると思っていましたが、
根本の関数型、オブジェクトの管理が別の世界であることがしみじみわかりました。
geoのalgebra、幾何空間のオブジェクトを識別することからスタートしてたのですね。
ということで、今回はgeogebraの課題はなしとします。