素数夜曲とプログラミング B.4.3-B.4.7 Scheme, Python, Haskell, JavaScript 第 3 回

この記事は12分で読めます

このサイトは学部では早稲田で物理を, 修士では東大で数学を専攻し, 今も非アカデミックの立場で数学や物理と向き合っている一市民の奮闘の記録です. 運営者情報および運営理念についてはこちらをご覧ください.

中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては自然を再現しよう役に立つ中高数学 中高数学お散歩コース
大学数学に関しては現代数学観光ツアーなどの無料の通信講座があります.
その他にも無料の通信講座はこちらのページにまとまっています.
ご興味のある方はぜひお気軽にご登録ください!


素数夜曲とこれまでの内容

素数夜曲

これまでの内容

緩募 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$ に固定した一次関数を lambdalet で書く.
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 だと多分また違う話として letwhere がある.
まだあまりピンと来ていないがスコープとかその辺に違いがある.

どういう場合にどちらがいいのかは,
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, cdrhead, 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 の略だ.

fstsnd はタプルで要素が 2 つある場合にしか使えない.
リストでも駄目だし, タプルで 3 つ以上あっても駄目.
実際にコードを書いて実験するとわかる.


main = do
  let a = (1,2)
  print $ fst a
  print $ snd a

RESULT

1
2

JavaScript 版

ここを見るとデフォルト値設定をできないことはないが,
ウルトラ面倒そう.

carcdr は次のような感じか.


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

caddrcarcdr を適当に組み合わせた関数だ.
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)

念のため consappend の違いを見ておこう.


(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)

carcdr の再確認.


(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

  1. 関数抽象, 読み飛ばしたからか何のことかわからない.
    索引もちょろっと見たらウルトラ使いづらい. 

中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては自然を再現しよう役に立つ中高数学 中高数学お散歩コース
大学数学に関しては現代数学観光ツアーなどの無料の通信講座があります.
その他にも無料の通信講座はこちらのページにまとまっています.
ご興味のある方はぜひお気軽にご登録ください!

  • このエントリーをはてなブックマークに追加
  • LINEで送る

関連記事

  • コメント (4)

  • トラックバックは利用できません。

  1. 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

      • phasetr
      • 2017年 1月7日

      ありがとうございます。
      びっくりしたのはスコープ的な意味で
      関数の中の関数(クロージャ)に関数の外からアクセスできるのか、
      というところです。

      理解の雑さが明らかになっていてとてもつらい。

      これから記事(の元原稿)にも追記します。

      • プログラミング言語の用語としての「スコープ」は一般的には「その名前で参照できる範囲」のことをいい、この場合にスコープという用語を使うのは妥当ではありません。 外側からその名前が見えるわけではありませんので。 (ひょっとすると分野によっては別の使い方をすることもあるのかもしれませんが。)

        https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

        クロージャの訳語として「閉包」ということがあるようですが、ローカル変数への参照などを包んで閉じ込めたオブジェクトだというような考え方で捉えるとわかりやすいかと思います。

          • phasetr
          • 2017年 1月8日

          その辺りを正確に理解していないのが元凶であることは認識しました。
          もにょもにょしている部分を正確に言葉にできないので理解まではしばらくかかりそうです。
          経験上、こういう状態になると理解に至るまで数ヶ月から年単位かかることもあるのでじっくり付き合う予定です。

このサイトについて

数学・物理の情報を中心にアカデミックな話題を発信しています。詳しいプロフィールはこちらから。通信講座を中心に数学や物理を独学しやすい環境づくりを目指して日々活動しています。
  • このエントリーをはてなブックマークに追加
  • LINEで送る

YouTube チャンネル登録

講義など動画を使った形式の方が良いコンテンツは動画にしています。ぜひチャンネル登録を!

メルマガ登録

メルマガ登録ページからご登録ください。 数学・物理の専門的な情報と大学受験向けのメルマガの 2 種類があります。

役に立つ・面白い記事があればクリックを!

記事の編集ページから「おすすめ記事」を複数選択してください。