Guile基礎/ローカル変数の束縛(let 式他)

変更履歴

概 要

目 次

参考資料

let 式

let 式は次の文法に沿って記述します.
(let bindings body+) syntax
   bindings     ::=  (bind*)
   bind         ::=  (identifier  expression)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式
注意: 束縛部(bindings)に現れる変数(identifier)は互いに異なっていなければなりません.

補足: R7RS に従うと,let 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,let 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

let 式はローカルな変数を使った処理を記述するための構文形式です.束縛部(bindings)は,ローカルな変数(identifier)を式(expression)の値に初期設定するためのもので,本体(body)は let 式の処理内容を記述したものです. Guileは,let 式を次のように評価します.
  1. まず let 式の実行を開始した時点の環境を次のように拡張します.
    1. 束縛部の中のすべての式を let 式を開始した時点の環境のもとで評価します.
    2. 束縛部の中のそれぞれの変数を新たな場所に束縛して, その場所に上で求めた式の値を格納します.
  2. 次に,拡張された環境のもとで本体に含まれる定義や式を先頭から順に評価し, 最後に評価した式の値を返します. また,束縛部で生成した束縛をすべて破棄し, 環境を let 式の実行を開始した時点のものに戻します.

具体例

以下のプログラムは,ローカルな変数 x と y をそれぞれ 10 と 20 に束縛して, 本体を実行しています.
;; let-example.scm
(let ((x 10) (y 20))
  (display "x=") (display x) (newline)
  (display "y=") (display y) (newline)
  (display "x+y=") (display (+ x y)) (newline)
  (display "x*y=") (display (* x y)) (newline))
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=10
y=20
x+y=30
x*y=200

具体例

let 式の束縛部において手続きを生成することもできます.
;; let-example.scm
(let ((sqr (lambda (x) (* x x)))
      (a 3) 
      (b 4))
  (display "a*a+b*b=") (display (+ (sqr a) (sqr b))) (newline))
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
a*a+b*b=25
ただし,再帰的な手続きは定義できません. 再帰的な手続きを定義したいときには letrec 式や letrec* 式を利用します.

手続き呼び出しへの翻訳

R7RS[7.3. Derived expression types]TSPL[Section 4.4. Local Binding] によると

(let (($x_1$ $e_1$) ... ($x_n$ $e_n$)) body+)

という let 式は

((lambda ($x_1$ ... $x_n$) body+) $e_1$ ... $e_n$)

という手続き呼び出しに翻訳できます.

具体例

最初の具体例で示した let 式は,以下に示す手続き呼び出しに等価です.
;; let-example.scm
((lambda (x y) 
   (display "x=") (display x) (newline)
   (display "y=") (display y) (newline)
   (display "x+y=") (display (+ x y)) (newline)
   (display "x*y=") (display (* x y)) (newline))
 10 20)
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=10
y=20
x+y=30
x*y=200

ローカル変数のスコープ

束縛部の中の変数のスコープ(有効範囲)は let 式の本体だけです. 束縛部自身は含まれません.もちろん let 式の外側は含まれません. 従って,初期設定の式を評価するとき,束縛部によって生成される束縛を使うことはできません.端的に言うと,束縛部におけるそれぞれの変数の初期設定は他の変数とは独立して行われます.

初期設定の式の中で, 束縛部で生成した変数と同じ名前の変数を利用することはまったく問題ありません. ただし,その変数は let 式の外側で定義した変数になります. もう少し一般的に言うと,let 式の外側で定義した変数と let 式の束縛部で生成した変数の名前が等しいとき,初期設定の式の中では外側の変数が使用され,let 式の本体では束縛部の変数が使用されます.

具体例

ローカルな変数のスコープについて確認してみましょう.
;; let-example.scm
(define x 111)
(define y 222)
(let ((x 10) (y x) (z (+ x y)))
   (display "x=") (display x) (newline)
   (display "y=") (display y) (newline)
   (display "z=") (display z) (newline))
初期設定の式に現れる変数は,すべて,let 式の外側で定義されたものです. 上の束縛部の場合,y を初期化する x は let 式の外側にある define 形式によって定義された x です.従って,y は外側の x の値(111)に初期設定されます.z についても,それを初期化する x と y は let 式の外側で定義された変数です.従って,z は それらの和(333)に初期設定されます.
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=10
y=111
z=333
また,この結果が示すように,let 式の本体における x と y は,let 式の外側で定義された変数ではなく,束縛部によって生成された変数です.

ネストした let 式

let 式をネストさせることによって,外側の let 式の束縛部で生成した束縛を内側の let 式全体で(特に,束縛部の中で)利用することができます.言い換えると,ネストした let 式によって初期設定を逐次的に行うことができます.

具体例

;; let-example.scm
(define x 111)
(define y 222)
(let ((x 10))
  (let ((y (+ x 20)))
    (let ((z (+ x y)))
      (display "x=") (display x) (newline)
      (display "y=") (display y) (newline)
      (display "z=") (display z) (newline))))
内側の let 式の y を初期設定する式 (+ x 20) における x は,外側の let 式の束縛部で生成された x です.define 形式によって定義された x ではありません.従って,y は 30(=10+20)に初期設定されます.同様に,z を初期設定する式における x と y は外側の let 式の束縛部で生成された変数です.define 形式によって定義された変数ではありません.従って,z は 40(=10+30)に初期設定されます.
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=10
y=30
z=40
この具体例は, ネストした let 式によってローカル変数を逐次的に初期設定できることを示しています.

let* 式

let* 式の文法は, 構文キーワードが let* であることを除いて,let 式の文法とまったく同じです.
(let* bindings body+) syntax
   bindings     ::=  (bind*)
   bind         ::=  (identifier  expression)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式

補足: 束縛部(bindings)の中に現れる変数(identifier)は互いに異なっている必要はありません.

補足: R7RS に従うと,let* 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,let* 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

R7RS[7.3. Derived expression types]TSPL[Section 4.4. Local Binding] を参考にすると, let* 式はネストした let 式の糖衣構文と見なすことができます.つまり,

(let* (($x_1$ $e_1$) ($x_2$ $e_2$) ... ($x_n$ $e_n$)) body+)

という let* 式は,次のようなネストした let 式に等価です.

(let (($x_1$ $e_1$))
 (let (($x_2$ $e_2$))
   ......
    (let (($x_n$ $e_n$)) body+) ... ))

具体例

前節の最後に示したネストした let 式は次の let* 式に書き換えることができます.
;; let-example.scm
(let* ((x 10)
       (y (+ x 20))
       (z (+ x y)))
  (display "x=") (display x) (newline)
  (display "y=") (display y) (newline)
  (display "z=") (display z) (newline))
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=10
y=30
z=40

具体例

束縛部における変数名は互いに異なっていなくてもかまいません. 以下の let* 式の束縛部では,まず x は 10 に設定され,次に y は 30(=10+20)に設定され,その次に x が 100 に再設定され,最後に z は 130(=100+30)に設定されます.
;; let-example.scm
(let* ((x 10)
       (y (+ x 20))
       (x 100)
       (z (+ x y)))
  (display "x=") (display x) (newline)
  (display "y=") (display y) (newline)
  (display "z=") (display z) (newline))
guile> (load "let-example.scm")
      ...... コンパイルメッセージ ......
x=100
y=30
z=130
この具体例が示すように,let* 式の束縛指定(bind)は先頭から順に評価されていって,初期設定の式や本体を評価するときには各時点の最新の変数の値が使用されます.

letrec 式

letrec 式の文法は, 構文キーワードが letrec であることを除いて,let 式の文法とまったく同じです.
(letrec bindings body+) syntax
   bindings     ::=  (bind*)
   bind         ::=  (identifier  expression)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式

注意: 束縛部(bindings)の中に現れる変数(identifier)は互いに異なっていなければなりません.

補足: R7RS に従うと,letrec 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,letrec 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

R7RS[7.3. Derived expression types]TSPL[Section 4.4. Local Binding] によれば

(letrec (($x_1$ $e_1$) ... ($x_n$ $e_n$)) body)

は次のような let 式に翻訳できます.

(let (($x_1$ $\bot$) ... ($x_n$ $\bot$))
 (let (($temp_1$ $e_1$) ... ($temp_n$ $e_n$))
  (set! $x_1$ $temp_1$)
   ......
  (set! $x_n$ $temp_n$)
  body+))

ここで:

上記の外側の let 式は,ローカル変数の$x_1$〜$x_n$を(値は不定のままとりあえず)場所に束縛することによって,内側の let 式全体にわたって$x_1$〜$x_n$を使えるようにしています.従って,不定の値($\bot$)を参照さえしなければ,$e_1$〜$e_n$の中でも$x_1$〜$x_n$を使用することができます.このことと lambda 式の本体が手続き呼び出し時まで評価されないことによって,letrec 式の束縛部では再帰的(および相互再帰的)な手続きを生成することができます.

要約

上で述べたことを要約しておきます.

具体例

以下の sum は2つの数値 start と end を受け取って,start〜end の総和を返す(再帰的な)手続きです.ただし,start$>$end の場合には 0 を返します.letrec 式の本体では sum を使って1〜10の総和を求めています.
;; letrec-example.scm
(letrec ((sum (lambda (start end)
                (if (> start end) 
                    0 
                    (+ start (sum (1+ start) end))))))
  (sum 1 10))
guile> (load "letrec-example.scm")
      ...... コンパイルメッセージ ......
$1 = 55

具体例

以下の isort は,数値からなるリスト lst を小さい順にソートし直したリストを返します.ソーティングのアルゴリズムは挿入ソートを使っています.insert は,数値 x と小さい順にソート済みのリスト lst を受け取って,lst の適切な位置(小さい順に反しない位置)に x を挿入します.letrec 式の本体では,適当なリストに対して isort を実行しています.
;; letrec-example.scm
(letrec
    ((insert (lambda (x lst) 
               (cond 
                ((null? lst) (list x))
                ((<= x (car lst)) (cons x lst))
                (else (cons (car lst) (insert x (cdr lst)))))))
     (isort (lambda (lst) 
              (if (null? lst) 
                  '()
                  (insert (car lst) (isort (cdr lst)))))))
  (isort '(9 1 8 2 7 3 6 4 5)))
guile> (load "letrec-example.scm")
      ...... コンパイルメッセージ ......
$1 = (1 2 3 4 5 6 7 8 9)

letrec* 式

letrec* 式の文法は, 構文キーワードが letrec* であることを除いて,let 式の文法とまったく同じです.
(letrec* bindings body+) syntax
   bindings     ::=  (bind*)
   bind         ::=  (identifier  expression)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式

注意: 束縛部(bindings)の中に現れる変数(identifier)は互いに異なっていなければなりません.

補足: R7RS に従うと,letrec* 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,letrec* 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

R7RS[7.3. Derived expression types]TSPL[Section 4.4. Local Binding] によれば

(letrec* (($x_1$ $e_1$) ... ($x_n$ $e_n$)) body)

は次のような let 式に翻訳できます.

(let (($x_1$ $\bot$) ... ($x_n$ $\bot$))
 (set! $x_1$ $e_1$)
  ......
 (set! $x_n$ $e_n$)
body+)

ここで,$\bot$ は letrec 式のところで説明した「不定の値」を表す便宜的な記号です. 上記の初期設定の式($e_1$〜$e_n$)や本体(body)の中でこれを参照しようとしたときには動作は不確定である(または,エラーが発生する)とします.それから,letrec 式の場合と同様に,上記の翻訳(body+を let 式で囲まないこと)は Guile の言語仕様に基づいています.

要約

上記の翻訳が意味するところは次のように要約できます.

letrec* 式は,再帰的な手続きを定義するとともに束縛部を逐次的に評価したい場合に使用することになります.そういった場合が発生するような自然な状況がなかなか思いつかないのですが,1つだけ言えることは,define 形式による内部定義 を利用した場合,GuileのようなR6RSやR7RSに準拠しようとしている処理系では,暗黙的に letrec* 式を利用していることになります.

let-values 式と let*-values 式

モジュールのロード

Guile(少なくとも 3.0.5)において let-values や let*-values を使用するためには, 以下に示すように (srfi srfi-11) モジュールをロードする必要があります.
(use-modulets (srfi srf-11))

文法

let-values 式と let*-values 式は次の文法に沿って記述します.
(let-values mv-bindings body+) syntax
(let*-values mv-bindings body+) syntax
   mv-bindings  ::=  (mv-bind*)
   mv-bind      ::=  (formals expression)
   formals      ::=  (identifier*)
                    |  identifier
                    |  (identifier+ . identifier)
   identifier   ::=  識別子
   body         ::=  definition* expression+
   definition   ::=  定義(define形式など)
   expression   ::=  式

補足: R7RS に従うと,let-values 式や let*-values 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,let-values 式や let*-values 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

let-values 式 と let*-values 式は,一般に, 初期設定の式(expresion)が多値を返すとき, その多値を分解してローカル変数(identifier)を束縛するための構文形式です.ただし,以下に述べる変数指定のパターンと適合する場合には,初期設定の式は多値ではない普通の値を返す式でもかまいません.

多値が扱える点を除くと,式全体の評価の仕方は let 式 や let* 式と同じです.

変数指定のパターンと意味

上記の文法における formals はlmbda 式の仮引数と同じ形式のものです. 従って,束縛指定(mv-bind)は次の3つのパターンがあります. 以下の $x_k$ や $x$ は変数を表し,$e$ は初期設定の式(多値を返す式)を表しています.
  1. (($x_1$ ... $x_n$) $e$)
    この場合,$e$はちょうど$n$個の値を返す式でなければいけません. そうでない場合,エラーが発生します. これは,$e$が返す$n$個の値に$x_1$〜$x_n$を束縛します. $n=1$のときには,$e$は普通の値を返す式でもかまいません.
  2. ($x$ $e$)
    これは,$e$が返す多値からなるリストに$x$を束縛します. $e$は普通の値を返す式でもかまいません.ただし,その場合であっても, $x$は$e$の値からなるリストに束縛されます.
  3. (($x_1$ ... $x_n$ . $x_{n+1}$) $e$)
    この場合,$e$は$n$個以上の値を返す式でなければいけません. そうでない場合,エラーが発生します. これは,$e$が返す多値のうち,先頭の$n$個の値に$x_1$〜$x_n$を束縛し, 残りの値からなるリストに$x_{n+1}$を束縛します. $n=1$のときには,$e$は普通の値を返す式でもかまいません. その場合,$x_1$は$e$の値に束縛され,$x_2$は空リスト '() に束縛されます.

具体例

上記の3つのパターンを試してみます.
;; letval.scm
(use-modules (srfi srfi-11))
(let-values
    (((a b c) (values 1 2 3))
     (p (values 11 22 33))
     ((x y . z) (values 55 66 77 88 99)))
  (format #t "a=~A b=~A c=~A\n" a b c)
  (format #t "p=~A\n" p)
  (format #t "x=~A y=~A z=~A\n" x y z))
この let-values 式は各ローカル変数を次のように束縛します.
  1. 束縛指定の1番目の   ((a b c) (values 1 2 3))   は,上記の1.のパターンです. これは,ローカル変数の a,b,c をそれぞれ 1,2,3 に束縛します.
  2. 2番目の   (p (values 11 22 33))   は,上記の2.のパターンです.これは,ローカル変数の p を 11,22,33 からなるリスト (11 22 33) に束縛します.
  3. 3番目の   ((x y . z) (values 55 66 77 88 99))   は,上記の3.のパターンです.これは,ローカル変数の x と y をそれぞれ 55 と 66 に束縛し,z を残りの値からなるリスト (77 88 99) に束縛します.
以下は実行結果です.最後の $1 = #t は let-values 式の返り値(最後のformat 式の返り値)を示しています.
guile> (load "letval.scm")
      ...... コンパイルメッセージ ......
a=1 b=2 c=3
p=(11 22 33)
x=55 y=66 z=(77 88 99)
$1 = #t

具体例

let*-values 式は let-values 式の let* 版です.つまり, 束縛指定を先頭から順に逐次的に評価していって, それぞれの初期設定の式を評価するときには,それまでに確立したローカル変数の値を利用することができます.以下の具体例では,これを確認してみます.
;; letval.scm
(use-modules (srfi srfi-11))
(let*-values 
    (((a b) (values 10 20))
     (p (values (+ a b) (* 2 a) (/ b 4)))
     ((x y . z) (values (car p) (cadr p) a b p)))
  (format #t "a=~A b=~A\n" a b) 
  (format #t "p=~A\n" p)
  (format #t "x=~A y=~A z=~A\n" x y z))
guile> (load "letval.scm")
      ...... コンパイルメッセージ ......
a=10 b=20
p=(30 20 5)
x=30 y=20 z=(10 20 (30 20 5))
$1 = #t

名前付き let 式

名前付き let 式(named let expression)は, 再帰的な手続きの定義と実行を同時に行うことのできる構文です. 繰り返し処理を記述するために利用することが多いと思います. そのため,R7RS は, 名前付き let 式を繰り返し処理の構文として分類しています.

名前付き let 式は次の文法に沿って記述します.let キーワードの直後に identifier を入れることを除いて,記述形式は let 式と同じです.
(let identifier bindings body+) syntax
   bindings     ::=  (bind*)
   bind         ::=  (identifier  expression)
   identifier  ::=  識別子
   body        ::=  definition* expression+
   definition  ::=  定義(define形式など)
   expression  ::=  式
注意: 束縛部(bindings)に現れる変数(identifier)は互いに異なっていなければなりません.

補足: R7RS に従うと,名前付き let 式の本体は body を1つしか記述できません.しかし,Gule 3.0 では,名前付き let 式の本体(body)は定義と式を混在して記述できます.言い換えれば,body を複数記述することができます.上記の文法はこれを反映しています. (参照:Guile[6.10.3 Internal definitions]define 形式による内部定義

名前付き let 式
(let name (($x_1$ $e_1$) ... ($x_n$ $e_n$)) body+)
は,$x_1$〜$x_n$ を仮引数とし body+ を本体とする手続き, つまり,
(lambda ($x_1$ ... $x_n$) body+)
という手続きを生成して,その手続きに name を束縛します. さらに,その手続きを$e_1$〜$e_n$を実引数として呼び出します.つまり,
(name $e_1$ ... $e_n$)
という手続き呼び出しを実行します.

body+ の中で name を使用することができます.そのため,再帰的な手続きを定義することができます.一方,$e_1$〜$e_n$の中では name(の束縛)は使用できません.もし name を使用した場合には,名前付き let 式の外部で定義された値を使用することになります. ちなみに,body+ の中で name を使用しなかった場合には let 式とまったく同じ処理をします.また,上記の文法が示しているように,束縛部は空でもかまいません. その場合,無引数の手続き(あるいは,無束縛のlet式)を実行することになります.

基本的な式への翻訳

R7RS[7.3. Derived expression types]TSPL[Section 5.4. Recursion and Iteration] によれば,上で示した名前付き let 式は,次のような式に翻訳できます.

((letrec ((name (lambda ($x_1$ ... $x_n$) body+))) name) $e_1$ ... $e_n$)

ちょっと分かりにくいので,次のように翻訳することもできます. 翻訳の要点は,$e_1$〜$e_n$の評価を name の束縛とは無関係に行なうことです.

(let (($temp_1$ $e_1$) ... ($temp_n$ $e_n$))
 (letrec ((name (lambda ($x_1$ ... $x_n$) body+)))
  (name $temp_1$ ... $temp_n$)))

ここで,$temp_1$〜$temp_n$はプログラム中のどこにも使われていないまったく新しい(一時的な)変数を表していて,$e_1$〜$e_n$の値を手続きの name に渡すためだけに使用しています.$e_1$〜$e_n$が name を含んでいないのであれば, 外側の let 式を削除して,次のように翻訳できます.

(letrec ((name (lambda ($x_1$ ... $x_n$) body+)))
 (name $e_1$ ... $e_n$)))

具体例

以下の名前付き let 式は 1〜10 の総和を求める手続き sum を定義して実行します.
;; namelet.scm
(let sum ((k 1) (s 0))
  (if (> k 10)
      s
      (sum (1+ k) (+ s k))))
guile> (load "namelet.scm")
      ...... コンパイルメッセージ ......
$1 = 55
上記の名前付き let 式は以下の letrec 式と等価です.
(letrec ((sum (lambda (k s) (if (> k 10)
                                s
                                (sum (1+ k) (+ s k))))))
  (sum 1 0))

具体例

名前付き let 式は letrec式と等価なので, 末尾再帰的な手続きだけでなく, 一般の再帰的な手続きを定義することもできます. 以下の名前付き let 式は 10 番目のフィボナッチ数を漸化式の通りに計算する再帰的手続きを定義して実行します.
;; namelet.scm
(let fib ((n 10))
  (if (<= n 1)
      n
      (+ (fib (- n 1)) (fib (- n 2)))))
guile> (load "namelet.scm")
      ...... コンパイルメッセージ ......
$2 = 21

補足

以下の check-ab は,正整数 n と2以上の整数 a,b を受け取って, n が a$^i$b$^j$($i,j \geq 0$)の形をしていたら #t を返し, そうでなければ #f を返します.また,途中の計算過程を format を使って表示しています.
;; check-ab.scm
(define (check-ab n a b)
  (let loop ((n n))
    (format #t "n=~A\n" n)
    (cond 
     ((= n 1) #t)
     ((= (remainder n a) 0) (loop (/ n a)))
     ((= (remainder n b) 0) (loop (/ n b)))
     (else #f))))
guile> (load "check-ab.scm")
      ...... コンパイルメッセージ ......
guile> (check-ab 36 3 4)
n=36
n=12
n=4
n=1
$1 = #t
guile> (check-ab 60 3 4)
n=60
n=20
n=5
$3 = #f

上記の名前付き let 式の束縛部に (n n) といった束縛指定があります. 右側の n は名前付き let 式の外側で定義された変数(つまり,check-ab 手続きの仮引数)を表していて,左側の n は名前付き let 式の内部で使用するローカル変数(というか,loop 手続きの仮引数)を表しています.つまり,この束縛指定は,ローカルな n を check-ab が受け取った正整数で初期設定しています.

他の言語,特にC言語から来た人は,スコープをほとんど意識することはないと思われるので,Schemeのようにスコープが入れ子になる言語には戸惑いを感じると思います. 筆者はそうでした. 上のような束縛指定を初めて見たとき,C言語風に「n = n って何だ?」と思ったものです.

具体例

これまで説明してきた構文を使って,ちょっとした手続きを定義してみます. 以下の groups は,リスト lst と正整数 n を受け取って,lst の要素を先頭から n 個ずつのグループ(リスト)に分けたリストを返します. ただし,最後のグループは残りものからなるリストになります. 例えば,lst $=$ '(1 2 3 4 5) と n $=$ 2 に対して groups を適用すると '((1 2) (3 4) (5)) といったリストが返ってきます. なお,n は正整数であることを仮定して,n に0以下の整数を指定すると無限ループしてしまいます.

下記の split-at は,リスト lst を先頭から n 個のグループ(リスト)と残りのリストに分割したものを多値として返します.実のところ,(srfi srfi-1) モジュールに同様の手続きがあるのですが,n が lst の長さより大きいときにエラーが発生するので独自に定義しています(それに letrec の使用例になるので).
;; groups.scm
(use-modules (srfi srfi-11))
(define (groups lst n)
  (letrec ((split-at 
            (lambda (lst n acc)
              (if (or (null? lst) (<= n 0))
                  (values (reverse! acc) lst)
                  (split-at (cdr lst) (1- n) (cons (car lst) acc))))))
    (let grouping ((lst lst) (acc '()))
      (let-values (((grp rest) (split-at lst n '())))
        (if (null? rest)
            (reverse! (cons grp acc))
            (grouping rest (cons grp acc)))))))
guile> (load "groups.scm")
      ...... コンパイルメッセージ ......
guile> (groups '(1 2 3 4 5 6 7 8 9 10) 3)
$1 = ((1 2 3) (4 5 6) (7 8 9) (10))

ちょっとお遊びです.split-at は名前付き let 式を使えば非再帰的な手続きにすることができます.一般に,相互再帰的でさえなければ,名前付き let 式を使えばどんな手続きでも非再帰的に定義できます.正確に言えば,名前付き let 式に再帰性を押し付けることができます.非再帰的な手続きは,単なる lambda 式(にたまたま名前が付いているだけ)に過ぎないので,grouping の引数にすることができます.ということで,groups を次のように定義することもできます.

;; groups.scm
(use-modules (srfi srfi-11))
(define (groups lst n)
  (let grouping ((split-at 
                  (lambda (lst n)
                    (let loop ((lst lst) (n n) (acc '()))
                      (if (or (null? lst) (<= n 0))
                          (values (reverse! acc) lst)
                          (loop (cdr lst) (1- n) (cons (car lst) acc))))))
                 (lst lst)
                 (acc '()))
    (let-values (((grp rest) (split-at lst n)))
      (if (null? rest)
          (reverse! (cons grp acc))
          (grouping split-at rest (cons grp acc))))))
でも,こんなプログラムを作ると怒られるような気がします.

その他

let 式に関連するもの(ローカル変数を新たに束縛する式)として, 以下に示すようなものがあります(他にもあるかも知れません).

let-optional や let-keyword など

これらは lambda* 式の let 版(ローカル変数版)です. 詳細は Guile[6.7.4.2 (ice-9 optargs)] を参照して下さい. ただ,Guileのライブラリモジュール内を grep コマンドを使って検索してみると, lambda* 式はあちらこちらで使われていますが, これらはまったく使われていないようです. あまり使うことはないように思います.

and-let*

これは and と let* を結合したものだそうです. 詳細は Guile[7.5.4 SRFI-2 - and-let*] を参照して下さい. マニュアルを読むと,役立ちそうな感じがするのですが, Guileのライブラリモジュール内ではほとんど使われていません.

match-let,match-let*,match-letrec

これ(パターンマッチング関連)は独立した話題です.いつか勉強したいと思います. 詳細は Guile[7.8 Pattern Matching] を参照して下さい.
(おしまい)