FP:純粋関数のメモリ不変エリアで、高速化する。
このワークシートはMath by Codeの一部です。
関数型プログラミングとは関数を使うだけではありません。
どのように使うかまで、決めてあるのです。
1.関数は原則、純粋に限る。
2.関数は副作用がない使い方をする。変数は不変性を維持する。
今回は、関数型プログラミングの基本中の基本を再確認しましょう。
1.関数は原則、純粋に限る
<関数は原則、純粋に限る>
純粋な関数とは数学の関数です。数学の写像です。
写像(しゃぞう、英: mapping, map)とは、ある集合の要素に対し、別の集合の要素をただ一つずつ対応させる規則ですね。
プログラミングのコトバ遣いでは、こうなります。
・入力と出力があること
・入力に対して出力が変わることがない。
これが純粋な関数、純粋関数のルールですね。
出力とはPythonではリターン文のことです。
printはメモリ世界の外に飛び出しているだけで、出力とは言いません。
プリント文だけの関数はたんなる命令であり純粋関数ではありませんね。
では、なぜ入力が決まる出力が必要なのでしょうか。
純粋関数をリレーすることが可能になり、純粋関数の連結が純粋関数になれるというメリットがあります。また、出力が一定することで、関数としての信頼性が増すからです。
ただし、「原則」、純粋に限るとしたのは、FP思想の必要な領域の中の世界です。
たとえば、決まった数ではなく、乱数を入力したいときは、入力後だけFPの世界にすればよいですね。
乱数だけはなく、現在時刻、ユーザーからの入力だってそうですね。純粋さはありません。
また、FPの処理結果をメモリ世界から取り出して、外に表示したいときは、print文が必要です。
画面表示だけでなく、ファイルへの書き込みもデータ処理から見れば、不純な外に出すことです。
だから、FP世界を固めて1つの関数として考えたとき、その中が純粋であればよいのです。
その入り口に入るときと、出口から出るときはクリーンな副作用のない世界とは関係ありません。
そこは不純でいいでしょう。
これは、HaskellでわざわざモナドMonadなどで不純さOKの部分さえ理論化して
区分けしていたことを思い出せばよいでしょう。
この世は純粋と不純の両方でできているのです。
理想世界の外は現実が渦巻いているでしょ。
たとえば、
import random
# 【不純な現実世界】外側で生々しいデータを捕まえる
lucky_number = random.randint(1, 100)
# 【純粋な理想世界】門をくぐった後は、ただの固定値としてクリーンに処理
def pure_process(x):
return x * 2 # いつ叩いても、同じ入力には同じ出力を返す
result = pure_process(lucky_number)
こんな感じです。
<不純な関数の例>
だから、
入力があっても出力がない、リターンがないのは不純ですね。
def Nonpure(x):
print("I am not pure")
入力がないのもダメです。
def AllOk():
x = 999
retrun x
入力も出力もあるが、出力が一定しないのもダメです。
numbers = [1, 2, 3, 4, 5]
def shuffling( numbers ):
return random.shuffle(numbers)
<純粋関数>
では、純粋な関数の例はというと、
数学の関数ならみな当てはまるでしょう。
def linearFunc(x):
return 3 *x +4
def f(x, y):
return x+y
def cubic(x):
return x**3
など、数学関数はフリーパスです。
数学関数をバケツリレーしても純粋関数のままですね。
f(linearFunc)もlinearFunc(f)も数学関数ですからね。
変数のタイプは数に限らないので、数学関数でなくも大丈夫
文字連結がたし算でできます。
a="純粋.txt"
b="関数.txt"
def addString(a,b):
return a+b
リスト連結もたし算でできます。
a=['p','u','r','e']
b=['関','数']
def addList(a,b):
return a+b
range(a,b)も関数です。
list(range(5, 10))は[5, 6, 7, 8, 9]を毎回返しますね。
2.関数を純粋化することで、メモリ破壊から救おう
関数を純粋化することで、メモリ破壊から救おう
<不純追加を純粋追加にしよう>
たとえば、リスト更新で、1つ要素を足したいとします。
Old=list(range(1,10))
print(Old)
Old.append(10)
print(Old)
#Oldは更新されて、末尾が10になってますね。
def update_list(original_list, value):
newjist = original_list + [value]
return newjist
Old=list(range(1,10))
New=update_list(Old, 10)
print(f"Old:{Old}")
print(f"New:{New}")
[OUT]
Old:[1, 2, 3, 4, 5, 6, 7, 8, 9]
New:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
appendの代わりに結果をNewに渡してやれば、Oldは破壊されず、新品のNewができます。
これは、y=x+10の処理で、xを破壊せずに、新品のyを作る。
予測可能でバグのおきない処理だから、数学と同じですね。
<不純更新を純粋更新にしよう>
タプルはイミュータブルなデータなので、更新しようとすると
エラーになります。
def increment(values):
for i in range(len(values)):
values[i] = values[i] + 1
return values
original_list=[1,2,3]
print(increment(original_list))
original_tuple=(1,2,3)
print(increment(original_tuple))
[OUT]
[2, 3, 4]
TypeError: 'tuple' object does not support item assignment
更新できないはずのタプルですが、中で新リストをつくり、それから新タプルにして返すと
加算したタプルが新品でできます。これはみかけデータの更新で、もとのタプルは壊れてません。
def increment_values(values):
return tuple([value + 1 for value in values])
original_tuple = (1, 2, 3)
new_tuple = increment_values(original_tuple)
print(new_tuple)
[OUT]
(2, 3, 4)
更新できないはずのタプルを更新しようとすること自体、
動機自体が不純なので、それはもうやめにして、有名関数increment_valuesを使わずに
1ふやすほかの方法はあります。それは、例のラムダ式ですね。
nums = [1, 2, 3]
news = list(map(lambda x: x + 1, nums))
print(news)
[OUT]
[2, 3, 4]
<バケツリレーは同名でもOK>
部品関数のバケツリレーをするときに、
格納する名前を毎回変えなければと思いますよね。
もちろんメモリ上も名前も変えると安心ですね。
def trim_whitespace(s):
return s.strip()
def to_lowercase(s):
return s.lower()
def remove_punctuation(s):
import string
return s.translate(str.maketrans('','',string.punctuation))
def clean_text(s):
t = trim_whitespace(s)
u = to_lowercase(t)
v = remove_punctuation(u)
return v
Sn=" With This Modular Approach, The Overall Logic Is MORE Transparent .!?"
print(clean_text(Sn))
print(Sn)
[OUT]
with this modular approach the overall logic is more transparent
With This Modular Approach, The Overall Logic Is MORE Transparent .!?
しかし、結果の代入は新品メモリにはいるので、同名でもパイプラインは動きます。
ただし、名前による歴史探訪はできなくなります。
def trim_whitespace(s):
return s.strip()
def to_lowercase(s):
return s.lower()
def remove_punctuation(s):
import string
return s.translate(str.maketrans(’’, ’’, string.punctuation))
def clean_text(s):
s = trim_whitespace(s)
s = to_lowercase(s)
s = remove_punctuation(s)
return s
Sn=" With This Modular Approach, The Overall Logic Is MORE Transparent .!?"
print(clean_text(Sn))
print(Sn)
[OUT]
with this modular approach the overall logic is more transparent
With This Modular Approach, The Overall Logic Is MORE Transparent .!?
どういうことでしょうか?
clean_textは入れたsをsの名前のままバケツリレーして、最後もsですね。
しかし、sにはSnの中身が入りますが、最後のreturnはsですから、Snの名前で返しているわけでは
ありません。この関数はSnという名前を知りませんから、Snに戻すことはできないのです。
だから、この関数はsを更新しているわけではありません。
最後にSnを表示しても、Snは無傷です。clean_textはイミュータブルです。
逆にclean_textを外から見ると、sの名前でメモリは取り出せません。sがどう変化しているかの
歴史をたどることはできませんね。では1つ1つの部品関数はどうでしょうか。
trim_whitespace(s)はsに入ったものをsを変えずにs.strip()を返すので出たものはsとは別物です。
だから、s = trim_whitespace(s)は一見上書き更新に見えますが、同名sの新品にsとは別メモリのものを
入れているので、上書き更新ではなく、同名の別物上書きです。だから、純粋です。
だから、clean_textの部品1つ1つが純粋であり、たまたま同名リレーですが、別メモリを作りながら
変えしているので、もとを壊すことがない。これが連続しているのです。だから、中身も純粋ですね。
<同じ計算式は答えをメモして再計算しない>
再帰計算をするときに、メモ化のデコレタを使うと、同じ計算を繰り返したり、結果を上書きしなくなるので、計算の効率的にしかも、純粋にすることができます。
たとえば、
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(100)
[OUT]
354224848179261915075
不変性はメモリ保存だけでなく、計算の効率化にもつながるんですね。
課題:geogabraのフィボナッチ数列で、メモ化の有無を感じよう。
n=slider(1,100,1)
lst=IterationList(a+b,a,b,{1,1},n)
fib=lst(n)
text1=fib+""
Pythonでさえ、メモ化デコレタをつけて100までの計算を工夫したのだが、
geogebraにはデコレタがない。
しかし、スライダーnが100近くでもスムーズに計算するようなら、
内部でデコレタつきにIterationListを設計していると思われます。
ただ、有効数字の関係で浮動小数表示になってしまいますね。
しかし、スムーズに動きますよ。
geogebraはすごい。