3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ぁmbだ

Posted at

初めに

この記事は部活のアドベントカレンダーとして書かれています。
昨日はUnitの記事でした。

今回話すこと

今回はPythonのlambda式について話したいと思います。
その前にlambda式の基礎となってるlambda計算について軽く話したいと思います。
僕の理解も深くないです。たまによく主張が変わることがあります。
ここまで飛ばしていいよ。


ぁmbだ計算

ラムダ計算(ラムダけいさん、英語: lambda calculus)は、計算模型のひとつで、計算の実行を関数への引数の評価(英語: evaluation)と適用(英語: application)としてモデル化・抽象化した計算体系である。

wikipediaより引用

例えば、受け取った数+3の数を返す関数 $f(x) = x+3$ があるとすると、lambda計算では$$\lambda x.x+3$$と表現される。意味としては、引数$x$を受け取って、返り値として$x + 3$を返す関数。ということになる。

これと同じ
def _(x): return x+3

この関数に数を適用するときは$$(\lambda x. x+3)(3)$$と書き、これには冗長性があるので$$(\lambda x. x+3)3$$と書く。これは$\lambda x. x+3$を$f$とすると$f x$と書ける。
結果はもちろん$$(\lambda x. x+3)3 = 6$$ $$f 3 = 6$$となる。

lambda計算では関数が関数を受け取ることもできるし関数を返すこともできる。
どういうことかというと、こいうことだ。$$\lambda f x. f x$$この式の意味は、$f$と$x$を受け取って$f$に$x$を適用する関数。ということになる。
しかし、実はlambda計算で表される関数は一つの変数しか受け取ることしかできない。なので上の関数は$$\lambda f. (\lambda x. f x)$$と表され、この式は冗長性があり、$$\lambda f. \lambda x. f x$$となる。これは、$f$を受け取って、$x$を受け取り$f x$を返す関数を返す関数ということになる。
しかし、この記法はめんどくさいので、普通は、結局、$$\lambda f x. f x$$と書く。が、意味は$$\lambda f. \lambda x. f x$$と同じである。
つまりこう。

\begin{align}
& \quad (\lambda f x. f x)(\lambda y. y+3)5 \\
&= (\lambda x.(\lambda y. y+3)x)5 \\
&= (\lambda y.y+3)5 \\
&= 8
\end{align}

ルールたち

α変換

引数の名前は重要じゃないから名前変えても同じだよねってルール。
つまり$\lambda x.x = \lambda y.y$ってこと。

β簡約

関数適用のこと。

\begin{align}
& \quad (\lambda f x y. f x y)(\lambda g z. g z) \\
&= \lambda x y. (\lambda g z. g z)x y
\end{align}

$\lambda g z. g z$が$f$という変数に束縛される。$f$に$\lambda g z. g z$が拘束される => $\lambda g z. g z$に$f$という名前をつける。とも見れる。

η変換

あらゆる入力に対して同じ出力をする関数を考えたとき、それらは等価なので書き換えれるよねってルール。


lambda計算とは以下の文脈自由文法によって定義される。
<expr> ::= <id> (変数: identifier)
| λ <id>. <expr>  (関数: function)
| <expr> <expr> (関数適用: application)

これを見たら分かる通り、実は純粋なlambda計算には+*などの演算子は一切存在しない。数字すらもが存在しない。なので前述した関数が関数も受け取ることができるというのは間違いで、関数しか存在しない世界なのである。
そんな世界でどうやって計算するのって話はlambda式から離れていく(もちろんPythonのlambda式には数も演算子もある)ので別の機会か各々で調べて欲しい。1とても面白い世界。


ぁmbだ式

書いてた進捗が消えたのでやる気がなくなりました。

4.7.6. Lambda Expressions
Small anonymous functions can be created with the lambda keyword. This function returns the sum of its two arguments: lambda a, b: a+b. Lambda functions can be used wherever function objects are required. They are syntactically restricted to a single expression. Semantically, they are just syntactic sugar for a normal function definition. Like nested function definitions, lambda functions can reference variables from the containing scope:

python.orgより引用

意訳すると、無名関数をlambdaキーワードで定義できるよ。すべての関数オブジェクトの代わりになれるんだ。文法的に一つの式しか持てないように制限されてるけど、普通の関数の糖衣構文だね。ネストして定義もできるし、lambda式があるスコープの変数も参照できるよ。

クロージャみたいだね。

lambda式は一つの式しか持てないようになってるけど関係ない。(もともとlambda計算ってそういうもの)

文法
lambda arg: expression

用法

上のlambda計算の例
lambda x: x+3 # 定義
assert (lambda x: x+3)(3) == 6 # 適用

# もちろんこういうこともできる
lambda f, x: f(x)

# 関数を返す関数になるように書けば引数を一つずつ渡せる
assert (lambda f: lambda x: f(x))(lambda x: x+3)(5) == 8

lambda計算と似てる(それはそう)

いつ使うんだ

ここまで説明されたけどいつ使うか分からない。多分皆んなそう思っていることでしょう。一番の利点は、この関数定義は「式」であること、です。

は????

def文は文です。ですがlambda式は式です。ここに利点があります。

例えば、リストの中からfilter関数を使って1桁目と最後の桁の数字が同じ数字だけを取り出したいとします。lambda式を使わなければこうなります。

def
def first_digit_and_last_digit_are_same(x):
    s = str(x)
    return s[0] == s[-1]

l = [*range(1000)]
l = list(filter(first_digit_and_last_digit_are_same, l))

一回のfilterのためにこんな関数書かされるのは発狂ものです。
ですがlambda式を使うとこうです。

lambda
l = [*range(1000)]
l = list(filter(lambda x: (lambda s: s[0]==s[-1])(str(x)), l))

中間変数が消えます。やったね。

[*range(100)]が何してるか分からない人のために

*generator *iteratorで中身が展開されます。

star_expression
assert [*range(3)] == [0, 1, 2]
a = (2, 3)
assert (lambda x, y: x + y)(*a) == 5

便利だ。


終わりに

多分僕が簡単に出した例でさえ理解するのが難しいと思います。はい、理解しなくてもいいです。見て分かる通りlambda式じゃないといけない場面はありません。
でも、その基になってる数学ってとても面白いんですよ。皆んな数学しよう。


明日はUnitの記事です。

超絶大事な話をされると思うので、今日は早く寝て明日の記事を待ちましょう。


続きます

続いてもlambda式の話です。

lambda式を書いてて、これってどうしたらいいんだ?ってことについて書いて行きます。
実用的ではないです。

ほぼOne-Linerの話です。

代入

多分一番最初にやりたくなると思います。関数型に慣れてるわけでもない人からすると、代入==プログラミングと思ってるはずです。(違うか)
しかし代入文は文なのでlambda式の中には書けません。悲しいね。

a = 3
# lambda : a = 3 # can't assign to lambda
# lambda : (a = 3) # invalid syntax

ですが安心してください。Pythonはlambda式の中での代入をサポートする仕組みを作ってます。
実はPython、グローバル変数テーブルにアクセスできます。

代入
a = 3
(lambda: globals().__setitem__("a", 5))()
assert a == 5

なにをやっているかというと、まずglobals()2でグローバル変数のテーブルを取得します。これはdictです。そしてdictに値を入れるとき__setitem__(key, val)が内部的に呼ばれます。いわゆるマジックメソッド3です。なのでそいつを直接呼んでいるわけです。

dict
assert type(globals()) == dict
a = dict()
a["a"] = 3
a.__setitem__("a", 3) # 意味は同じ

インスタンスのメンバに代入したいときはもっと単純です。

メンバの更新
class A():
    pass

a = A()
a.a = 3
(lambda: a.__setattr__("a", 5))()
assert a.a == 5

さっきとほぼ同じ原理です。メンバの値を変えるとき内部的に__setattr__(name, val)が呼ばれているので、それを明示的に呼んであげた。それだけです。

これで代入マスターだ。やったね。

ですが!僕はこんなことしません。気持ち悪いからです。副作用を書くなと声を大にして言いたい。
じゃあ代入どうするの...
諦めます。というか必要ないと思います。lambda式の中で代入文が欲しくなるときの大部分が一度計算した値を使い回したいケースだと思います。それはできます。lambda計算の$\beta$簡約のところでやりました、引数が変数に拘束されるという見方です。

lambda x: (lambda y: [print(x), print(y)])(str(x*x+2*x+1))

内側のlambda式からはxyも参照できます。実質代入。

分岐

if文ですね。まあ使いたくなるよね。いろいろ方法はあります。

# 三項演算子
assert (lambda b: "true" if b else "false")(True) == "true"
assert (lambda b: "true" if b else "false")(False) == "false"

# 短略評価
assert (lambda b: b and "true" or "false")(True) == "true"
assert (lambda b: b and "true" or "false")(False) == "false"

# リストインデックス
assert (lambda b: ["false", "true"][b])(True) == "true"
assert (lambda b: ["false", "true"][b])(False) == "true"

三項演算子と短絡評価は片方の式だけしか評価(=実行)されません。
しかしリストインデックスはすべての要素が評価されます。インデックスにintを渡せば、3つ以上の分岐、Pythonにはないcase文みたいなこともできます。(全部評価されちゃうけど)

リストインデックス
lambda x: [f(x), print("Not Implemented.")][0]

みたいな感じに返したい値とは別に評価したい関数があるときに便利です。

繰り返し

for文です。流石にできないだろ...
できます。
そのために不動点コンビネータを勉強してきてください。

勉強してきましたか?
お疲れ様です。その知識いりません。
YコンビネータだのZコンビネータだのいちいち難しいんですよ。僕には理解できません。

要するに再起呼び出しができればいいんです。
じゃあどうやったら再起ができるか、関数のインスタンスを拘束できたらいいんですよ。
ってなったら簡単ですね。

階乗
lambda x: (lambda f, x: f(f, x))((lambda f, x: x*f(f, x-1)if x else 1), x)

assert (lambda x: (lambda f, x: f(f, x))((lambda f, x: x*f(f, x-1)if x else 1), x)(5) == 120 # 5!

簡単じゃないですね。
一番外側のlambda式は初期値を渡しているだけなので、そこを外してじっくり考えると分かるかもしれません。

素直に内包表記使ってください。

階乗
lambda x: functools.reduce(lambda a,b:a*b, [*range(1, x+1)])

assert (lambda x: functools.reduce(lambda a,b:a*b, [*range(1, x+1)])(5) == 120

import文

やりたくない?そうでもない?そうかあ

階乗
lambda x: (lambda ft: ft.reduce(lambda a,b:a*b, [*range(1, x+1)]))(__import__("functools"))

class

実はPythonのclassはtypeオブジェクトのインスタンス。なので適当にパラメータを設定してあげたらclassが出来上がる。

A = type("A", (object, ), {})
a = A()
a.f = lambda x: x+3
assert a.f(4) == 7

selfが勝手に渡されないのはどうしたらいいんだろう。誰か教えて。
外のスコープからインスタンスを参照するとそれっぽいことはできるけど。


終わりに

ここまで呼んでもやっぱりほとんど理解できてないと思います4。大丈夫、必要ないから。

  1. SKIコンビネータ、チャーチ数とか調べてみるといいかも。

  2. locals()もあるよ。

  3. ここで勉強して。 http://diveintopython3-ja.rdy.jp/special-method-names.html

  4. もうlambdaなんて見たくないよう( ´:ω:` )

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?