Schemeの続編とマクロの紹介

call / ccのことを聞いたことがない場合は、この強力なツールに間違いなく精通する必要があります。 継続(call / cc)についてお話ししましょう。これは、右手に途方もない力を持っているシンプルだが理解しにくいデザインです。 それらを使用して、Pythonの場合と同様のメカニズムでyield / next / for ...を実装します。 別の興味深いSchemeメカニズムであるマクロで内部をラップします。



この記事はプログラマー始めることを目的としています 。 Lispersが新しいことを学ぶことはまずありませんが、見つかったエラーに感謝します。



画像








何と理由



シンプルで美しいアイデアが数百行を書く必要があるとき、すべてのプログラマは定期的に自分のコードの表現力の欠如に遭遇します。 これにより、プログラムテキストの単位あたりの意味の密度がはるかに高くなる、 FPなどのより表現力豊かな新しいツールの作成が常に推進されてきました。



継続(継続) -これは、プログラマが外出先でこのようなツールを作成できるメカニズムの1つです。 call / cc(call-with-current-continuation、継続の概念を反映した構文構成)の歴史はSchemeと密接に関連しています。Schemeは最も人気のある言語ではありませんが、数十年にわたってプログラマーのインスピレーションの源となっています。 したがって、ナレーションはScheme言語で行われ、すべてのコード例はGuileを使用して解釈されるように設計されています(ほとんどゼロの労力でRacket、Chicken、そしておそらくこのLisp方言の他の何百ものインタプリタに到達するでしょう)。



パートI.続き



始まり(続編の本質)



続編はGoToの最大の兄弟であり、 使用できない演算子です 。 継続により、次のことが可能になります。





しかし、なぜ過去に戻るのでしょうか?





実際に言われたことを説明しましょう。



プログラムの一部を想像してください。 関数1は関数2を呼び出し、関数2は他の変数から関数3を呼び出します。 たとえば、関数2を呼び出す前に、状態を保存します( 現在のコンテキストと呼ばれます )。 その後、いつでも、関数のチェーン(func2 (func3 abc))



結果を必要な値、たとえばdで置き換えることにより、このコンテキストに戻ることができます。



すべてがそのように機能することを確認してください。



最初の例



真空で球形の例を作成してみましょう。 関数test-func



定義します。



 ;     (define test-func (if (> 3 5) "true " "false ")) (display test-func) (newline)
      
      





結果(明らか):



 >> false
      
      





ifで条件を計算する前に、コンテキストを保存します。 その方法を見てみましょう:



 (call/cc (lambda (cc) (Some actions)))
      
      





call / ccの出現には、インタープリターが現在のコンテキストを取得し、それを内部で定義されたラムダ関数に渡す必要があります。 ラムダ関数は1つの引数を取ります(以降のプログラムテキストでは、 ccと呼びます-「現在の継続」の略です)。 このコンテキストを保持できます。



 (define *env* #f) (call/cc (lambda (cc) (begin (set! *env* cc) ;   cc,      *env* (Some actions))))
      
      





さあ、魔法をかけましょう。 保存されたコンテキストを取得し、次に進みます。 call / ccコンストラクトをifブロックでラップしたので、コンテキストを呼び出すときに、評価する(> 3 5)



代わりに返される値を渡す必要があります。



これは次のように行われます。



 (*env* Any_value)
      
      





ここで、「任意の値」の代わりに、何らかの値を返すコードまたは値自体を指定できます。



さて、次のように書くと:



 (*env* #t)
      
      





コンテキストが受信されたポイントに戻り、ブロック内の関数(if条件)が#tを返したかのように、すべてがブロック外部のコード(call/cc ...)



を探します。



だから、結果



更新

コメントにより、コード(display (*env* #t))



が混乱を(display (*env* #t))



可能性があることが(display (*env* #t))



になりました。 インタプリタが(*env* #t)



に達するとすぐに、別の状態への変更不可の遷移が行われるため(コメントに詳細があります)、この行の構成(表示..)は何も出力しません。 したがって、次のコードの動作は、 (display (*env* #t))



(*env* #t)



置き換えて変わりません。



 (define test-func (if (call/cc (lambda (cc) (begin (set! *env* cc) (> 3 5)) )) "true " "false ")) (display test-func) (display (*env* #t)) (newline)
      
      





 >> false true true true true true true true true true true true true true true true true true true true true true true true true true true true ...
      
      





すべては私たちが望んでいたように機能しますが、...無限ループですか?! 穏やかに、すべてが理論と一致しています。 この例の仕組みを見てみましょう。











(display ...)によって生成された呼び出しスタック内のどこかにプログラム状態に戻ります。 そこで、結果に影響する(trueが表示される)、正常に終了する(display ...)、呼び出される(* env * #t)、そして新しい方法で、私たちが受け入れられる#tを置き換えます...



ジェネレーターを作成します



call / ccを初めて知った人は、このツールの威力を理解しているように思いますが、それをどのように使用し、何を実装できるかはすぐにはわかりません。 call / ccで実装される古典的なもののリストには、ループ、ループまたは再帰の終了、戻り値を持つループまたは再帰の終了、コルーチン、および協調マルチタスクが含まれます。 したがって、継続により、考えられるあらゆる方法でプログラム実行のフローを変更できます。



この機能を使用して、Scheme言語でPythonのジェネレーターの類似物を実装してみましょう。 その結果、次のように機能する必要があります。



 ;    (define-generator (generator-func arg1 arg2 ...) (... (yield result) ;    ...)) ;   .  my-gen — : (define my-gen (generator-func 10 30 ...)) (display my-gen) ;    (display my-gen) ;    ;   ,   (for item in (generator-func 100 70 ...) (display item))
      
      





おそらくPythonほど簡潔ではありませんが、Schemeはその構文によって依然として制限されています(これは非常に単純で信じられないほど普遍的であることを妨げません)。



最初の空白



yield



関数の実装はほとんど明らかです。 現在のコンテキストを保存して(それから続行するには)、ジェネレーターが呼び出された場所にジャンプし、返された値をこのジェネレーターの代わりにyieldで置き換える必要があります。



 (define (yield value) (call/cc (lambda (cc) (set! *env* cc) ;   (*return* value)))) ;    ,  ,   value
      
      







これを実現するには、1からnまでの数の2乗のジェネレーターの例を作成します。



 (define (create-generator func) (define *env* (lambda () (func yield))) (define *return* #f) (define (yield value) (call/cc (lambda (cc) (set! *env* cc) (*return* value)))) (lambda () (   ))) ;    (define (squares-gen n) (lambda (yield) (let loop ((n (+ n 1)) (kn)) (if (> k 0) (begin (yield (expt (- nk) 2)) (loop n (- k 1)))))))
      
      





ほぼ完了



残っているのは小さいことだけです。変数*return*



何かを書き込む必要があります。 もう一度呼び出されたジェネレーターが値を与えるために、ジェネレーターの一番最初にコンテキストを保存し、その内部を返された値で置き換える必要があります。 これは投稿の最初からこの写真についてです:



画像








私たちはプログラムのある部分にいて、先に進みたいと思っています。 ただし、このためには、ジェネレータから次の値を取得する必要があります(ボックスc)。 ジェネレーターに入り(状態を保存して階段を上る)、ボックスを選択し、「テレポート」バック(保存された状態に戻ります)しますが、ボックスを使用します! 実際、次の行を追加する必要があります。



 (define (create-generator func) (define ...) ... (lambda () (call/cc (lambda (cc) (set! *return* cc) (*env*))))) ;      func,     , ;  (yield smth)   func... !
      
      





結果



すべてをまとめる:



結果
 (define (create-generator func) (define *env* (lambda () (func yield))) (define *return* #f) (define (yield value) (call/cc (lambda (cc) (set! *env* cc) (*return* value)))) (lambda () (call/cc (lambda (cc) (set! *return* cc) (*env*))))) ;    (define (squares-gen n) (lambda (yield) (let loop ((n (+ n 1)) (kn)) (if (> k 0) (begin (yield (expt (- nk) 2)) (loop n (- k 1))))))) (define my-gen (create-generator (squares-gen 10)))
      
      





私たちはチェックします:



 ;  my-gen 7  (let loop ((n 7)) (if (> n 0) (begin (display (my-gen)) (display " ") (loop (- n 1)))))
      
      







 >> 1 4 9 16 25 36 49
      
      





やった! うまくいく!



発言



明らかな間違いに気づいたことを願っています。 結果のジェネレーターを10回以上呼び出すと、無限ループに入ります。 (*env*)



呼び出すことで、ジェネレーター関数のコードでyield



関数に到達しないため、新しい反復を保存しないため、最後の反復の状態に完全に戻ります。 たとえば、次のようにできます。ジェネレータが次の値を生成できない場合、 「Stop Iteration」などのスタブ値を返します。



どうやってやるの? 理解のために自分をテストし、自分で考えてください(コメントを歓迎します) (define (create-generator func) ...)



中にコードを1行だけ追加する必要があり(define (create-generator func) ...)







パートII マクロ



なんで?



必要な動作を達成しました。 しかし、構文はまったく異なります。 ジェネレーター関数を作成するには、ラムダ関数でラップし、多くの余分な体の動きを行う必要があります...幸いなことに、Schemeには強力なマクロメカニズムがあります。 マクロは、いつものように、どこにでも散らばっていると悪です。しかし、一度だけ長い間書くなら、美しい構文糖衣であなたの人生を楽にしませんか?



簡単な説明



マクロは続編よりもはるかによくインターネットでカバーされているので、少しだけ触れてください(2つ目の理由は、予想外の大きな記事サイズです。コミュニティがSchemeのマクロの詳細な説明を必要と考える場合、2つ目の記事を書きます)。





おそらく 、2回以上言われたことを本質的に複製するのは間違っているでしょう 。Habrでも同様です。

Schemeのマクロの基本(「Macros」ページで検索): habrahabr.ru/company/tcsbank/blog/267113



ここでは、重要な役割を果たす微妙な点について検討します。



リストの要素を単純に合計する関数を定義するマクロの例を作成します(このためにはもちろん、マクロは必要ありません。例はいつものように空からですが、理解するために必要です)。



 ;    (sum (list 5 9 1 ...)) (define-syntax sum (syntax-rules () ((_ args-list) (let loop ((res 0) (left-list args-list)) (if (not (null? left-list)) (loop (+ res (car left-list)) (cdr left-list)) res))))) (display (sum (list 3 4 5 1))) (newline)
      
      





 >> 13
      
      





すべてが機能します。



次のようにしてみましょう:



 (define loop (list 3 4 5 1)) (display (sum loop))
      
      





このマクロがどのように混乱するか想像できますか(名前の一致により-ループ、マクロの内部、外部)。 え、今、コンパイルエラーが発生します...実行...



 >> 13
      
      





ほんと? これはどのように機能しますか? 実際、Schemeでは、マクロは見かけほど簡単ではありません。 この場合、マクロは次のように展開されます。



 (let loop-1 ((res 0) (left-list loop)) (if (not (null? left-list)) (loop-1 (+ res (car left-list)) (cdr left-list)) res))
      
      





マクロは内部と外部の名前の競合を検出できるため、 loop



内部変数には異なる名前loop-1



ます。



Syntax-case、with-syntax、datum->構文



残念ながら、このようなマクロインテリジェンスは干渉するだけです。 マクロ内では、確実にyield



使用しますが、これは確実にyield-1



変換されます。



必要に応じてマクロを機能させるために、 syntax-case



パワフルなsyntax-case



ます。



この記事は大きすぎることが判明したため、このツールの詳細な説明は、必要に応じて次の出版物に掲載されます。



構文はsyntax-rules



似ており、違いはラッパー関数とラムダ関数にあり、値を返す方法は(syntax something)



は「何か」から構築された構文構造を返す関数です。



Schemeには、 (syntax ...)



省略形があり(syntax ...)



#'





前の例は次のように書き直されます(そして、以下のコードはすべての意味でsyntax-rules



を使用したコードと同等syntax-rules



):



構文ケースを介した合計
 (define-syntax sum (lambda (x) (syntax-case x () ((_ args-list) #'(let loop ((res 0) (left-list args-list)) (if (not (null? left-list)) (loop (+ res (car left-list)) (cdr left-list)) res))))))
      
      







datum->syntax



を使用して、マクロの外部の可視領域(いわば、通常のSchemeから)のオブジェクトの識別子をマクロの内部スコープにdatum->syntax







たとえば、内部でyield-1



変わるのを防ぐために(syntax ...)



、これを行うことができます:



 (define-syntax sum (lambda (x) (syntax-case x () ((pattern) (syntax-case (datum->syntax x 'yield) () (yield (syntax ... yield ...)))))))
      
      





Schemeは、このコードをより見栄え良くするための構文糖衣を提供しています。



 (define-syntax sum (lambda (x) (syntax-case x () ((pattern) (with-syntax (yield (datum->syntax x 'yield)) (yield (syntax ... yield ...)))))))
      
      





その結果、 syntax-case



を使用して、問題なくyield



を使用できる通常のマクロを本質的に作成しました。すべてが期待どおりに機能します。



最後に、ビジネスでマクロを使用します



探していた構文を思い出してください。



構文
 ;    (define-generator (generator-func arg1 arg2 ...) (... (yield result) ;    ...)) ;   .  my-gen -- : (define my-gen (generator-func 10 30 ...)) (display my-gen) ;    (display my-gen) ;   
      
      







マクロdefine-generatorを作成します。これは、引数arg1、arg2 ...の関数を作成し、ジェネレーターを返します。 コードはすでに書いたものに似ています:



 ;   : (define-syntax define-generator (lambda (x) ;   syntax-case (syntax-case x () ((_ (name . args) body) (with-syntax ((yield (datum->syntax x 'yield))) ;    yield     (syntax (define (name . args) ;   ,     ,    (define *env* (lambda () (body))) ;     ,    (define *return* #f) (define (yield value) (call/cc (lambda (cc) (set! *env* cc) (*return* value)))) (lambda () (call/cc (lambda (cc) (set! *return* cc) (*env*))))))))))) (define-generator (squares-gen n) (let loop ((n (+ n 1)) (kn)) (if (> k 0) (begin (yield (expt (- nk) 2)) (loop n (- k 1)))))) (define my-gen (squares-gen 10)) (let loop ((n 7)) (if (> n 0) (begin (display (my-gen)) (display " ") (loop (- n 1)))))
      
      





 >> 1 4 9 16 25 36 49
      
      





またね! うまくいく!



あとがき



突然あなたが継続とマクロについて少し知っていて、上に書いたものを私があなたに伝えることができたなら、類推によってfor ... in ...



実装を簡単に書くでしょうfor ... in ...



意図的にコードに残されたエラーを忘れないでください。



最後まで読んでくれてありがとう。 現在、Schemeが私たちのお気に入りのビジネスにインスピレーションを与えてくれることを願っています。



All Articles