このサイトは学部では早稲田で物理を, 修士では東大で数学を専攻し, 今も非アカデミックの立場で数学や物理と向き合っている一市民の奮闘の記録です. 運営者情報および運営理念についてはこちらをご覧ください.
中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては自然を再現しようや役に立つ中高数学 中高数学お散歩コース
大学数学に関しては現代数学観光ツアーなどの無料の通信講座があります.
その他にも無料の通信講座はこちらのページにまとまっています.
ご興味のある方はぜひお気軽にご登録ください!
目次
素数夜曲とこれまでの内容
素数夜曲
これまでの内容
- 素数夜曲とプログラミング B-B.1.2 Scheme, Python, Haskell, JavaScript 第 1 回
- 素数夜曲とプログラミング B.2-B.4 Scheme, Python, Haskell, JavaScript 第 2 回
緩募 Emacs で Scheme やるときに lispxmp 的なことをやりたい
いまさらだがいちいち write
をかませるのがあまりに不細工だし,
何より本当にめんどくさい.
lispxmp 的なことがやりたい.
どなたか情報をご存知だったら教えてほしい.
B.4.3 Let
let
は次の lambda
の糖衣構文として定義されている.
((lambda ([var_1] [var_2] ... [var_n]) [body])
[exp_1] [exp_2] ... [exp_n])
let
は次の通り.
(let (([var_1] [exp_1])
([var_2] [exp_2])
...
([var_n] [exp_n]))
[body])
let
には let*
と letrec
, 名前つき let
という 3 種類の派生がある.
$a=2$, $b=3$ に固定した一次関数を lambda
と let
で書く.
Lambda 記法で $x$ に関する関数抽象を括り出す1.
((lambda (x)
((lambda (a b)
(+ (* a x) b)) 2 3))
5)
(write ((lambda (x)
((lambda (a b)
(+ (* a x) b)) 2 3))
5))
RESULT
13
$x$ に対する値 5 を取り除いて改めて関数 f_2x+3
を定義する.
(define f_2x+3
(lambda (x)
((lambda (a b)
(+ (* a x) b)) 2 3)))
(write (f_2x+3 5))
RESULT
13
let
を使った方がみやすい.
(define f_2x+3
(lambda (x)
(let ((a 2) (b 3))
(+ (* a x) b))))
(write (f_2x+3 5))
RESULT
13
Haskell 版: let と where?
Python や JavaScript だとあまり言うことない気がする.
JavaScript なら use strict;
と let
はあってもここの文脈での Scheme の let
とは違うという理解.
Haskell だと多分また違う話として let
と where
がある.
まだあまりピンと来ていないがスコープとかその辺に違いがある.
どういう場合にどちらがいいのかは,
Haskell をもっとたくさん書くとわかるようになるのだろう.
where
の方が一般的に使われるとか何とか聞いてはいる.
とりあえず書いておく.
f0 :: Int -> Int
f0 x =
let a = 2
b = 3
in a * x + b
main = do
print $ f0 5
RESULT
13
f1 :: Int -> Int
f1 x = a * x + b
where a = 2
b = 3
main = do
print $ f1 5
RESULT
13
クロージャ
関数の本体の評価値を調べる.
(write (lambda (x) ((lambda (a b)
(+ (* a x) b)) 2 3)))
RESULT
#<procedure 110ac27c0 at <current input>:87:41 (x)>
参照される変数の定義, 定数の値を含んだ環境と関数抽象の組をクロージャあるいは関数閉包と呼ぶ.
$a=2$, $b=3$ はクロージャの中でだけ有効で,
外部に影響を与えないし影響も受けない.
外部とのやりとりは変数 $x$ が受け持つ.
スキームはレキシカルスコープを採用している.
クロージャ, いまだに何なのかよくわかっていない.
何というか半端に具体的にプログラミング言語と絡めて説明されるからよくわからないのではないか,
という感じもする.
ラムダ計算から見た数学ベースの定義はどうなっているのだろう.
いろいろなプログラミング言語ごとの事情よりもそちらを見た方がわかるのではないか感.
Python 版
ここを見ると次のようにある.
- 関数内のローカル変数以外の名前解決が、
- 呼び出し時のスコープではなく、
- 宣言時のスコープによって行われるもの。
- またそのような機能を持った関数のこと。
例が面白かった.
x = 1
def get_x():
return x
class MyClass(object):
x = 10
print(get_x()) # 10 ではなく 1 が返される
RESULT
1
次のコードには驚いた.
circle_area()
が gen_circle_area_func()
の外から呼べている.
他の言語でこんなことできる?
def gen_circle_area_func(pi):
"""円の面積を求める関数を返す"""
def circle_area(radius):
# ここの pi は circle_area_func に渡された pi が使われる
return pi * radius ** 2
return circle_area
circle_area1 = gen_circle_area_func(3.14)
print(circle_area1(1)) # => 3.14
print(circle_area1(2)) # => 12.56
circle_area2 = gen_circle_area_func(3.141592)
print(circle_area2(1)) # => 3.141592
print(circle_area2(2)) # => 12.566370614
RESULT
3.14
12.56
3.141592
12.566368
Haskell 版
クロージャとは、その関数が定義された環境(変数及びスコープ)への参照を持った関数のことです。
クロージャは英語で「閉鎖」という意味です。
(実際には、クロージャの概念自体が言語によって定義が異なります。)Haskellにおけるクロージャ
Haskellは純粋関数型言語であるため、JavaScriptなどのクロージャと違い、変数(再束縛)がありません。
そのため、関数から無名関数を返す際に無名関数の外側の値を束縛することがHaskellにおけるクロージャといえます。
さらに広義にとらえれば、関数内で一時的な関数を生成してそれを戻り値とするような「関数を返す関数」をクロージャといえます。
(実際には「無名関数の部分適用」と類似した機能といえます。)
上記ページに出てくるコードの挙動はわかるが,
どこをもってどうクロージャと呼んでいるのだろうか.
add :: Integer -> Integer -> Integer
add x y = x + y
main = print(add 5 2)
上のコードのクロージャ化 2 を引いておこう.
add :: Integer -> Integer -> Integer
add x y = f x y
where f a b = a + b
main = print $ add 5 2
RESULT
7
JavaScript 版
JavaScriptでは関数はすべてクロージャです。
クロージャの簡単な定義として
「自分を囲むスコープにある変数を参照できる関数」
が挙げられます。
これはまあわかる, というコード例.
function func() {
var value = 1;
console.log(value);
}
func(); // 1
// console.log(value); // undefined, エラーで怒られるのでコメントアウト
RESULT
1
クロージャの例.
これもわかる.
function func() {
var value = 1;
function innerFunc() {
console.log(value);
}
innerFunc();
}
func(); // 1
RESULT
1
追記: クロージャとスコープ
またコメントを頂いた.
Python での circle_area にずいぶん驚いているようですが、それが「クロージャが環境を保存する」「クージャ (関数) が第一級 (値としてやりとりできる存在) である」ということです。 この記事で取り上げられている他の言語でも出来ますよ。
スコープを全然理解していないことが明らかになっていてとてもつらい.
何はともあれコードまで教えて頂いたのでこちらにも転記しておこう.
実にありがたい.
Scheme に関してはこんなコメントも.
;; 余談ですが、 Scheme ではローカル環境での define (internal definition
;; と呼ばれています) は letrec 又は letrec* で定義したのと同じです。
;; r5rs までは letrec、 R6RS 以降は letrec* での定義と等しいと規定されています。
Scheme 版
(define (gen_circle_area_func pi)
(define (circle_area radius)
(* pi radius radius))
circle_area)
(let ((circle_area1 (gen_circle_area_func 3.14)))
(display (circle_area1 1))
(newline)
(display (circle_area1 2))
(newline))
RESULT
3.14
12.56
Haskell 版
gen_circle_area_func pi =
let circle_area radius = pi * radius * radius
in circle_area
main = do
print $ circle_area1 1
print $ circle_area1 2
where circle_area1 = gen_circle_area_func 3.14
RESULT
3.14
12.56
JavaScript 版
function gen_circle_area_func(pi) {
function circle_area(radius) {
return pi * radius * radius;
}
return circle_area;
}
var circle_area1 = gen_circle_area_func(3.14);
console.log(circle_area1(1));
console.log(circle_area1(2));
RESULT
3.14
12.56
B.4.4 引数の書法
lambda
の引数部はリストになっている.
3 次元のユークリッド空間の原点からの距離 (の 2 乗) を計算する関数は次のように定義できる.
(lambda (x y z)
(+ (* x x) (* y y) (* z z)))
$x=2, y=3, z=5$ のときの値を見る.
(define d ((lambda (x y z)
(+ (* x x) (* y y) (* z z)))
2 3 5))
(write d)
RESULT
38
仮引数の全てに値を与えないとエラーになる.
Python の場合は次の通り.
def dist(x, y, z):
return x**2 + y**2 + z**2
print(dist(2, 3, 5))
RESULT
38
Haskell の場合は次の通り.
dist :: Int -> Int -> Int -> Int
dist x y z = x^2 + y^2 + z^2
main = do
print $ dist 2 3 5
RESULT
38
JavaScript の場合は次の通り.
function dist(x, y, z) {
return Math.pow(x, 2) + Math.pow(y, 2)+ Math.pow(z, 2);
}
console.log(dist(2, 3, 5));
RESULT
38
ドット対
引数のデフォルト値とかその辺に関する話のようだが,
いまひとつピンと来ない.
とりあえず本の記述を優先させて書こう.
必須引数とそれ以外の引数をわけて設定できる.
(define a ((lambda (x y . z)
(+ (* x x) (* y y) (* 5 5))) 2 3))
(write a)
RESULT
38
z
に値を指定していないがエラーにならない.
デフォルト値と言われると次のようなのを想像した.
def dist(x, y, z=5):
return x**2 + y**2 + z**2
print(dist(2, 3))
print(dist(2, 3, 4))
RESULT
38
29
見たところインタプリタが Gauche ならこんな形があるらしい.
; Gauche の場合
optional (x x-default) (y y-default) z
Scheme として一般的にはどうなっているのだろう?
何はともあれオプションの引数はリストとしてわたる.
そのリストからの値取り出しには car
, cdr
を使う.
car
はリストの先頭の値を取り出し, cdr
はそれ以外をリストで取り出す.
(define l '(1 2 3))
(write l)
(newline)
(write (car l))
(newline)
(write (cdr l))
RESULT
(1 2 3)
1
(2 3)
Python 版
さっき書いたがもう一度.
def dist(x, y, z=5):
return x**2 + y**2 + z**5
print(dist(2, 3))
print(dist(2, 3))
RESULT
3138
3138
Python での car
, cdr
の対応物.
a = [1, 2, 3]
print(a[0])
print(a[1:len(a)])
b = [1]
print(b[0])
print(b[1: len(b)])
RESULT
1
[2, 3]
1
[]
b[1: len(b)]
はエラーになるかと思ったがセーフだった.
Haskell 版
オプションの引数をどう定義したらいいのかよくわからなかった.
car
, cdr
は head
, tail
と自然に意味が取れる関数名になっている.
main = do
let a = [1,2,3]
print $ head a
print $ tail a
RESULT
1
[2,3]
要素が 2 つだけのタプルに対して 1 番目, 2 番目を取り出す関数として
fst
, snd
がある.
初見でわかりづらい可能性があるので補足しておくと,
first
, second
の略だ.
fst
と snd
はタプルで要素が 2 つある場合にしか使えない.
リストでも駄目だし, タプルで 3 つ以上あっても駄目.
実際にコードを書いて実験するとわかる.
main = do
let a = (1,2)
print $ fst a
print $ snd a
RESULT
1
2
JavaScript 版
ここを見るとデフォルト値設定をできないことはないが,
ウルトラ面倒そう.
car
と cdr
は次のような感じか.
var a = [1,2,3]
console.log(a[0]);
console.log(a.slice(1, a.length));
RESULT
1
[ 2, 3 ]
リストの生成
括弧がない次のような場合を考えたい.
(lambda x x)
この lambda 項はこれだけで任意個数の実引数を受けとる.
評価値はリストで引数に束縛される.
(define a ((lambda x x) 1 2 3))
(write a)
RESULT
(1 2 3)
これで与えられた要素からリストを作れる.
組み込みの list
はこの糖衣構文である.
(define list (lambda x x))
(write (list 1 2 3))
RESULT
(1 2 3)
MIT 記法では次の通り.
(define (list . x) x)
(write (list 1 2 3))
RESULT
(1 2 3)
Python 版
a = [1, 2, 3]
print(a)
RESULT
[1, 2, 3]
Haskell 版
a = [1, 2, 3]
main = do print $ a
RESULT
[1,2,3]
JavaScript 版
リストはない.
代わりは配列か.
var a = [1, 2, 3]
console.log(a);
RESULT
[ 1, 2, 3 ]
B.4.5 引数の無い関数
(write ((lambda () (* 2 3))))
RESULT
6
比較のために次の定義をしてみよう.
(define late (lambda () (* 2 3)))
(define now (* 2 3))
(write (late))
(newline)
(write now)
RESULT
6
6
この機能を使うと値呼びシステムの中に名前呼び評価を実現させられるとのこと.
まだよくわかっていない.
MIT 記法を使うと上の 2 つは次のようにも書ける.
(define (late) (* 2 3))
(define now (* 2 3))
(write (late))
(newline)
(write now)
RESULT
6
6
同種の機能は最初から Scheme に組み込まれている.
(write (delay (* 2 3)))
(newline)
(write (force (delay (* 2 3))))
RESULT
#<promise #<procedure 10a9204a0 at <current input>:118:41 ()>>
6
delay
は評価値を出力しないで保留する.
force
ではじめてその評価値を出力する.
この遅延を含んだ計算がプログラムの発想を大いに変えるらしい.
まだよくわかっていない.
Python, Haskell, JavaScript で何か対応するものはあるだろうか?
B.4.6 抽象化の問題
要は中身は無視して同じ入力に対して同じ出力を返すかどうかを見るとかそういう感じの話.
具体例としては次の 3 つ.
(define five (lambda (x) (* 5 x)))
(define five' (lambda (y) (+ y y y y y)))
(define five'' (lambda (z) (/ (* 10 z) 2)))
(write (five 2))
(newline)
(write (five' 2))
(newline)
(write (five'' 2))
RESULT
10
10
10
中身は違うが入力した数を 5 倍する作用は同じ.
B.4.7 quote
リテラルを増やす方法.
"Gauss"
と入力するとこれはリテラルと見なされて評価値は "Gauss"
になる.
引用符の無い文字列そのものを評価値とする方法があって,
それが quote
だ.
(write (quote Gauss))
(newline)
(write "Gauss")
(newline)
(write (quote "Gauss"))
(newline)
(write 'Gauss)
RESULT
Gauss
"Gauss"
"Gauss"
Gauss
最初の 2 つの評価の違いは何なのだろう?
参考のために 3 つめを追加した.
write
をかませていることはあるにせよよくわかっていない.
ちなみに quote
はよく出てくるので '
で代用できるようになっている.
もちろん糖衣構文というやつだ.
Scheme はまず引数の評価値を求める.
だから評価されて困るなら quote
しないといけない.
今まで elisp もちょこちょこ書いていたがいまだにこの辺がよくわかっていない.
本にも書いてある次のコードを見てようやく何かわかるような気がしてきた気もする.
(write (+ 2 3))
(newline)
(write '(+ 2 3))
RESULT
5
(+ 2 3)
評価しないでそのまま出力するというのはこういう感じ.
これがプログラムをデータをしてやりとりできる
LISP 系の言語の特性とかいうのはよく聞くもの,
まだきちんとわかった気はしない.
この逆が eval
.
この辺を見てみたがよくわからない.
eval
の第 2 引数の指定の仕方で,
本に書かれた次の指定は Gauche 前提の実装で,
他のコンパイラでうまくいく保証がないようだ.
Guile で実行しているこのミニ講座という名の自前まとめではどうなるのかよくわからない.
(eval '(+ 2 3) (interaction-envirionment))
これ, Guile の REPL (と言ってもいい? コマンドラインで guile
と叩いて実行するやつ) で
実行したらエラーになる.
ちなみに次の 2 つもエラーになる.
(eval 'x (environment '(a library)))
(eval 'x (scheme-environment 5))
エラーメッセージは次の通り.
scheme@(guile-user)> (eval 'x (environment '(a library)))
;;; :1:9: warning: possibly unbound variable `environment'
:1:9: In procedure #:1:0 ()>:
:1:9: In procedure module-lookup: Unbound variable: environment
scheme@(guile-user) [1]> (eval 'x (scheme-environment 5))
;;; :2:9: warning: possibly unbound variable `scheme-environment'
:2:9: In procedure #:2:0 ()>:
:2:9: In procedure module-lookup: Unbound variable: scheme-environment
よくわからない.
ちなみにこの quote,
他の言語で対応する概念あるのだろうか.
マクロと何か関係あるとか何とかいうのを聞いたこともあるが,
全くわからない.
ちょっと調べてみたが一応いろいろあるらしい.
いったん「よくわからない」箱に入れておいて適当なときに勉強しよう.
リストとの関係
四則演算の手続も記号としてリストにできるとか何とかいう次のサンプルコードがある.
(write ((car (list + - * /)) 5 3))
(newline)
(write ((cadr (list + - * /)) 5 3))
(newline)
(write ((caddr (list + - * /)) 5 3))
(newline)
(write ((cadddr (list + - * /)) 5 3))
RESULT
8
2
15
5/3
caddr
は car
と cdr
を適当に組み合わせた関数だ.
4 段の深さまで計 28 個ある.
コード内に空リストを書きたいなら次のように書く.
(write '())
RESULT
()
数値はリテラルだから空リストとの cons
でそれを唯一つの要素とするリストが作れる.
(write (cons 7 '()))
RESULT
(7)
文字はリテラルではないから quote
が必要.
(write (cons 'b '()))
RESULT
(b)
cons
を何度も重ねるのもめんどいので list
を使うのがいい.
(write (list 'a 'b 'c))
RESULT
(a b c)
list
は lambda 項 (lambda x x)
だった: (define list (lambda x x))
.
リストを繋ぎたいなら append
.
(write (append '(a b) '(c d)))
RESULT
(a b c d)
念のため cons
と append
の違いを見ておこう.
(write (cons '(a b) '(c d)))
(newline)
(write (append '(a b) '(c d)))
(newline)
(write (append '((a b)) '(c d)))
RESULT
((a b) c d)
(a b c d)
((a b) c d)
car
と cdr
の再確認.
(write (car (list 'a 'b 'c)))
(newline)
(write (cdr (list 'a 'b 'c)))
RESULT
a
(b c)
2 重の quote
に対する分析.
(write (car (quote (quote Euler))))
(newline)
(write (car ''Euler))
RESULT
quote
quote
-
関数抽象, 読み飛ばしたからか何のことかわからない.
索引もちょろっと見たらウルトラ使いづらい. ↩
中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては自然を再現しようや役に立つ中高数学 中高数学お散歩コース
大学数学に関しては現代数学観光ツアーなどの無料の通信講座があります.
その他にも無料の通信講座はこちらのページにまとまっています.
ご興味のある方はぜひお気軽にご登録ください!
Python での circle_area にずいぶん驚いているようですが、それが「クロージャが環境を保存する」「クージャ (関数) が第一級 (値としてやりとりできる存在) である」ということです。 この記事で取り上げられている他の言語でも出来ますよ。
JavaScript
function gen_circle_area_func(pi) {
function circle_area(radius) {
return pi * radius * radius;
}
return circle_area;
}
var circle_area1 = gen_circle_area_func(3.14);
console.log(circle_area1(1));
console.log(circle_area1(2));
Scheme
(define (gen_circle_area_func pi)
(define (circle_area radius)
(* pi radius radius))
circle_area)
(let ((circle_area1 (gen_circle_area_func 3.14)))
(display (circle_area1 1))
(newline)
(display (circle_area1 2))
(newline))
;; 余談ですが、 Scheme ではローカル環境での define (internal definition
;; と呼ばれています) は letrec 又は letrec* で定義したのと同じです。
;; r5rs までは letrec、 R6RS 以降は letrec* での定義と等しいと規定されています。
Haskell
gen_circle_area_func pi =
let circle_area radius = pi * radius * radius
in circle_area
main = do print $ circle_area1 1
print $ circle_area1 2
where circle_area1 = gen_circle_area_func 3.14
ありがとうございます。
びっくりしたのはスコープ的な意味で
関数の中の関数(クロージャ)に関数の外からアクセスできるのか、
というところです。
理解の雑さが明らかになっていてとてもつらい。
これから記事(の元原稿)にも追記します。
プログラミング言語の用語としての「スコープ」は一般的には「その名前で参照できる範囲」のことをいい、この場合にスコープという用語を使うのは妥当ではありません。 外側からその名前が見えるわけではありませんので。 (ひょっとすると分野によっては別の使い方をすることもあるのかもしれませんが。)
https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97
クロージャの訳語として「閉包」ということがあるようですが、ローカル変数への参照などを包んで閉じ込めたオブジェクトだというような考え方で捉えるとわかりやすいかと思います。
その辺りを正確に理解していないのが元凶であることは認識しました。
もにょもにょしている部分を正確に言葉にできないので理解まではしばらくかかりそうです。
経験上、こういう状態になると理解に至るまで数ヶ月から年単位かかることもあるのでじっくり付き合う予定です。