FP:探索パタンで「リストも関数もオブジェクト」を再認する
このワークシートはMath by Codeの一部です。
今回は、探索3兄弟のうちの2つイテレタとインタプリタをOOPからFPに移植しよう。
イテレタは実装に関係なく共通の探索仕様で使えた。
インタプリタは単語と文法をまるごと装備して確実な変換ができた。
FPにするとこのメリットを守りながら、さらにどんな価値を付加できるだろうか。
1.イテレタをFPにしてスマートにしよう。
<OOP:Iterator>
検索のキーワードをきめて、実装は子クラス(3の倍数検索と3のつく数検索)でちがっていい。
これが、Iteratorだった。OOPでは、リストを先に作っておいて、そのリストあればnabeを真と返すロジックにしたね。
#nabeatsu.py
# ==========================================
# 【インターフェース】ナベアツイテレータの共通契約
# ==========================================
class NabeatsuIterator:
"""内部のリスト構造がどうであれ、共通のキーワード『nabe(x)』を保証する"""
def __init__(self, max_num=100):
self.max_num = max_num
self.target_list = self._create_list() # 子クラスごとに異なるリストが作られる
def _create_list(self):
"""【型枠】リストの具体的な作り方は子クラスに委ねる"""
raise NotImplementedError()
def nabe(self, x):
"""ユーザーが使う共通キーワード。リストに属していればTrueを返す"""
return x in self.target_list
# ==========================================
# 【子クラス1】3の倍数リストイテレータ(数理的なアプローチ)
# ==========================================
class Multi3Iterator(NabeatsuIterator):
def _create_list(self):
# 1からmax_numまでの間で、3の倍数だけを格納したリストを作る
return [i for i in range(1, self.max_num + 1) if i % 3 == 0]
# ==========================================
# 【子クラス2】3のつく整数リストイテレータ(集合を駆使したアプローチ)
# ==========================================
class HasDigit3Iterator(NabeatsuIterator):
def _create_list(self):
# 10の位が3の集合
A = {30 + x for x in range(10)}
# 1の位が3の集合
B = {3 + 10 * x for x in range(10)}
# 重複する33の集合
C = {33}
# 集合の和(OR)から重複を引き、1からmax_numに収まるものだけをリスト化
union_set = (A | B) - C
return [i for i in range(1, self.max_num + 1) if i in union_set]
# ==========================================
# 【実行・ユーザー側のコード】中身を知らなくても、同じ命令で全件調査!
# ==========================================
# 1. 異なる内部実装を持つ、2つのイテレータインスタンスを生成
M = Multi3Iterator(100)
D = HasDigit3Iterator(100)
print("ナベアツさん登場! \n")
# 2. ユーザーは共通の「nabe(i)」だけで、1から100まで確実に走査する
for i in range(1, 100 + 1):
# Mの中身が倍数リストか、Dの中身が集合演算か、ユーザーは一切知らなくてよい
if M.nabe(i) or D.nabe(i):
print("(変顔)", end=" ")
else:
print(f"{i}", end=" ")
# 10個ごとに改行して見やすく整える
if i % 10 == 0:
print()
[OUT]
1 2 (変顔) 4 5 (変顔) 7 8 (変顔) 10
11 (変顔) (変顔) 14 (変顔) 16 17 (変顔) 19 20
(変顔) 22 (変顔) (変顔) 25 26 (変顔) 28 29 (変顔)
(変顔) (変顔) (変顔) (変顔) (変顔) (変顔) (変顔) (変顔) (変顔) 40
41 (変顔) (変顔) 44 (変顔) 46 47 (変顔) 49 50
(変顔) 52 (変顔) (変顔) 55 56 (変顔) 58 59 (変顔)
61 62 (変顔) 64 65 (変顔) 67 68 (変顔) 70
71 (変顔) (変顔) 74 (変顔) 76 77 (変顔) 79 80
(変顔) 82 (変顔) (変顔) 85 86 (変顔) 88 89 (変顔)
91 92 (変顔) 94 95 (変顔) 97 98 (変顔) 100
<FP:Iterator>
OOPではリストを先に作って、順次的に判定していた。
どうせ、最後は順次判定なのだから、3の倍数も数字3ありも関数にして、その場で判定しよう。
3の倍数関数is_multi_3も数字3ありも関数has_digit_3もいきなりtrueを返すようにし
共通の親クラスはいない。
FPでは、関数を招きいれるスーパー関数、高階関数を用意した。
そこに、ラムダをいれて柔軟に対応するというのがお決まりのパタンだった。
今回は、pythonにもれなく入っている高階関数のAnyを使う。
どれかがでもあたる、なんか1つでも当たればOKという、
ハードルの低い関数、論理的には「OR」と同じだね。
execute_nabeatsuが高階関数だ。
判定最大数と、2つの関数をリストにしたpredicate_funcsを読ませよう。
その中で、for文をまわすiをis_multi_3(i)とhas_digit_3(i)に読ませるとき、これを
func(i)という名前で受け取る。
funcとは predicate_funcsのリストにある関数だから、ORを使わずに、
any(func(i))でfuncがいくつ入っても対応できる。
だから、5の倍数で変顔するようになっても、
any(func(i))という共通仕様
で変顔が簡単に出せる。
# ==========================================
# 1. 判定ロジックの定義(純粋関数 / 述語関数)
# ==========================================
def is_multi_3(x: int) -> bool:
"""3の倍数かどうかを判定する純粋関数"""
return x % 3 == 0
def has_digit_3(x: int) -> bool:
"""3のつく数字かどうかを判定する純粋関数"""
return '3' in str(x)
# ==========================================
# 2. フレームワーク側(高階関数による共通の走査)
# ==========================================
def execute_nabeatsu(max_num: int, predicate_funcs: list):
for i in range(1, max_num + 1):
if any(func(i) for func in predicate_funcs):
print("(変顔)", end=" ")
else:
print(f"{i}", end=" ")
# 10個ごとに改行して見やすく整える
if i % 10 == 0:
print()
# ==========================================
# 3. 実行
# ==========================================
print("ナベアツさん登場! \n")
rules = [is_multi_3, has_digit_3]
execute_nabeatsu(max_num=100, predicate_funcs=rules)
OOPは親を共通にして、動き方も共通にしてリストを先に作るという重装備だった。
FPでは親は問わない。
ただ処理する関数をリストに束ねてリスト名をつけて処理の親玉である高階関数に渡すだけだった。
メモリも紙面も無駄に食わないスマートなシステムだね。
2。インタプリタをさらに純粋にしよう
<FP:Interpreter>
OOPではありません。すでに関数型になっています。
from functools import reduce
def calcDiff(RPN: str) -> int:
"""【Interpreter】
逆ポーランド記法(RPN)の文字列を通訳し、数式の意味を解釈して計算結果を返す。
words ➔ foldl(foldStack) ➔ head のパイプライン合成。
"""
# 1. 【words関数】文字列をスペースで区切ってリスト(トークン列)に分解する
def words(text: str) -> list:
return text.split()
# 2. 【foldStack関数】左畳み込み(foldl)の各ステップで、スタックに値を積む、または計算する
# Pythonの reduce(foldl) に合わせて、第1引数をスタック(S)、第2引数を読み取った文字(item)とする
def foldStack(S: list, item: str) -> list:
operators = ["+", "-", "*", "/"]
if item in operators:
# 演算子を読み込んだ場合:スタックの先頭からx, yを取り出す(Sの先頭から順にx, yになる)
x = S.pop(0)
y = S.pop(0)
# 演算子に応じた計算を行い、結果zをスタックの先頭(xs)に戻す
if item == "+": z = y + x
elif item == "-": z = y - x
elif item == "*": z = y * x
elif item == "/": z = int(y / x) # 割り算は整数丸め
S.insert(0, z)
else:
# 数値を読み込んだ場合:整数に変換してスタックの先頭(xs)に追加する
S.insert(0, int(item))
return S
# 3. 【head関数】畳み込んだあとのスタックの先頭要素を読み込む
def head(S: list) -> int:
return S[0]
# ========================================================
# 【パイプライン合成の実行】
# 提示された数式「 1 2 + 3 * 」の文脈を、左から右へと畳み込んで解釈していく
# ========================================================
tokens = words(RPN) # ① words: 文字列をリストLに分解
final_stack = reduce(foldStack, tokens, []) # ② foldl: 空リスト[]からスタートしてトークンを畳み込む
return head(final_stack) # ③ head: 先頭の計算結果を回収
# ==========================================
# 【作動テスト】
# ==========================================
print("インタプリタ\n")
rpn_expr1 = "4 3 2 + 1 - *"
result1 = calcDiff(rpn_expr1)
print(f"RPN表記: {rpn_expr1}")
print(f"[OUT] ➔ {result1}")
<More FP:Interpreter>
FPの思想(純粋さと不変性)でコードをさらに磨きましょう。
S.pop(0) や S.insert(0, ...)が副作用がある関数なのでこれをなくしましょう。
破壊的なメソッドを使わずに、新スタックを作る方法を考えましょう。
計算結果のInsertを書き方は、
計算結果をスタックの先頭(xs)に追加して、Sを破壊していたS.insert(0, int(item))をやめ、
計算結果zを先頭にして、3番目以降のスライス(S[2:])をつないだ[z, *S[2:]]にします。
数値のInsertを書き方は、
整数に変換してスタックの先頭(xs)に追加するS.insert(0, int(item))をやめ、
アンパック演算子(*)でレベルをリストの中身にしておき、 [int(item), *S]で数値を先頭に追加します。
演算数値x、yの取り出し、
x = S.pop(0);y = S.pop(0)をやめ、
x = S[0];y = S[1]でリストを破壊せずに取り出します。
また、オペレーションの適用は、事前に演算子リストを作り、それに対応するif文をかいてました。
これを、オペレーション辞書にしましょう。
つまり、演算記号ごとにラムダ式で計算します。
こうすれば、いくらでもオペレーションを1行単位で追加、削除できますね。
# Iterator FP
from functools import reduce
def calcDiff_pure(RPN: str) -> int:
"""【Interpreter: 究極純粋FP版】
.pop() も .insert() も一切使わない。
情報の更新という名の破壊を完全にやめ、イミュータブルな新築リストだけでリレーする。
"""
# 1. 【words関数】(変更なし)
def words(text: str) -> list:
return text.split()
# 2. 【foldStack関数】一切の破壊メソッドを追放
def foldStack(S: list, item: str) -> list:
# 演算子と処理(ラムダ式)の辞書
operators = {
"+": lambda y, x: y + x,
"-": lambda y, x: y - x,
"*": lambda y, x: y * x,
"/": lambda y, x: int(y / x)
}
if item in operators:
x = S[0]
y = S[1]
z = operators[item](y, x)
return [z, *S[2:]]
else:
return [int(item), *S]
# 3. 【head関数】(変更なし)
def head(S: list) -> int:
return S[0]
# パイプライン実行(完全にクリーンな世界)
return head(reduce(foldStack, words(RPN), []))
# ==========================================
# 【作動テスト】
# ==========================================
print("至高のインタプリタ(純度100%)\n")
rpn_expr1 = "4 3 2 + 1 - *"
result1 = calcDiff_pure(rpn_expr1)
print(f"RPN表記: {rpn_expr1}")
print(f"[OUT] ➔ {result1}")
イテレタでは処理の関数のリスト化、
インタプリタでは演算子ごとの関数の辞書化。
どちらも関数という処理名を
まるで、ふつうの数値データのようにリストや辞書でパックして、親玉関数で使います。
インタプリタではイミュータブルなリストをバケツリレー(words ➔ reduce ➔ head)」しました。
なんてクリーンなんでしょう、スリムなんでしょう!
また、対象データもスライス、アンパックまど、破壊しないで加工して、
イミュータブルなクリーンな世界を守ってます。
つまり、FPのクリーンでスリムなことは、
見た目とメモリ、表と裏、どっちにも言えるんですね。3.振り返り
<振り返り>
関数型というと、リスト内包表記と連携して使われることが多い。
そのため、関数プログラミングはリストにたいするsum,product,reduceなどの演算子の使えるものという
イメージを持ちやすい。
いわゆる1行プログラムだ。
しかし、FPのめざすものは純粋さだ。必ず入出力に不動の対応があることと
関数によって、かかわるオブジェクトが壊れない、副作用がない不変な世界を作るという思想がある。
for文が リストの中に閉じ込められるからすっきりかけるというのとは意味がちがう。
一行プログラムは見た目はすっきすりするが、リストの中に変数、処理、範囲をすべて詰め込むため
行数はへるが1行が長くなる。また、扱う変数が多くなると、まわすfor句が重なる。
一行プログラムでは、
通常のブロック(改行やカッコによる)よりも行数は減るが、メモリ効率が上がるわけではない。
その点、これまでのOOPのFP化は、リスト中心主義ではない。
関数さえ辞書やリストにするので、関数さえオブジェクトにしているのだから、
汎オブジェクト主義といえるかもしれない。
だから、FPはクラスを使わない今風のOOP
ともいえるかも。
さて、
geogebraに目を向けよう。
geogebraはリストもリスト処理もできる。
関数的言語だけど、関数ではなくコマンドだ。値を返すx、yなどの関数のグラフはかける。
geogebraは図形上の対象、
ベクトル、点、点のあつまり、関数のグラフ、図形、文字列、コントロール、それらがオブジェクトだ。つまり、視覚化できるものがすべオブジェクトだ。
しかし、それらの関係性である関数や行列が極端に弱い。
これはそれ自体が図形上にものとしてあるわけではないからだ。
関数のグラフはあるが、関数一般はない。
それがgeogebraの基本設計だと思う。
だから、geogebraの仕様をみると、関数や変換は膨大だけれど、すべてグラフ化にかかわっている。
郷に入っては郷に従うということで、ナベアツをgeogebraでどうするかを考えてみよう。
課題:ナベアツ登場、1から100で変顔をどうやればできますか。
クラスは使わないがリストという物を使うオブジェクト指向でやってみよう。
Sequenceでリストオブジェクトを作り、Joinで合体して、Uniqueで重複をとる。
IsMulti3=Sequence(3*k, k, 1,33)
A =Sequence (30 + k, k,0,10)
B =Sequence (3 + 10 * k ,k, 1, 9)
AorB=Join(A,B)
Has3=Unique(AorB)
IsOrHas3=Join(isMulti3,Has3)
nabe=Unique(IsOrHas3)
res=Sequence(if(k∈nabe,"変顔",k+""),k,1,100)
num=slider(1,100,1)
text1=res(num)+""
text1を青で特大にして、
sliderをアニメーション、増大、スピードを適度(×0.15くらい)にすると、
text1がよいタイミングで変顔になったり、数のまま表示になります。
ポイントはresを作るとき、trueでもfalseでも文字列を返すように、
あえて、kではなく、k+""にするということです。これがstr(k)と同じ意味になります。
こうすることで、resのオブジェクトとして属性が文字列となります。
もし、resでkのままにすると、ずっとtrueの属性が文字列となり、それにあう変顔のみになります。
また、リストの要素の抜き出しはElement(res,num)が基本ですが、res(num)でも
動きますので、記述を短くするのに役立ちます。