有名なSICPを読んだとき、著者は第3章でSchemeへの割り当てステートメントを紹介するのに少し消極的であるように見えました。
Schemeは私が知っている最初の関数型プログラミング言語であるため、割り当てなしで実行できる関数型プログラミング言語(Schemeではない)があることに驚いています。
本が提供する例、bank account
の例を使用してみましょう。割り当てステートメントがない場合、これをどのように行うことができますか?balance
変数を変更するには?いわゆる純粋関数型言語がいくつか存在することはわかっているので、チューリング完全理論によれば、これも実行できるはずです。
C、Javaを学びましたPythonと私が書いたすべてのプログラムで割り当てを多く使用しているので、それは本当に目を見張るような経験です。割り当てがこれらの関数で回避される方法を誰かが簡単に説明できることを本当に望みますプログラミング言語およびそれらがこれらの言語に与える影響(ある場合)。
上記の例はここにあります:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
これにより、balance
がset!
によって変更されました。私には、クラスメンバーbalance
を変更するクラスメソッドによく似ています。
私が言ったように、私は関数型プログラミング言語に精通していないので、それらについて何か間違ったことを言ったら、遠慮なく指摘してください。
割り当てステートメントがない場合、これはどのように行うことができますか?バランス変数を変更するには?
なんらかの代入演算子がないと変数を変更できません。
そこにはいわゆる純粋関数型言語がいくつかあることを知っており、チューリング完全理論によれば、これも実行できるはずなので、私はそう尋ねます。
結構です。言語がチューリング完全である場合、それは他のチューリング完全言語が計算できるすべてのものを計算できることを意味します。他の言語が持つすべての機能を備えている必要があるという意味ではありません。
チューリング完全プログラミング言語が変数の値を変更する方法がないことは矛盾ではありません。可変変数を持つすべてのプログラムについて、可変変数を持たない同等のプログラムを作成できます(「同等」とは、同じことを計算します)。そして実際、すべてのプログラムはそのように書くことができます。
あなたの例について:純粋に関数型の言語では、呼び出されるたびに異なる勘定残高を返す関数を作成することはできません。しかし、そのような関数を使用するすべてのプログラムを異なる方法で書き直すことができます。
例を求めたので、(疑似コードで)make-withdraw関数を使用する命令型プログラムを考えてみましょう。このプログラムを使用すると、ユーザーはアカウントから引き出したり、預金したり、アカウントの金額を照会したりできます。
account = make-withdraw(0)
ask for input until the user enters "quit"
if the user entered "withdraw $x"
account(x)
if the user entered "deposit $x"
account(-x)
if the user entered "query"
print("The balance of the account is " + account(0))
可変変数を使用せずに同じプログラムを作成する方法は次のとおりです(参照については透過的ではありませんIO質問はそれに関するものではなかったため):
function IO_loop(balance):
ask for input
if the user entered "withdraw $x"
IO_loop(balance - x)
if the user entered "deposit $x"
IO_loop(balance + x)
if the user entered "query"
print("The balance of the account is " + balance)
IO_loop(balance)
if the user entered "quit"
do nothing
IO_loop(0)
同じ関数は、ユーザー入力に対して折りたたみを使用することにより、再帰を使用せずに作成することもできます(これは、明示的な再帰よりも慣用的です)。それはあなたがまだ知らないものを何も使用しない方法です。
オブジェクトのメソッドのように見えるのはあなたの言うとおりです。それは本質的にそれがそうであるからです。 lambda
関数は、外部変数balance
をそのスコープにプルするクロージャーです。同じ外部変数を閉じる複数のクロージャーを持ち、同じオブジェクトに複数のメソッドを持つことは、まったく同じことを行うための2つの異なる抽象化です。両方のパラダイムを理解していれば、どちらか一方をもう一方の観点から実装できます。
純粋な関数型言語が状態を処理する方法は、不正行為によるものです。たとえば、Haskellで外部ソースから入力を読み取りたい場合(もちろん、これは非決定的であり、繰り返しても同じ結果が2度になるとは限りません)、モナドトリックを使用して「 残りの世界全体の状態を表すこの別のふり変数を取得しました。直接調べることはできませんが、入力の読み取りは、外界の状態を取得して返す純粋な関数ですその正確な状態が常にレンダリングする確定的な入力に加えて、外界の新しい状態。」 (もちろん、これは簡単な説明です。実際に機能する方法を読み上げると、脳が著しく壊れます。)
または、銀行口座の問題の場合は、変数に新しい値を割り当てる代わりに、関数の結果として新しい値を返すことができます。その後、呼び出し元は、一般的にデータを再作成することにより、関数形式で処理する必要があります更新された値を含む新しいバージョンでその値を参照します。 (これは、データが適切な種類のツリー構造で設定されている場合に聞こえるほど大きな操作ではありません。)
「複数代入演算子」は、一般的に言えば副作用があり、関数型言語のいくつかの有用なプロパティ(遅延評価など)と互換性がない言語機能の一例です。
ただし、これは一般的な割り当てが純粋な関数型プログラミングスタイルと互換性がないことを意味するわけではありません(たとえば、 この説明 を参照してください)。一般的に割り当てのように見えますが、副作用なしで実装されています。ただし、この種の構文を作成し、その中で効率的なプログラムを作成することは、時間がかかり困難です。
あなたの具体的な例では、あなたは正しいです-セット!演算子は割り当てです。これはではなく副作用のない演算子であり、Schemeがプログラミングへの純粋に機能的なアプローチで壊れる場所です。
最終的に、純粋な関数型言語は、いつか純粋な関数型のアプローチで中断する必要があります-有用なプログラムの大部分doには副作用があります。それをどこで行うかは通常便宜上の問題であり、言語設計者はプログラマーに、プログラムと問題領域に応じて、純粋に関数型のアプローチでどこを壊すかを決定する際の最高の柔軟性を提供しようとします。
純粋に関数型の言語では、銀行口座オブジェクトをストリーム変換関数としてプログラムします。オブジェクトは、アカウント所有者(または誰でも)からの無限のリクエストストリームから、潜在的に無限のレスポンスストリームへの関数と見なされます。この関数は、初期バランスから開始し、入力ストリームの各リクエストを処理して新しいバランスを計算します。その後、バランスは再帰呼び出しにフィードバックされ、残りのストリームを処理します。 (私は、SICPが本の別の部分でストリームトランスフォーマーのパラダイムについて論じていることを思い出します。)
このパラダイムのより複雑なバージョンは、「関数型リアクティブプログラミング」と呼ばれます ここではStackOverflowで 。
ストリーム変換を行う素朴な方法にはいくつかの問題があります。古い要求をすべて保持し、スペースを浪費するバグのあるプログラムを作成することは(実際にはかなり簡単です)可能です。さらに真剣に、現在の要求への応答を将来の要求に依存させることが可能です。これらの問題の解決策は現在取り組んでいます。 Neel Krishnaswami はその背後にある力です。
免責事項:私は純粋な関数型プログラミングの教会に属していません。実際、私はどの教会にも属していません:-)
それが何か有用なことをすることになっているなら、プログラムを100%機能させることは不可能です。 (副作用が必要ない場合は、全体の思考が一定のコンパイル時間に短縮されている可能性があります)withdraw-exampleのように、ほとんどのプロシージャを機能させることができますが、最終的には副作用(ユーザからの入力、コンソールへの出力)。とはいえ、コードのほとんどを機能的にすることができ、その部分は自動でも簡単にテストできます。次に、デバッグが必要な入力/出力/データベース/ ...を実行するためにいくつかの命令型コードを作成しますが、コードの大部分をクリーンに保つことは、それほど多くの作業にはなりません。 withdraw-exampleを使用します。
(define +no-founds+ "Insufficient funds")
;; functional withdraw
(define (make-withdraw balance amount)
(if (>= balance amount)
(- balance amount)
+no-founds+))
;; functional atm loop
(define (atm balance thunk)
(let* ((amount (thunk balance))
(new-balance (make-withdraw balance amount)))
(if (eqv? new-balance +no-founds+)
(cons +no-founds+ '())
(cons (list 'withdraw amount 'balance new-balance) (atm new-balance thunk)))))
;; functional balance-line -> string
(define (balance->string x)
(if (eqv? x +no-founds+)
(string-append +no-founds+ "\n")
(if (null? x)
"\n"
(let ((first-token (car x)))
(string-append
(cond ((symbol? first-token) (symbol->string first-token))
(else (number->string first-token)))
" "
(balance->string (cdr x)))))))
;; functional thunk to test
(define (input-10 x) 10) ;; define a purly functional input-method
;; since all procedures involved are functional
;; we expect the same result every time.
;; we use this to test atm and make-withdraw
(apply string-append (map balance->string (atm 100 input-10)))
;; no program can be purly functional in any language.
;; From here on there are imperative dirty procedures!
;; A procedure to get input from user is needed.
;; Side effects makes it imperative
(define (user-input balance)
(display "You have $")
(display balance)
(display " founds. How much to withdraw? ")
(read))
;; We need a procedure to print stuff to the console
;; as well. Side effects makes it imperative
(define (pretty-print-result x)
(for-each (lambda (x) (display (balance->string x))) x))
;; use imperative procedure with atm.
(pretty-print-result (atm 100 user-input))
ほとんどすべての言語で同じことを行い、同じ結果を得ることができます(バグは少ない)。ただし、プロシージャ内で一時変数を設定したり、変更したりする必要があるかもしれませんが、プロシージャさえあれば問題ありません。実際に機能します(パラメーターだけで結果が決まります)。少しLISPをプログラミングした後は、どの言語でもより優れたプログラマーになると思います。
割り当ては、状態空間を割り当て前と割り当て後の2つの部分に分割するため、不適切な操作です。これにより、プログラムの実行中に変数がどのように変更されるかを追跡することが困難になります。関数型言語では、次のものが代入に取って代わります。