Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
ラベル Scheme の投稿を表示しています。 すべての投稿を表示
ラベル Scheme の投稿を表示しています。 すべての投稿を表示

2013年2月18日月曜日

Scheme で car, cdr の組み合わせ

1. c ではじまり r で終わる関数

cons 関数によりペアが作られる。ペアの第1要素を得る関数は car 。第2要素を得る関数は cdr 。

連続して car, cdr をペアに適用するため関数が用意されている。関数名は `c’ ではじまり `r’ で終わる。

SnapCrab_NoName_2013-2-18_3-0-13_No-00

cr の間に入れる文字は `a’ または `d’ 。それぞれ car, cdr に対応している。

Structure and Interpretation of Computer Programs によると、

9 Since nested applications of car and cdr are cumbersome to write, Lisp dialects provide abbreviations for them -- for instance,

The names of all such procedures start with c and end with r. Each a between them stands for a car operation and each d for a cdr operation, to be applied in the same order in which they appear in the name. The names car and cdr persist because simple combinations like cadr are pronounceable.

11.9 Pairs and lists

(caar pair)‌‌procedure

(cadr pair)‌‌procedure

[r6rs-Z-G-D-1.gif]

(cdddar pair)‌‌procedure

(cddddr pair)‌‌procedure

These procedures are compositions of car and cdr, where for example caddr could be defined by

(define caddr (lambda (x) (car (cdr (cdr x))))).

CAR and CDR - Wikipedia > Continued acceptance

car and cdr have the advantage that short compositions of the functions can be given short and more or less pronounceable names of the same form.

 

2. c と r の間にある a, d は適用する順を表す

例えば、

(define xs '(1 (20 21 22) 3 4 5))
(display (caadr xs))    ; 20

c と r の間にある文字列 `aad’ を見て、順に cdr, car, car を適用する。

SnapCrab_NoName_2013-2-18_3-24-32_No-00

c と r の間にある文字列が `dad’ の場合は、cdr, car, cdr と適用する。

(display (cdadr xs))    ; (21 22)

 

3. Racket に用意されている関数

Racket には car, cdr を 4つまで合成した関数が用意されている。

3.9 Pairs and Lists > 3.9.6 Pair Accessor Shorthands によると、

2つの組み合わせ。

3つの組み合わせ。

4つの組み合わせ。

2012年11月6日火曜日

Scheme で変数の束縛 - let, let*, letrec, 名前付きlet, letrec*

1. Haskell の let 式は相互再帰的

Haskell で値を変数に束縛したい場合、let 式を使う。

束縛とは、束縛 (情報工学) – Wikipedia によると、

名前束縛(Name binding)あるいは名前結合とは、識別子に対応付けることを意味する。値に束縛された識別子を、その値への参照と呼ぶ。

例えば、値を変数に束縛した後、その変数を利用して計算を行う場合、

main = let x = 100 
           y = 200
           z = x + y
       in print (x, y, z)    -- (100,200,300)

Haskell の let 式の特徴は、束縛した変数を相互再帰的に利用できること。

3.12 let 式 によると、

Let 式let { d1 ; ... ; dn } in e という一般形式を持ち、入れ子でレキシカルスコープで相互再帰的な宣言リスト (このような let他の言語ではよく letrec と呼ばれる) を持つ。宣言のスコープ(有効範囲)は、式 e および宣言の右辺である。

 

2. Scheme の let 式における初期値の評価は、現在の環境で行われる

a. let 式の書き方

Scheme も let 式により、値を変数に束縛することができる。

let の書き方は、

let 束縛 本体

となり、束縛の中身は、

((変数1 初期値1) ...)

と書く。 4.2.2 Binding constructs によると、

— library syntax: let <bindings> <body>

Syntax: <Bindings> should have the form

((<variable1> <init1>) ...),

where each <init> is an expression, and <body> should be a sequence of one or more expressions.

let 式を使った簡単な例を挙げる。

(let ((x 100)
      (y 200))
  (display (cons x y)))   ; {100 . 200}

束縛された変数を参照できる範囲は、let 式の本体に限られる。

Each binding of a <variable> has <body> as its region. (同上より)

本体にある最後の式が、let 式の返り値となる。

 

b. 初期値が評価される環境

ただし、最初に Haskell で書いた let 式によく似たコードを書いても、エラーとなってしまう。

(let ((x 100)
      (y 200)
      (z (+ x y)))   ; x: unbound identifier in module in: x
  (display (cons x (cons y z))))

エラーの原因は、変数 z の初期値を評価するとき、変数 x を見つけることができないため。理由は、let 式 の初期値は、現在の「環境」で評価されるため。

Semantics: The <init>s are evaluated in the current environment (in some unspecified order),  … (同上より)

4.2.2. 束縛コンストラクト

意味: 各 <初期値> が現在の環境の中で (ある未規定の順序で) 評価される。その結果を保持する新しい場所へ,それぞれの <変数> が束縛される。その拡張された環境の中で <本体> が評価される。

「環境」とは、変数名と対応する値を関係付けるテーブルである「フレーム」と呼ばれる連鎖の構造を指す。Scheme は、フレームにより、変数が管理される。

3.2 The Environment Model of Evaluation

An environment is a sequence of frames. Each frame is a table (possibly empty) of bindings, which associate variable names with their corresponding values.

詳しくは、評価環境のモデルを参照。

上記の例では、変数 z に値を割り当てるとき、変数 x, y が「環境」に存在しない。そのため、x という識別子を見つけることができない。

 

c. 変数を参照するための方法

let 式の初期値は、現在の環境で評価される。そのため、上記の例に対して、トップレベルに変数 x, y が定義してあれば、変数を参照することができる。

(define x 1)
(define y 2)

(let ((x 100)
      (y 200)
      (z (+ x y)))
  (display (cons x (cons y z))))   ; {100 200 . 3}

もしくは、let 式をネストすれば、外側に定義した let 式の変数を参照できる。

(let ((x 100)
      (y 200))
  (let ((z (+ x y)))
    (display (cons x (cons y z)))))    ; {100 200 . 300}

 

d. let 式を lambda で表す

ところで、let 式は、lambda 式を呼び出す形のシンタクティックシュガーである。

let (special form) によると、

(let ((<var1> <exp1>)
(<var2> <exp2>)

(<varn> <expn>))
<body>)

という let 式は、以下の lambda 式の呼び出しに相当する。

((lambda (<var1> ...<varn>)
<body>)
<exp1>

<expn>)

先ほどの例で考えると、

(let ((x 100)
      (y 200))
  (display (cons x y))) 

を、次のような lamda 式に置きかえることができる。

((lambda (x y)
   (display (cons x y)))
   100
   200)

 

3. let* 式は、左から右へと順に初期値が評価され、変数を束縛する

Scheme には、let 式の末尾にアスタリスクがついた let* 式がある。この式は let と似ているが、値が変数に束縛されるタイミングが違う。let* は初期値の評価と変数への束縛が、左から右へと行われる。

Binding constructs - Revised(5) Scheme によると、

Semantics: Let* is similar to let, but the bindings are performed sequentially from left to right, and the region of a binding indicated by (<variable> <init>) is that part of the let* expression to the right of the binding.

4.2.2. 束縛コンストラクト

意味: let* は let と似ているが,束縛を逐次的に左から右へと行うから,一つの (<変数> <初期値>) が示す束縛の領域は let* 式でその束縛から右の部分である。したがって2番目の縛は1番目の束縛が可視である環境でなされる等々である。

先ほどと同じ式を let 式から、let* 式に置き換えてみる。

(let* ((x 100)
       (y 200)
       (z (+ x y)))
  (display (cons x (cons y z))))   ; {100 200 . 300}

この場合、変数 z が束縛されるとき、既に変数 x, y は存在する。そのため、最初に let でエラーとなった式が、let* では問題なく変数を参照できる。

 

束縛の順序が重要

初期値の変数への束縛は、左から右へと行われる。このため、各変数は、自身の右側にある束縛から参照できる。

the region of a binding indicated by (<variable> <init>) is that part of the let* expression to the right of the binding.(同上より)

Haskell では、let 式における束縛の順序を変更しても、結果は変わらない。

main = let z = x + y
           x = 100 
           y = 200
       in print (x, y, z)    -- (100,200,300)

なぜなら、let 式の中で相互再帰的に変数を参照できるため。

これに対して、Scheme では let* における束縛の順序を変更すると、エラーとなる。

(let* ((z (+ x y))
       (x 100)
       (y 200))
  (display (cons x (cons y z)))) ; x: unbound identifier in module in: x

この場合、最初に変数 z を束縛するために変数 x, y が評価される。このとき、変数 z の右にある変数 x, y を参照できない。

 

4. letrec は再帰的な定義をするときに用いる

let, let* との違い

R5RS には、let, let* に加えて、変数を束縛するための式がもう一つある。それが letrec.

letrec は、初期値と本体が評価される環境が let, let* と異なる。束縛された変数の有効範囲が letrec 式全体となるので、相互再帰的な手続きを定義することができる。

Binding constructs - Revised(5) Scheme によると、

Semantics: The <variable>s are bound to fresh locations holding undefined values, the <init>s are evaluated in the resulting environment …, the <body> is evaluated in the resulting environment, … Each binding of a <variable> has the entire letrec expression as its region, making it possible to define mutually recursive procedures.

4.2.2. 束縛コンストラクト

意味: 未定義値を保持する新しい場所へ,それぞれの<変数> が束縛される。その結果として得られた環境の中で各<初期値> が (ある未規定の順序で) 評価される。各 <変数>にそれぞれ対応する <初期値> の結果が代入される。その結果として得られた環境の中で <本体> が評価される。

 

再帰の例

letrec 式を使い、再帰的な関数を定義してみる。例えば、総和を求める sum 手続き。

(letrec ((sum (lambda (x)
                (if (null? x) 0
                    (+ (car x) (sum (cdr x)))))))
  (display (sum '(1 2 3 4 5))))  ; 15

相互再帰の例は、以下を参照。

 

letrec の制約

letrec 式には制約がある。それは、変数に対して代入したり、参照することなしに、初期値を評価できなければならないというもの。

it must be possible to evaluate each <init> without assigning or referring to the value of any <variable>. (同上より)

このような制約が必要となる理由は、Scheme では引数が値渡しであるため。名前渡しではないことによる。

Scheme passes arguments by value rather than by name.

値渡し とは、

… 実引数として変数を渡したとしても、その値のみが渡される。… その仕組みとしては、独立した新たな変数が関数内に用意され、元の値がコピーされる。そのため変数を渡したとしても、元の変数が変更されるという事はない。

名前渡し とは、

名前渡しでは値でも参照でもなく、式がそのまま渡される。… 式を参照するごとに値を計算して取り出す事が特徴である。

このため、letrec の典型的な使い方は、初期値に手続きを書くことである。

初期値を手続きにすることにより、変数が束縛されるとき、手続きは呼び出されない。変数に束縛した手続きを呼び出すときに、はじめて束縛した変数を参照することになる。

 

変数の初期値の中で、変数を参照した場合、#<undefined>

最初に letrec の制約について確認する。letrec 式の初期値で変数を参照し、その変数を本体で出力したら何が表示されるだろう?

(letrec ((x 100)
         (y x))
  (display x)     ; 100
  (display y))    ; #<undefined>

この場合、変数 y に対応した初期値は、変数 x を参照している。本体で変数 x を表示すると

#<undefined>

という値が表示された。

#<undefined> は、初期化されてない束縛を参照したときの定数を意味する。 letrec では、束縛の初期値として使われる。

3.12 Void and Undefined によると、

A constant that prints as #<undefined> is used as the result of a reference to a local binding when the binding is not yet initialized.

3.18 Void and Undefined

The constant #<undefined> is used as the initial value for letrec bindings.

上記の例を、letrec から let* 式に置き換えると、#<undefined> の代わりに 100 が表示される。

(let* ((x 100)
       (y x))
  (display x)     ; 100
  (display y))    ; 100

これは let* 式により、初期値が順に評価されるため。

letrec 式を使う場合、初期値で lambda を利用すると、変数 y を参照できるようになる。

(letrec ((x 100)
         (y (lambda () x)))
  (display x)     ; 100
  (display (y)))  ; 100

letrec 式では、束縛の順番を変更しても変数を参照できる。

(letrec ((y (lambda () x))
         (x 100))
  (display x)     ; 100
  (display (y)))  ; 100

 

処理系による違い

上記のコードを実行するために、Racket で R5RS, R6RS を利用した。

同じコードを Gauche, BiwaScheme Blackboard, Racket で Pretty Big を利用した場合、#<undefined> の代わりに 100 が表示される。一体どの実装が仕様を満たしているのだろう?

Gauche:letrec* によると、

たとえばこんな式が可能。

(letrec* ([var1 1] 
          [var2 (+ 1 var1)]
          [var3 (+ 1 var2)])
  var3) ;=> 3

ここにletrecを使った場合、R5RSでは結果は未定義 (処理系によってはletrec*のように動作するものもある)、R6RSではエラー (処理系は&assertionコンディションを通知しないとだめ)。

0.8.14現在のGaucheでは上の式のletrec*をletrecに置き換えても動作する。けれど、単純にletrec*をletrecのエイリアスにしてしまうことはできない。 letrecは最適化によって初期化式の順序が変わる場合があるからだ。

 

5. 名前付き let

let のバリエーションとして、名前付き let がある。この構文は、繰り返し処理を定義するときに用いられる。

11.16 Iteration によると、

(let <variable> <bindings> <body>) syntax

… <variable> is bound within <body> to a procedure whose parameters are the bound variables and whose body is <body>.

Scheme 入門 7. 繰り返し

簡単なループは名前つき let を使うのが一般的です。また、 複雑な再帰は letrec を使うことが多いようです。

上記の letrec で書いた手続き sum を、名前付き let で置き換えてみる。

(display
 (let sum ((x '(1 2 3 4 5)))
   (if (null? x) 0
       (+ (car x) (sum (cdr x))))))

名前付き let を書くときは、let 式を lambda 式に置き換えた方法を思い出し、束縛を引数と対応付けて考えると良い。

複数の束縛がある場合、次のように書く。例えば、手続き sum を累積変数を用い、末尾再帰の形に変更するなら、

(display
 (let sum ((x '(1 2 3 4 5))
           (acc 0))
   (if (null? x) acc
       (sum (cdr x)
            (+ acc (car x))))))

 

6. letrec*

R6RS には、更に letrec* という形式がある。let + rec + * という形をしている。

これは、次のように覚えれば良い。

  1. rec : 再帰的な手続きを定義するのに用いることができる。
  2. * : 束縛が左から右へと、順に行われる。 ⇒ 先に束縛された変数を参照できる。

Gauche:letrec* によると、

R6RSで導入されたletrec*は、letrecに初期化式の実行順序の保証を入れたもの。

Revised^6 Report on the Algorithmic Language Scheme

Semantics: … each <variable> is assigned in left-to-right order to the result of evaluating the corresponding <init>, the <body> is evaluated in the resulting environment, …  Despite the left-to-right evaluation and assignment order, each binding of a <variable> has the entire letrec* expression as its region, making it possible to define mutually recursive procedures. …

 

制約の比較

letrec と letrec* の制約を比較しておく。letrec では、以下のように述べられていた。

it must be possible to evaluate each <init> without assigning or referring to the value of any <variable>. (同上より)

これに対して letrec* には、次にように書かれている。

It must be possible to evaluate each <init> without assigning or referring to the value of the corresponding <variable> or the <variable> of any of the bindings that follow it in <bindings>. Another restriction is that the continuation of each <init> should not be invoked more than once. (同上より)

 

相互再帰と、変数の参照の例

Revised^6 Report on the Algorithmic Language Scheme には、letrec の例が挙げられている。この例における変数の参照関係を図示すると、以下のようになる。

SnapCrab_NoName_2012-11-3_11-27-9_No-00

これより、letrec* では相互再帰と、先に束縛された変数を参照できることが分かる。

この例で、letrec* を letrec に置き換えると、エラーが表示される。なぜなら、変数 x の初期値を評価したとき、参照する変数 p が #<undefined> となり、手続きの呼び出しができなくなるため。

 

7. まとめ

let, let*

let と let* は、初期値が変数を束縛するタイミングが違う。

  • let は、変数を束縛する前に初期値を評価する。
  • let* は、初期値の評価と変数への割り当てが順に行われる。

11.4.6 Binding constructs より、

let

let*

the initial values are computed before any of the variables become bound;

the bindings and evaluations are performed sequentially.

 

letrec, letrec*

letrec と letrec*の共通点は、初期値を計算するときに、束縛された変数を利用できること。このため、相互再帰的な定義ができる。

In a letrec or letrec* expression, all the bindings are in effect while their initial values are being computed, thus allowing mutually recursive definitions.

違いは、let と let* の関係と似ている。

  • letrec は、初期値が変数に割り当てられる前に評価される。
  • letrec* は、初期値の評価と割り当てが順に行われる。
letrec letrec*

the initial values are computed before being assigned to the variables;

the evaluations and assignments are performed sequentially.

 

覚えておくこと
  1. let 式により、初期値を変数に束縛する。
  2. 末尾に * を付けると、評価と変数の割り当てが順に行われる。
  3. 末尾に rec を付けると、初期値を計算する中で、束縛された変数を参照できる。

2012年9月28日金曜日

DrRacket でコードの整形

1. メニューから選択する

Emacs でコードの整形をする場合、C-M-\ を入力し、indent-region を呼び出す。

DrRacket でコードの整形をするには、

  1. インデントを整えたい式を範囲選択しておく。
  2. メニューより、Racket > Reindent を選択する。

表示されている全てのコードを整形するには、Reindent の下にある Reindent All を選ぶ。

 

2. キーボードで操作する

キーボードから、メニューの Racket > Reindent を選択するには、他のアプリケーションと同様に、

  • Alt, r, r

のキーを順に押せば良い。

もっと簡単に Reindent するには、整形するコードを選択した後、

  • Tab キー

を押す。

3.1 Menus によると、

Reindent : Indents the selected text according to the standard Racket formatting conventions. (Pressing the Tab key has the same effect.)

 

3. 移動、選択、整形の操作手順

コードを整形するには、式を選択する必要がる。S 式を選択するには、

3.3 Keyboard Shortcuts 

M-C-SPACE : select forward S-expression

 

ところで、DrRacket を Emacs のキーバインディングで利用している。また、Escape キーの入力は C-[ で代用している。

例えば、次のような式を入力した後、結果を display 手続きに渡したくなったとする。

(* 3
   (+ 1 2)
   (- 10 5))

キャレットが式に末尾にある場合、

  1. 先頭へ移動: Ctrl キーを押しながら [, b
  2. S 式の選択: Ctrl キーを押しながら [, Space
  3. 括弧で括る: Ctrl キーを押しながら [ 。次に ( を入力する。
  4. display と入力。

M-( : wrap selection in parentheses

このとき、キャレットは display の右横にある。

(display (* 3
   (+ 1 2)
   (- 10 5)))

全体を整形するために、

  1. 外側の括弧へ移動: Ctrl キーを押しながら [, u
  2. S 式の選択: Ctrl キーを押しながら [, Space
  3. 整形: Tab
  • M-C-u : move up out of an S-expression

  • M-C-d : move down into a nested S-expression

  •  

    4. その他

    メニューより、Edit > Keybindings > Show Active Keybindings を選択すると、現在有効なキーバインディングの一覧が表示される。

    S 式を選択する (forward-select-sexp) キーとして、上記以外に、

    • esc;c:s:f

    とあるが、キーを入力しても、forward-select-word

    • esc;s:f

    と同じ動作になってしまう。

    6.16 keymap% によると、

    For a special keyword, the capitalization does not matter. However, capitalization is important for single-letter keynames. Furthermore, single-letter ASCII keynames are treated specially: A and s:a are both treated as s:A. However, when c: is included on Windows without m:, or when d: is included on Mac OS X, then ASCII letters are not upcased with s:, since the upcasing behavior of the Shift key is cancelled by Control without Alt (on Windows) or by Command (on Mac OS X).

    その他、タブの入力に関する動作を以下に挙げておく。

    Index

    tabify
    tabify-all
    tabify-on-return?
    tabify-selection

    2012年9月23日日曜日

    Scheme のペアとリスト

    1. Haskell の (:) コンスはリストを生成する

    Haskell で、リストの先頭に要素を追加するには (:) 演算子を使う。

    Prelude> 1:[2,3,4]
    [1,2,3,4]

    演算子 (:) は「コンス」と呼ばれる。この関数は、リストのデータコンストラクタとして定義されている。

    The Haskell 98 Language Report > 6.1.3 リスト

    data [a] = [] | a : [a] deriving (Eq, Ord)
    リストは、二つの構成子をもつ代数的データ型であるが、3.7 節で述べたように特別な構文をもつ。最初 の構成子は空リストで、`[]' (「ニル」)と書く。二つ目の構成子 は、`:' (「コンス」)と書く。

    (:) の型を確認する。

    Prelude> :t (:)
    (:) :: a -> [a] -> [a]

    リストの要素は、同じ型でなければならない。そのため、次のような計算ではエラーとなる。

    "Hoge":[2,3,4]

     

    2. Lisp の cons はペアを生成する

    Lisp にも同じ名前の cons 関数がある。ただし、Haskell の (:) とは異なる。

    1. cons により、2つの、値または値へのポインタを持つオブジェクトを生成する。
    2. cons により生成されたオブジェクトは「ペア」と呼ばれる。
    3. ペアの1番目の要素は car により、2番目の要素は cdr により参照する。

    cons - Wikipedia, the free encyclopedia

    cons (play /ˈkɒnz/ or /ˈkɒns/) is a fundamental function in most dialects of the Lisp programming language. cons constructs memory objects which hold two values or pointers to values. These objects are referred to as (cons) cells, conses, non-atomic s-expressions ("NATSes"), or (cons) pairs.

    Haskell のように、リスト型に対して定義されている関数ではない。

     

    ペア

    Lisp の cons によって生成される「ペア」とは、順序対 のこと。

    順序対は、2つの要素で構成される。

    Ordered pair – Wikipedia

    … an ordered pair (a, b) is a pair of mathematical objects. In the ordered pair (a, b), the object a is called the first entry, and the object b the second entry of the pair.

    cons によって生成されるのは、リストではない。

     

    3. タプルでペアを表現できる

    Haskell には、Data.Tuple 型が定義されている。この型を順序対として用いることができる。データコンストラクタは、リストと同じように特別な表記がある。

    (,)

    タプルとは、要素が順に並んでいるリストのこと。要素の数 n に応じて、n-tuple と呼ばれる。特に 2-tuple は順序対と言う。

    Tuple – Wikipedia

    … a tuple is an ordered list of elements. In set theory, an (ordered) n-tuple is a sequence (or ordered list) of n elements, where n is a positive integer.

    The unique 0‑tuple is called the null tuple. A 1‑tuple is called a singleton, a 2‑tuple is called an ordered pair and a 3‑tuple is a triple or triplet.

    タプルは、順序対をネストしたものと見なせる。

    Tuples as nested ordered pairs

    Another way of formalizing tuples is as nested ordered pairs:

    1. The 0-tuple (i.e. the empty tuple) is represented by the empty set \emptyset.
    2. An n-tuple, with n > 0, can be defined as an ordered pair of its first entry and an (n-1)-tuple (which contains the remaining entries when n > 1):
      (a_1, a_2, a_3, \ldots, a_n) = (a_1, (a_2, a_3, \ldots, a_n))

    This definition can be applied recursively to the (n-1)-tuple:

    (a_1, a_2, a_3, \ldots, a_n) = (a_1, (a_2, (a_3, (\ldots, (a_n, \emptyset)\ldots))))

     

    4. Scheme と Haskell の比較

    Data.Tuple に定義されている関数は、順序対に対して適用できる。

    Lisp のペアに対する操作と名前を比較すると、

      Lisp Haskell
    ペアを生成 cons (,)
    1番目の要素を取得

    car

    fst
    2番目の要素を取得 cdr snd

    Haskell でタプルを生成し、上記の関数を適用する。

      print (1, 2)              -- (1,2)   print (1, "hoge")         -- (1,"hoge")
      print $ fst $ (1, 2)      -- 1
      print $ snd $ (1, "hoge") -- "hoge"

    リストと違い、タプルの要素は異なる型でも良い。

    次に、同じ操作を Scheme で書いてみる。Scheme は Lisp の一方言なので、上記と同じ関数が用意されている。

    11.9 Pairs and lists

    A pair is a compound structure with two fields called the car and cdr fields (for historical reasons). Pairs are created by the procedure cons. The car and cdr fields are accessed by the procedures car and cdr.

    (display (cons 1 2))             ; {1 . 2}
    (newline) 
    (display (cons 1 "hoge"))        ; {1 . hoge}
    (newline)  
    (display (car (cons 1 2)))       ; 1
    (newline)
    (display (cdr (cons 1 "hoge"))) ; hoge
    (newline)

     

    5. Scheme のリストはペアからできている

    リストの定義

    Lisp 系の言語は、ペアで「リスト」を表現する。ペアという構造の上にリストが作られる。Haskell では、ペアを表現するためのタプルとリストは全く別の型。

    Scheme において、リストは次のように定義されている。

    1. 空リスト
    2. ペアの cdr フィールドがリストであるもの

    A list can be defined recursively as either the empty listor a pair whose cdr is a list. More precisely, the set of lists is defined as the smallest set X such that

    • The empty list is in X.

    • If list is in X, then any pair whose cdr field contains list is also in X.

    11.9 Pairs and lists より)

    要素のない空リストは、特別に用意されており、

    ()

    と表現する。

    (display (cons 1 '()))          ;(1)
    (newline)
    (display (cons 1 (cons 2 '()))) ; (1 2)
    (newline)

    最初の例は、ペアの cdr フィールドに空リストがあるので、リストである。

    次の例は、(cons 2 ‘()) がリスト。そのため、全体では

    (cons 1 リスト)

    という構造になるのでリスト。

     

    リストはペアであるが、空リストはペアでない

    リストは、ペアの特殊な形として作成される。そのため、リストはペアでもある。

    対象がペアの場合真を返す述語 pair? と、リストである場合真を返す述語 list? を使って確かめてみる。

    (define ls (cons 1 (cons 2 (cons 3 '()))))
    (display (pair? ls))    ; #t
    (display (list? ls))    ; #t

    ただし、空リストはペアではない。

    The empty listis a special object of its own type. It is not a pair. It has no elements and its length is zero.

    また、述語 null? に対して唯一真を返すオブジェクト。

    (null? obj)    procedure

    Returns #t if obj is the empty list, #fotherwise.

    (display (pair? '()))  ; #f
    (display (list? '()))  ; #t
    (display (null? '()))  ; #t

    まとめると、

    1. リストはペアである。
    2. ただし、空リストはペアではない。
    3. 空リストは、null である特別なオブジェクト。

    当初、この点に違和感を覚えた。なぜなら、ペアが保持している要素によって、可能な操作が異なる。そのため、要素の内容により、型が変化するように見えたから。

    たとえば、リストの逆順を返す reverse 手続きはリストに適用できるが、ペアには適用できない。同じような手続きはライブラリにいくつか定義されている。

    Haskell でたとえると、

    • (1, [])
    • (1, (2, [])

    は、ペアであると同時にリストであり、この場合にのみ適用可能な関数がある、といった感じ。Scheme よりも Haskell を先に学んだので、Scheme のペアとリストの関係がしっくりこなかった。

     

    ドット対

    ところで、cons 関数で生成したペアを display 関数で表示すると、`.’(ドット)を用いて表される。要素が3つのときは、次のように表示される。

    (display (cons 1 (cons 2 3)))    ; {1 2 . 3}

    ネストされたペアは、ドットが省略された形。

    11.9 Pairs and lists

    A chain of pairs not ending in the empty list is called an improper list. Note that an improper list is not a list. The list and dotted notations can be combined to represent improper lists:

    (a b c . d)

    is equivalent to

    (a . (b . (c . d)))

    ドットで表現されたペアを、ソースコードにそのままの形で書き、実行するとエラーとなる。

    (display (1 2 . 3))

    括弧で囲まれた最初の要素は手続き、それ以降の要素は引数として解釈されるため。

    Procedure calls

    (<operator> <operand1> ...) syntax

    A procedure call consists of expressions for the procedure to be called and the arguments to be passed to it, with enclosing parentheses.

    これを回避するには、quote を用いる。

    11.4.1 Quotation

    Semantics: (quote <datum>) evaluates to the datum value represented by <datum> (see section 4.3).

    quote の代わりに ‘ を使うこともできる。

    (display (cons 1 (cons 2 3)))    ; {1 2 . 3}
    (newline)
    (display (quote (1 2 . 3)))
    (newline)

     

    6. Scheme の基本的な型

    Scheme で操作する対象となるオブジェクト(値)は「型」を持つ。

    1.1 Basic types

    Scheme programs manipulate objects, which are also referred to as values. Scheme objects are organized into sets of values called types.

    基本的な型に対応した述語が9つ定義されている。この手続きを用いて値の型を特定できる。

    11.1 Base types

    No object satisfies more than one of the following predicates:

    boolean? pair?
    symbol? number?
    char? string?
    vector? procedure?
    null?

    These predicates define the base types boolean, pair, symbol, number, char (or character), string, vector, and procedure. Moreover, the empty list is a special object of its own type.

    Note that, although there is a separate boolean type, any Scheme value can be used as a boolean value for the purpose of a conditional test; see section 5.7.

    上記には「ペア」と「空リスト」を特定する述語が挙げられている。しかし、リストに対応した述語は含まれていない。

    述語 list? は、ペアとリストの解説で述べられている。

    11.9 Pairs and lists

    (list? obj)    procedure

    Returns #t if obj is a list, #f otherwise. By definition, all lists are chains of pairs that have finite length and are terminated by the empty list.

    このことからも、リストはペアの特殊な形であることが分かる。

     

    関連記事

    参考サイト

    2012年9月7日金曜日

    Racket で R6RS を利用する

    1. R5RS

    Scheme の言語仕様には、R5RS と R6RS がある。

    Scheme – Wikipedia によると、

    … その仕様はRevisedn Report on the Algorithmic Language Scheme (RnRS)と呼ばれている。現在広く実装されているものは改訂第五版に当たるR5RS(1998年)であり、2007年にはR6RSが策定された。

    Racket は、Scheme から派生した言語。R5RS でコードを書きたい場合、メニューより

    • Language > Choose Language > Choose a language > R5RS

    を選択する。

    SnapCrab_Choose Language_2012-9-7_21-25-0_No-00

     

    2. R6RS

    R6RS とは、Scheme – Wikipedia によると、

    2007年9月に新仕様「The Revised6 Report on the Algorithmic Language Scheme (R6RS)」[5] が成立した。4部構成となり、R5RSに比べおよそ3倍の文章量となった。今までは小さな言語仕様に対してのこだわりが見られたが、Unicodeサポート等の実用的な言語として必要な要素が盛り込まれている点が特徴的である。

    R6RS:概要と例 によると、

    R6RS準拠のSchemeプログラムは、ひとつのトップレベルプログラムと、いくつかのライブラリから構成されます。

    トップレベルプログラムはSchemeプログラムの実行が開始されるコードです。先頭で必要とするライブラリをimportして、後はR5RSまでと同じように定義や式を並べておきます。定義と式はどのような順序で現れても構いません。上から順番に実行されます。

    (import (rnrs)           ; R6RS baseと標準ライブラリをインポート
            (mylib mystuff)) ; (mylib mystuff)ライブラリをインポート

     

    a. 言語の選択

    Racket では、メニューより、

    • Language > Choose Language > Use the language declared in the source

    を選択する。

    SnapCrab_NoName_2012-9-7_22-40-46_No-00

    R6RS: Scheme2 Running Top-Level Programs に、R6RS の利用方法が書かれている。

    #!r6rs
    (import (rnrs))
    (display "hello\n")

    1行目に、UNIX のスクリプトのように #! を記述をする。

    シバン (Unix) – Wikipedia とは、

    シバンまたはシェバン (shebang) とはUNIXスクリプト#!から始まる1行目のこと。起動してスクリプトを読み込むインタプリタを指定する。

     

    b.新規ファイルを作成したときの自動入力

    新規ファイルを作成したときに、1行目に自動的に #!r6rs を記述してほしい。

    そのためには、メニューより、

    • Language > Choose Language > Use the language declared in the source

    を指定した後、左下の Show Details ボタンを押し、Automatic #lang line に

    #!r6rs

    を入力しておく。

     

    c.ライブラリの指定

    1 Using R6RS with DrRacket には、複数のライブラリを指定する例が書かれている。

    #!r6rs
    (import (rnrs lists (6))
            (rnrs base (6))
            (rnrs io simple (6)))
    (display (find even? '(3 1 4 1 5 9)))

    読み込まれるライブラリは、5 Libraries and Collections によると、

    (rnrs io simple (6))  means  (lib "rnrs/io/simple-6.rkt")
    (rnrs)                means  (lib "rnrs/main-6.rkt")
    (rnrs main)           means  (lib "rnrs/main_.rkt")
    (rnrs (6))            means  (lib "rnrs/main-6.rkt")
    (racket base)         means  (lib "racket/base.rkt")
    (achtung!)            means  (lib "achtung%21/main.rkt")
    (funco new-λ)         means  (lib "funco/new-%ce%bb.rkt")

    ライブラリについて詳しくは、8 R6RS Libraries を参照。

     

    d. #lang の意味

    1行目の記述は、 R6RS Module Language によると、

    The r6rs language is usually used in the form #!r6rs, which is equivalent to #lang r6rs and is also valid R6RS syntax.

    #lang の意味は、6.2.2 The #lang Shorthand によると、

    In the case of #lang racket, the syntax is

    #lang racket

    decl ...

    which reads the same as

    (module name racket

    decl ...)

    where name is derived from the name of the file that contains the #lang form.

     

    3. Pretty Big

    Legacy Languages として、R5RS と並んで Pretty Big がある。

    2.2 Legacy Languages によると、

    The PLT Pretty Big language provides a language roughly compatible with a language in earlier versions of DrRacket. It evaluates a program in the same way as load, and it starts by importing the following modules: mzscheme, racket/gui/base, mzlib/class, mzlib/etc, mzlib/file, mzlib/list, mzlib/unit, mzlib/include, mzlib/defmacro, mzlib/pretty, mzlib/string, mzlib/thread, mzlib/math, mzlib/match, and mzlib/shared.

     

    関連サイト

    2012年9月3日月曜日

    DrRacket で Emacs 風にコードを補完するためのキーバインディング

    1. デフォルトのキーバインディング

    DrRacket でコードの補完をするには、

    • メニューより、Edit > Complete Word

    を選択する。

    デフォルトのキーバインディングでは、

    C-/

    に割り当てられている。

    • メニューより、Edit > Keybindings > Show Active Keybindings

    で調べると、C-/ が Complete Word に割り当てられていることを確認できる。

     

    2. Emacs のキーバインディングを利用している場合

    a. Emacs の補完機能

    DrScheme (Racket) で Emacs のキーバインディングを利用している場合、Complete Word にキーが割り当てられていない。

    Emacs では、

    M-/

    によって、バッファ内にある単語を補完してくれる。

    abbrev によると、

    Abbrev とは要するに、略称展開です。…

    M-/ (dabbrev-expand) は、カーソル直前の単語を、バッファ内にある単語で補完するコマンドなのです。

    Dynamic Abbrevs - GNU Emacs Manual

    M-/
    Expand the word in the buffer before point as a dynamic abbrev, by searching in the buffer for words starting with that abbreviation (dabbrev-expand).

    そこで、DrRacket でも M-/ でコードが補完されるようにしたい。

     

    キーボードショートカットのカスタマイズ

    3.3 Keyboard Shortcuts には、キーバインディングの例が書かれている。

    For example, this remaps the key combination “control-a” key to “!”.

    #lang s-exp framework/keybinding-lang

    (keybinding "c:a" (λ (editor evt) (send editor insert "!")))

    キーバインディングを有効にするには、上記内容のファイルを作成し、

    • Edit > Keybindings > Add User-defined Keybindings…

    で読み込む。

     

    補完機能の割り当て

    には、補完機能のキーバインディングをカスタマイズするための具体的な方法が書かれていた。入力するキーだけを変更する。

    #lang s-exp framework/keybinding-lang
    (keybinding "m:/"
                (λ (editor evt)
                  (if (is-a? editor text:autocomplete<%>)
                      (send editor auto-complete)
                      #f)))

    各関数については、以下を参照。

    キーバインディングを無効にするには、

    • メニューより、Edit > Keybindings > Remove …

    を選択すれば良い。

     

    関連記事

    2012年3月13日火曜日

    レキシカルスコープとダイナミックスコープ

    1. レキシカルスコープとダイナミックスコープの違い

    言語によって、変数のスコープに関する仕様が異なる。スコープには、レキシカルスコープとダイナミックスコープがある。採用しているスコープにより、変数の参照の仕方が違う。

    レキシカルスコープでは、プログラムとして書かれた字句を解析すれば、変数のスコープを把握できる。実行時のことは考えなくて良い。これに対して、ダイナミックスコープでは、実行時における関数の呼び出され方により、参照できる変数が異なる。

    用語の説明を見る前に、具体例を見た方が理解しやすい。

    Scope (computer science) - WikipediaLexical scoping and dynamic scoping によると、

    … if function f invokes a separately-defined function g, then under lexical scoping, function g does not have access to f's local variables (since the text of g is not inside the text of f), while under dynamic scoping, function g does have access to f's local variables (since the invocation of g is inside the invocation of f).

    (同上より、装飾は引用者による)

    関数 f から関数 g を呼び出す場合を想定する。関数は、各々独立に定義されており、一方の関数がもう一方をネストしてないとする。

    関数 f で定義されているローカル変数を、関数 g から、

    1. レキシカルスコープでは、参照できない。
    2. ダイナミックスコープでは、参照できる。

    img_0058

     

    各言語が採用しているスコープ

    言語によって、採用しているスコープが異なる。もしくは、両方使える。

    Lexical scoping によると、

    Lexical scoping is standard in all ALGOL-based languages such as Pascal, Modula2 and Ada as well as in modern functional languages such as ML and Haskell.

    Dynamic scoping

    Some languages, like Perl and Common Lisp, allow the programmer to choose static or dynamic scoping when defining or redefining a variable. Logo and Emacs lisp are examples of languages that use dynamic scoping.

    多くの言語で、レキシカルスコープが採用されている。ダイナミックスコープを採用している言語として、Emacs Lisp がある。

     

    レキシカルスコープの例

    Scope (computer science) - Wikipedia に書かれていた例を、各言語で試してみる。

    Python

    x = 7
    def g(): return x
    def f(): 
        x = 9
        return g()
    print f()           #=> 7

    javascript

    var x = 7;
    function g(){ return x; }
    function f(){ 
        var x = 9;
        return g();
    }
    f();                //=> 7

    Haskell

    x = 7
    g = x
    f = let x = 9 in g
    main = print f      #=> 7

    scheme

    (define x 7)
    (define g (lambda () x))
    (define f
      (lambda ()
        (let ((x 1))
          (g))))
    (f)   ;=> 7

    関数 f が呼び出された後に、関数 g が呼び出されても、関数 g の中にある変数の参照先は変わらない。

    レキシカルスコープの実行結果を見ると、「コレ普通じゃない?」と感じる。理由は、

    Dynamic scoping によると、

    dynamic scoping can be dangerous and few modern languages use it.

    ダイナミックスコープは取り扱いが難しいので、モダンな言語でほとんど採用されていない。そのため、レキシカルスコープの考え方の方が馴染みがある。

     

    ダイナミックスコープの例

    続いて、ダイナミックスコープを採用している Emacs Lisp の例。

    Emacs Lisp

    (setq x 7)
    (defun g () x)
    (defun f ()
      (let ((x 9))
        (g))
    (message "%d" (f))  ;=> 9

    レキシカルスコープの言語に慣れていると、ダイナミックスコープの結果に違和感を感じる。

    関数 f を呼び出した後、関数 f の中で関数 g が呼び出される。これにより、関数 g の中にある変数の参照先が変わってしまう。

    もし、関数 f が呼び出されず、関数 g だけが実行されたら、

    (setq x 7)
    (defun g () x)
    (message "%d" (g))  ;=> 7

    関数の呼び出され方により、参照できる変数が変わる。実行時のことを考える必要なければならない。そのため、コード全体を見通すことが大変そう。 (+_+)

    では、ダイナミックスコープにメリットはあるのだろうか?

    Emacs Lisp - Wikipedia によると

    Emacs Lispは、アプリケーション・プログラミングで使われる方言群であるSchemeCommon Lispとは根本的に異なる。大きな違いの1つは、デフォルトで字句的スコープではなく動的スコープを使うことである。つまり、呼出し関数の局所変数は、ポインタや参照を渡さなくとも、呼び出された関数から参照できる。

    関数から呼び出された側の関数は、自分のローカルにある変数の内容が、呼び出された文脈により変化する。

    レキシカルスコープ的な視点で考えると、呼び出された側の関数は、呼び出した側の関数にネストされたかのように見える。

    ところで、なぜ Emacs Lisp は、ダイナミックスコープを採用したのだろう?

    Scope (computer science) - Wikipedia, the free encyclopedia によると、

    Correct implementation of static scope in languages with first-class nested functions is not trivial, as it requires each function value to carry with it a record of the values of the variables that it depends on (the pair of the function and this environment is called a closure).

    Dynamic scoping is fairly easy to implement. To find an identifier's value, the program could traverse the runtime stack, checking each activation record (each function's stack frame) for a value for the identifier.

    レキシカルスコープより、ダイナミックスコープの方が実装が簡単とのこと。

    Scope - GNU Emacs Lisp Reference Manual にも、同様のことが書かれている。

    Emacs Lisp uses dynamic scoping because simple implementations of lexical scoping are slow. In addition, every Lisp system needs to offer dynamic scoping at least as an option; if lexical scoping is the norm, there must be a way to specify dynamic scoping instead for a particular variable. It might not be a bad thing for Emacs to offer both, but implementing it with dynamic scoping only was much easier.

     

    2. レキシカルスコープとダイナミックスコープにおける変数の生存期間

    書かれたテキストにより決まるレキシカルスコープ

    Scope (computer science) - WikipediaLexical scoping and dynamic scoping によると、

    In lexical scoping (…), if a variable-name's scope is a certain function, then its scope is the program text of the function definition: within that text, the variable-name exists, and is bound to its variable, but outside that text, the variable-name does not exist.

    レキシカルスコープでは、関数の中で定義されてる変数のスコープは、プログラムの字句によって決まる。関数の中に書かれている変数は、その関数が記述されている内側で存在するが、外側では存在しない。

     

    実行時に決まるダイナミックスコープ

    Scope (computer science) - WikipediaLexical scoping and dynamic scoping によると、

    By contrast, in dynamic scoping (or dynamic scope), if a variable-name's scope is a certain function, then its scope is the time-period during which the function is executing: while the function is running, the variable-name exists, and is bound to its variable, but after the function returns, the variable-name does not exist. (同上より)

    ダイナミックスコープでは、関数の中で定義されてる変数のスコープは、その関数が実行されている期間と同じ。関数が実行されている間、変数は存在するが、関数の実行が終了すると変数は消える。

     

    3. 実装から見る、名前の解決

    実装の難しさ

    レキシカルスコープとダイナミックスコープを実装する難しさについて、以下のように述べられている。

    Lexical scoping

    Correct implementation of static scope in languages with first-class nested functions is not trivial, as it requires each function value to carry with it a record of the values of the variables that it depends on (the pair of the function and this environment is called a closure).

    レキシカルスコープを実装することが難しいのは、関数に対して、クロージャと呼ばれる、その関数が依存する変数(環境)を持って回る必要があるため。

    Dynamic scoping

    Dynamic scoping is fairly easy to implement. To find an identifier's value, the program could traverse the runtime stack, checking each activation record (each function's stack frame) for a value for the identifier. In practice, this is made more efficient via the use of an association list, which is a stack of name/value pairs. Pairs are pushed onto this stack whenever declarations are made, and popped whenever variables go out of scope.[1]

    ダイナミックスコープの実装が簡単なのは、実行中の関数に関する情報を持つスタックの中から、参照したい値を探すだけで済むため。

    スタックとは、Call stack - Wikipedia によると、

    Since the call stack is organized as a stack, the caller pushes the return address onto the stack, and the called subroutine, when it finishes, pops the return address off the call stack and transfers control to that address. If a called subroutine calls on to yet another subroutine, it will push another return address onto the call stack, and so on, with the information stacking up and unstacking as the program dictates.

    サブルーチンを呼ぶときに、利用されるデータ構造。サブルーチンが呼び出されるとき、スタックにリターンアドレスが積まれ、呼び出されたサブルーチンが終了すると、リターンアドレスが取り出され、そのアドレスに制御が移される。

    プログラムを実行するときの、メモリの利用のされ方と名称について、詳しくは、 Data segment - WikipediaProgram memory を参照。

    The computer program memory is organized into the following:

     

    名前(識別子)の探し方

    変数を参照するとき、レキシカルスコープとダイナミックスコープでは、以下の点が異なる。

    Lexical scoping によると、

    With lexical scope, a name always refers to its (more or less) local lexical environment. This is a property of the program text and is made independent of the runtime call stack by the language implementation. Because this matching only requires analysis of the static program text, this type of scoping is also called static scoping. …

    Static scoping allows the programmer to reason about object references such as parameters, variables, constants, types, functions, etc. as simple name substitutions. This makes it much easier to make modular code and reason about it, since the local naming structure can be understood in isolation.

    レキシカルスコープでは、「名前」が指し示す対象は、プログラムの字句を見れば分かる。実行時のことを考える必要ない。そのため、プログラムのテキストにおいて、参照しているものの名前を置き換えるだけで、何を指し示しているか把握できる。

    Dynamic scoping によると、

    With dynamic scope, each identifier has a global stack of bindings. Introducing a local variable with name x pushes a binding onto the global x stack (…), which is popped off when the control flow leaves the scope. Evaluating x in any context always yields the top binding. In other words, a global identifier refers to the identifier associated with the most recent environment. Note that this cannot be done at compile-time because the binding stack only exists at run-time, which is why this type of scoping is called dynamic scoping.

    ダイナミックスコープでは、実行時に変数のような識別子があると、スタックに積まれる。その後、識別子が必要になったら、スタックに積まれている順に検索される。つまり、実行中の関数から、一番近い場所にある環境から、対象を見つけ出そうとする。

    先ほどの例で考えると、

    (setq x 7)
    (defun g () x)
    (defun f ()
      (let ((x 9))
        (g))
    (message "%d" (f))  ;=> 9
    1. 変数 x は 7 に束縛され、スタックに積まれる。
    2. 関数 f の呼び出しにより、変数 x は 9 に束縛され、スタックに積まれる。
    3. 関数 g が呼び出され、変数 x が参照される。このとき、直前にスタックに積んだ、中身が 9 の変数 x が取り出される。

    これにより、結果が 9 となる。

    2011年9月6日火曜日

    Haskell で特定の構造を前提としない関数 - 要素を辿る関数を必要に応じて定義

    0. 概要

    1. 普通、「特定の構造に対して適用する関数」は、特定の型を前提として考える。
    2. これに対して、構造を辿る関数を与えることによって、「特定の型を前提としない関数」を定義することができる。

    前者の方法と比べながら、後者について見ていく。

    目次:
    • 1. 型を決めてから、関数を定義する
      • リストに適用する関数の場合
      • 木に適用する関数の場合
    • 2. 特定の構造を前提としない関数
      • treewalk 関数の引数について
      • 第4引数 walker がイメージしにくい理由
      • ノードを辿っていく関数 walker
      • treewalk 関数のまとめ
      • treewalk 関数の使い方
      • Data.Tree 型の値に対して treewalk 関数を適用
      • リストに対して treewalk 関数を適用
    • 3. 要素を辿っていく walk 関数について、はじめから再考
      • Data.Tree 型に対して、walk 関数を適用
      • リストに対して、walk 関数を適用
    • 3-1. walk 関数から、要素に適用する関数を分離
      • Data.Tree に対して、walk 関数を適用
      • リストに対して、walk 関数を適用
    • 3-2. walk 関数から、更に述語を分離
      • Data.Tree に対して、walk 関数を適用
      • リストに対して、walk 関数を適用

     

    1. 型を決めてから、関数を定義する

    リストに適用する関数の場合

    リストの要素に、関数を適用するには、map 関数を使う。

    例えば、要素を 2 倍したいなら、

    *Main> map (* 2) [1..5]
    [2,4,6,8,10]

    map 関数は、「リスト」という構造を前提に定義されている。定義を確認すると、

    map :: (a -> b) -> [a] -> [b]
    map _ []     = []
    map f (x:xs) = f x : map f xs

    第 2 引数は関数の対象であるリスト。

    第 1 引数である関数 f は、データコンストラクタ (:) を使い、リスト構造を辿っていくかのように見える。

     

    木に適用する関数の場合

    2 つの子を持つ「木」を、以下のように定義したとする。

    data Tree a = Leaf a   | Branch (Tree a) (Tree a)
                deriving Show

    適当に Tree a 型の値を作る。

    tree = Branch (Leaf 1) 
                  (Branch (Branch (Leaf 2)
                                  (Leaf 3))
                          (Leaf 4))

    木の「葉」が持つ値に、関数を適用する関数を mapTree とすると、

    mapTree f (Leaf x)     = Leaf (f x)
    mapTree f (Branch l r) = Branch (mapTree f l) (mapTree f r)

    mapTree 関数を、変数 tree に適用してみる。

    *Main> mapTree (*2) tree
    Branch (Leaf 2) (Branch (Branch (Leaf 4) (Leaf 6)) (Leaf 8))

    mapTree 関数も、先ほどの map 関数のように、適用する対象である Tree a  型の構造を前提としている。

    型コンストラクタ Tree を、Functor クラスのインスタンスにするなら、

    instance Functor Tree where
        fmap f (Leaf x)    = Leaf (f x)
        fmap f (Branch l r) = Branch (fmap f l) (fmap f r)

    ( cf .Haskell の fmap )

    変数 tree に fmap を適用すると、

    *Main> fmap (*2) tree
    Branch (Leaf 2) (Branch (Branch (Leaf 4) (Leaf 6)) (Leaf 8))

    この方法も、fmap を具体的に定義するとき、Tree a 型の構造を前提としている。

    オブジェクト指向で何か考える場合も、問題領域の対象を整理し、クラスと構造を決めながら、メソッドを実装していく。型と構造を考えつつ、操作を割り当てる。構造を離れて、操作だけを考えることは難しい。

     

    2. 特定の構造を前提としない関数

    これに対して、「Scheme:オブジェクト指向表現」 の 抽象化の方向 には、オブジェクト指向と比べながら、関数指向のメリットが述べられている。

    自分の理解した範囲で、簡単にまとめると、

    1. オブジェクト指向では、はじめに「型、構造」を想定し、それに相応しい操作を定義していく。これにより、「型、構造」と操作は不可分となり、操作のためのコンテナが必要となる。
    2. 関数指向では、「型、構造」を事前に想定しない関数を定義することができる。「型、構造」と操作が独立していることにより、必要な操作を、必要に応じて、定義することができる。これを実現するために、クロージャを用いる。

    クロージャに関しては、以下を参照。

    脱線するが、上記に関連して、「Why OO sucks」のオブジェクト指向に対する批判を、また、思い出した。

    Objection 1 - Data structure and functions should not be bound together
    Objects bind functions and data structures together in indivisible units. I think this is a fundamental error since functions and data structures belong in totally different worlds. Why is this?
    • Functions do things. They have inputs and outputs. The inputs and outputs are data structures, which get changed by the functions. In most languages functions are built from sequences of imperatives: "Do this and then that ..." to understand functions you have to understand the order in which things get done (In lazy FPLs and logical languages this restriction is relaxed).
    • Data structures just are. They don't do anything. They are intrinsically declarative. "Understanding" a data structure is a lot easier than "understanding" a function.

    (装飾は引用者による)

    Scheme:オブジェクト指向表現」 の 抽象化の方向に戻り、少し長めに引用する。

    関数指向の考え方をちょっと示してみます。 与えられた「木」のすべての葉を深さ優先で辿り、与えられた処理を施して行く、という処理を考えてみます。 但し、木の具体的な実装はわかりません。…

    関数型では、抽象的な「木」に関する可能な操作を直接関数で渡してやります。

      (define (tree-walk tree proc leaf? walker)
        (define (rec node)
          (walker (lambda (n) (if (leaf? n) (proc n) (rec n))) node))
        (if (leaf? tree) (proc tree) (rec tree)))

    ここで、leaf? は木のノードを取り、それが葉かそうでないかを返す関数、 walkerは関数と木のノードを取り、ノードのすべての子供に対して渡された関数を適用する関数。…

    関数指向のメリットは、tree型に対して適用可能な操作というものを限定していないことです。tree-walkを適用したくなったら、leaf? と walkerに相当する関数を(なんならその場ででも)作って渡してやれば良いのです。…

    • (Shiro) この木の例に関しては、コンスセルによる表現を全く仮定していないっす。 (walkerはリストを返す、とかいう制約もついていません)。コンスセルだろうがアレイだろうが適当なコンテナタイプだろうがファイルシステムだろうが、詳細をクロージャの中に隠蔽する、というのがクロージャ指向 :-) の流儀だと思います。

    (装飾は引用者による)

    先ほど述べたように、関数を適用する対象の構造を前提とせず、構造は後回しにして、関数を定義。クラス指向のように、型と操作が密結合してないので、必要に応じて、構造に対する操作を関数に渡せば良い。

    しかし、例としてあげられていた関数を、スラスラと理解できないので、ゆっくりと確認していくことに。 (+_+)

    とりあえず、Scheme は見慣れてないので、上記 tree-walk を Haskell で書いてみる。

    treewalk tree proc isLeaf walker = if isLeaf tree 
                                       then proc tree
                                       else rec tree
        where
          rec node = walker (\n -> if isLeaf n
                                   then proc n
                                   else rec n)
                            node

    if の部分を整理して、

    treewalk tree proc isLeaf walker = treewalk' tree
        where
          treewalk' = \n -> if isLeaf n
                            then proc n 
                            else walker treewalk' n

    う-ん、これでもまだ、動作のイメージがつかめない。。(@_@;

     

    treewalk 関数の引数について

    treewalk 関数の引数を、一つ一つ見ていく。

    • 第 1 引数 tree : treewalk を適用する対象
    • 第 2 引数 proc : 葉に適用する関数
    • 第 3 引数 isLeaf : ノードが葉である場合に、真を返す述語

    イメージしにくいのは、第 4 引数 walker 。この関数が使われているのは、上記コードの3 行目。

    walker treewalk’ n

    tree-walk 関数の説明より、適用対象が「木」であるとき、walker の

    • 第 2 引数は、木のノード。(親ノード)
    • 第 1 引数は、木のノード(子ノード)に適用する関数。

    となるように、利用が想定されている。ただし、あくまでも「想定」であり、使う側が機能を満たすように実装する必要がある。

     

    第4引数 walker がイメージしにくい理由

    treewalk 関数の機能は、以下の通りだった。

    与えられた「木」のすべての葉を深さ優先で辿り、与えられた処理を施して行く

    定義した関数の引数の名前を使い、言い換えると、

    対象 tree に対して、要素が isLeaf 関数で真となる「葉」に、関数 proc を適用する

    treewalk 関数の実装を見ると、すべての葉を「辿る」ための手段が具体的に書かれていない。いきなりノードに対する処理が記述されおり、walker が何をするか不明なまま、関数が再帰的に定義されているように見える。この点がイメージしにくいところ。

     

    ノードを辿っていく関数 walker

    CropperCapture[310]木のノードを辿るには、親ノードから、その子ノードへ辿る方法が必要となる。どこかに定義されていなくては、木の要素を辿れない。 treewalk 関数において、その具体的な方法が期待されるのが walker 。

    walker を加え、もう一度、言い換えると、

    対象 tree に対して、要素を辿るために walker を使い、要素が isLeaf 関数で真となる「葉」に、proc を適用する

    今回は、すべての葉を対象に proc 関数を適用したい。よって、次のように walker の役割を想定することになる。

    • 第4引数 walker : 与えられたノードから辿ることのできる、子ノードに対して、関数を適用する。

    このように、walker にノードの辿り方を任せるメリットは、treewalk 関数のような、特定の構造を前提としない関数を定義できることにある。関数の適用対象の構造が変われば、walker を取り替えるだけで済むため、異なる構造に対して、共通部分を抽出できる。

    結果的に walker を、

    構造を結びつける関数

    と見なすことができる。

     

    treewalk 関数のまとめ

    treewalk で行っていることをまとめると、

    1. 対象のノードが葉の場合、ノードに proc を適用する。
    2. 対象のノードが葉ではない場合、walker に、そのノードと関数を与え、与えたノードから辿ることができる子ノードに関数を適用する。

    treewalk 関数の型を調べると、

    treewalk
      :: t1 -> (t1 -> t) -> (t1 -> Bool) -> ((t1 -> t) -> t1 -> t) -> t

    第1引数が、特定の型に縛られていないことがわかる。

     

    treewalk 関数の使い方

    先ほど使った変数 tree に、treewalk 関数を適用するには、次のように引数を与える。

    main = do print $ treewalk tree
                               (\(Leaf x) -> Leaf (x * 2))
                               (\n -> case n of
                                        Leaf _     -> True
                                        Branch _ _ -> False)
                               (\f (Branch l r) -> Branch (f l) (f r))
    • 第2引数は、葉に対する処理。
    • 第3引数は、葉であるときに真を返す関数。

    第 4 引数の walker は、親となるノード(walker の第 2 引数)から、辿ることができる子ノードに対して、関数(walker 関数の第1引数)を適用する。結果は、以下のようになる。

    Branch (Leaf 2) (Branch (Branch (Leaf 4) (Leaf 6)) (Leaf 8))

     

    Data.Tree 型の値に対して treewalk 関数を適用

    同じ treewalk 関数を使い、Data.Tree 型の値に適用してみる。

    import Data.Tree
    
    tree2 = Node 1 [ Node 2 []
                   , Node 3 [ Node 4 []
                            , Node 5 []
                            , Node 6 []]]

    これに対して、

    main = print $ treewalk tree2
                            (\(Node x xs) -> Node (x*2) xs)
                            (\n -> case n of
                                     Node x [] -> True
                                     otherwise -> False)
                            (\f (Node x xs) -> Node x (map f xs))

    子を持たない Node を「葉」と見なし、ノードを辿る walker は、先ほどの定義を少し変更するだけで済んだ。

    CropperCapture[311]

    結果は、

    Node { rootLabel = 1
         , subForest = [ Node { rootLabel = 4
    , subForest = []
                              }
                       , Node { rootLabel = 3
                              , subForest = [ Node { rootLabel = 8
    , subForest = []
                                                   }
                                            , Node { rootLabel = 10
    , subForest = []
                                                   }
                                            , Node { rootLabel = 12
    , subForest = []
                                                   }
                                            ]
                              }
                       ]
         }

    ちなみに、「葉」に対してのみ、関数が適用されるので、子を持つノードの値は変わらない。

     

    リストに対して treewalk 関数を適用

    では、treewalk 関数を使って、リストの要素を 2 倍できるだろうか?

    リストの場合、各要素をすべて「葉」と見なし、walker が次の要素へと辿るように考えればいいのかな…

    CropperCapture[312]

              print $ treewalk [0..5]
                               (\(x:xs) -> (x*2) : xs)       
                               (\_ -> True)
                               (\f n -> case n of
                                          x : xs -> x : f xs
                                          [] -> [])

    しかし、結果、要素は 2 倍されなかった。。 (+_+)

    理由は、treewalk が proc を適用するのは「葉」に対してだけれど、walker がノードを辿るのは、葉ではない要素であるため。リストの各要素を、「葉である」と同時に「子ノードを持つ」と見なしたので、proc を適用できなかった。先ほど Data.Tree 型の値に対して、treewalk 関数を適用したら、子を持つノードの値が変わらなかったことと同じ。

     

    3. 要素を辿っていく walk 関数について、はじめから再考

    treewalk 関数のように、要素を辿る関数について、単純な形から考えてみる。

    まずは、以下の 2 つの引数を与える関数。

    • 第 2 引数: ノード (node)
    • 第 1 引数: 上記ノードから辿ることができる要素に、関数を適用していく、と想定する関数 (walker)

    関数名を walk とすると、定義は以下の通り。

    walk walker node = walker (\n -> walk walker n) 
                              node

    改めて、適用対象の「木」の型を、以下のように定義する。

    data BinaryTree a = Leaf a 
                      | Branch (BinaryTree a) a (BinaryTree a) 
                      deriving Show

    適当に値を作る。

    tree = Branch (Leaf 1)
                  100
                  (Branch (Leaf 2)
                          200
                          (Leaf 3))

    これに対し、walk 関数を適用してみる。要素を辿るだけで、何もせず、そのまま返すには、

      print $ walk (\f n -> case n of
                         Leaf x -> Leaf x
                         Branch l x r -> Branch (f l) x (f r))
                   tree
    => Branch (Leaf 1) 100 (Branch (Leaf 2) 200 (Leaf 3))

    葉を 2 倍し、枝を 3 倍するなら、

      print $ walk (\f n -> case n of
                         Leaf x -> Leaf (x*2)
                         Branch l x r -> Branch (f l) (x*3) (f r))
                   tree
    => Branch (Leaf 2) 300 (Branch (Leaf 4) 600 (Leaf 6))

     

    Data.Tree 型に対して、walk 関数を適用

    次に、先ほど使った Data.Tree 型の値 (tree2) に、同じように walk 関数を適用してみる。

      -- 何もしないで、そのまま返す
      print $ walk (\f n -> case n of
                              Node x [] -> Node x []
                              Node x xs -> Node x $ map f xs)
                   tree2
    
      -- 葉を 2 倍し、枝を 3 倍する
      print $ walk (\f n -> case n of
                              Node x [] -> Node (x*2) []
                              Node x xs -> Node (x*3) $ map f xs)
                   tree2

     

    リストに対して、walk 関数を適用

    リストに対して、walk 関数を適用してみる。

      print $ walk (\f n -> case n of
                              []     -> []
                              x : xs -> x : f xs)
                   [0..5]
    
      print $ walk (\f n -> case n of
                              []     -> []
                              x : xs -> (x*2) : f xs)
                   [0..5]

     

    3-1. walk 関数から、要素に適用する関数を分離

    上記の方法は、

    1. ノードを辿っていくこと
    2. ノードに関数を適用すること

    の 2 つを同時に行っている。これを分離したい。

    「ある構造を持つ型に対して、各要素を辿り、特定の関数を適用する関数」に変更。

    walk f walker node = walker (\n -> walk f walker n) 
                                (f node)
    • 第1引数 f は、要素に適用する関数。

    その他は同じ。

    これを使い、何もしないで、そのまま返すには、

      print $ walk id
                   (\f n -> case n of
                              Leaf x       -> Leaf x
                              Branch l x r -> Branch (f l) x (f r))
                   tree
    => Branch (Leaf 1) 100 (Branch (Leaf 2) 200 (Leaf 3))

    要素を辿る関数を使いまわしたいので、別に定義しておく。

    walkBinaryTree _ (Leaf x)       = Leaf x
    walkBinaryTree f (Branch l x r) = Branch (f l) x (f r)

    葉は 2 倍し、枝を 3 倍するなら、

      print $ walk (\n -> case n of 
                            Leaf x       -> Leaf (x*2)
                            Branch l x r -> Branch l (x*3) r)
                   walkBinaryTree
                   tree
    => Branch (Leaf 2) 300 (Branch (Leaf 4) 600 (Leaf 6))

    要素に適用する関数も使いまわしたいので、別に定義。

    doubleLeafTripleBranch (Leaf x)       = Leaf $ x*2
    doubleLeafTripleBranch (Branch l x r) = Branch l (x*3) r

    要素を辿ったとき、リストを返して欲しいなら、

    walkBinaryTree2List _ (Leaf x)       = [x]
    walkBinaryTree2List f (Branch l x r) = x : f l ++ f r

    これを使い、

      print $ walk doubleLeafTripleBranch
                   walkBinaryTree2List
                   tree
    => [300,2,600,4,6]

    逆に辿りたい場合は、辿り方を変更する。

    reverseWalkBinaryTree2List _ (Leaf x)       = [x]
    reverseWalkBinaryTree2List f (Branch l x r) = f r ++ f l ++ [x]

    これを使い、

      print $ walk doubleLeafTripleBranch
                   reverseWalkBinaryTree2List
                   tree
    => [6,4,600,2,300]

     

    Data.Tree に対して、walk 関数を適用

    次に、Data.Tree 型の値を対象にする。

    Node の辿り方と、要素に適用する関数を予め定義。

    walkTree f (Node x xs) = Node x (map f xs)
    
    doubleNodeTripleBranch (Node x []) = Node (x*2) []
    doubleNodeTripleBranch (Node x xs) = Node (x*3) xs

    これを使い、

      print $ walk doubleNodeTripleBranch
                   walkTree
                   tree2
    => Node { rootLabel = 3
            , subForest = [ Node { rootLabel = 4
                                 , subForest = []
                                 }
                          , Node { rootLabel = 9
                                 , subForest = [ Node { rootLabel = 8
                                                      , subForest = []
                                                      }
                                               , Node { rootLabel = 10
                                                      , subForest = []
                                                      }
                                               , Node { rootLabel = 12
                                                      , subForest = []
                                                      }
                                               ]
                                 }
                          ]
            }

     

    リストに対して、walk 関数を適用

    同じく、リストの場合。

    要素の辿り方と、要素に適用する関数を予め定義しておく。

    walkList _ []     = []
    walkList f (x:xs) = x : f xs
    
    doubleElem [] = []
    doubleElem (x:xs) = (x*2) : xs

    これを使い、

      print $ walk doubleElem
                   walkList
                   xs
    => [2,4,6,8,10]

     

    3-2. walk 関数から、更に述語を分離

    次に、変数 tree に戻り、「偶数であるノードの値」に対して、葉を 2 倍し、枝を 3 倍したいとする。

      print $ walk (\n -> case n of
                            Leaf x       | even x    -> Leaf (x*2)
                            Branch l x r | even x    -> Branch l (x*3) r
                            node                     -> node)
                   walkBinaryTree
                   tree
    => Branch (Leaf 1) 300 (Branch (Leaf 4) 600 (Leaf 3))
    • 要素に対して関数を適用する
    • 要素が条件を満たしているか検査する

    という 2 つのことを同時に行っているので、これを分離してみる。

    walk p f walker node = walker (\n -> walk p  f walker n) 
                                  (if p node then f node else node)

    第1引数 p は述語で、これが真である場合、第2引数 f をノードに適用する。

    予め、ノードの値が、偶数である場合、真を返す述語を定義。

    isNodeEven (Leaf x)       | even x = True
    isNodeEven (Branch l x r) | even x = True
    isNodeEven _                       = False

    これを使い、

      print $ walk isNodeEven
                   doubleLeafTripleBranch
                   walkBinaryTree
                   tree
    => Branch (Leaf 1) 300 (Branch (Leaf 4) 600 (Leaf 3))

    結果をリストにするなら、

      print $ walk isNodeEven
                   doubleLeafTripleBranch
                   walkBinaryTree2List
                   tree
    [300,1,600,4,3]

     

    Data.Tree に対して、walk 関数を適用

    Data.Tree 型の値を対象にする場合、

      print $ walk (\n -> case n of
                            Node x xs | even x -> True
                            otherwise          -> False)
                   doubleNodeTripleBranch
                   walkTree
                   tree2
    => Node { rootLabel = 1
            , subForest = [ Node { rootLabel = 4
                                 , subForest = []
                                 }
                          , Node { rootLabel = 3
                                 , subForest = [ Node { rootLabel = 8
                                                      , subForest = []
                                                      }
                                               , Node { rootLabel = 5
                                                      , subForest = []
                                                      }
                                               , Node { rootLabel = 12
                                                      , subForest = []
                                                      }
                                               ]
                                 }
                          ]
            }

     

    リストに対して、walk 関数を適用

    リストを対象にする場合、

      print $ walk (\n -> case n of
                            x : _ | even x -> True
                            otherwise      -> False)
                   doubleElem
                   walkList
                   xs
    [1,4,3,8,5]

    このように、構造を辿る方法を抽象化しておくと、同じ関数を使い回し、組み合わせることができるようだ。

     

    参考サイト