純粋な関数型プログラミング言語は、割り当てステートメントなしでどのように管理しますか?


26

有名な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")))

これによりが変更されましbalanceset!。私には、クラスメンバーを変更するクラスメソッドによく似ていますbalance

私が言ったように、私は関数型プログラミング言語に精通していないので、それらについて何か間違ったことを言ったら、気軽に指摘してください。


1
純粋に機能的な言語の学習について:すぐにそれを行うことを必ずしもお勧めしません。Haskellを学習する場合、可変変数なしでプログラムを作成する方法を学習することに加えて、怠iness性とIOを実行するHaskellの方法についても学習する必要があります。それは一度に多くのことかもしれません。可変状態なしでプログラムを書くことを学びたい場合、おそらく最も簡単な方法は、を使用せず、set!または末尾が!。それに満足したら、純粋なFPへの移行がより簡単になるはずです。
sepp2k

回答:


21

割り当てステートメントがない場合、どのようにこれを行うことができますか?バランス変数を変更するには?

何らかの代入演算子なしで変数を変更することはできません。

いわゆる純粋関数型言語が存在することを知っているので、私はそう尋ねます。チューリング完全理論によれば、これも行う必要があります。

そうでもない。言語がチューリング完全である場合、他のチューリング完全言語が計算できるものをすべて計算できることを意味します。他の言語が持つすべての機能を備えている必要があるという意味ではありません。

可変変数を持つすべてのプログラムについて、可変変数を持たない同等のプログラムを記述できる限り、チューリング完全プログラミング言語が変数の値を変更する方法を持たないことは矛盾ではありません(「同等」は同じことを計算すること)。そして実際、すべてのプログラムはそのように書くことができます。

あなたの例について:純粋に関数型の言語では、呼び出されるたびに異なる口座残高を返す関数を書くことはできません。ただし、そのような関数を使用するすべてのプログラムを別の方法で書き換えることはできます。


例を求めたので、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))

mutable-variablesを使用せずに同じプログラムを書く方法を次に示します(質問がそれについてではなかったので、参照透過的な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)

同じ関数は、ユーザー入力にフォールドを使用して再帰を使用せずに書くこともできます(明示的な再帰よりも慣用的です)が、フォールドにまだ慣れているかどうかはわかりませんので、あなたがまだ知らないものを使用しない方法。


私はあなたのポイントを見ることができますが、銀行口座もシミュレートし、これらのこと(引き出しと預金)を行うことができるプログラムにしたいのですが、これを行う簡単な方法はありますか?
グニジューズ

@Gnijuohzそれは常にあなたがまさに解決しようとしている問題に依存します。たとえば、開始残高と引き出しと預金のリストがあり、それらの引き出しと預金の後の残高を知りたい場合、単に預金の合計から引き出しの合計を引いたものを計算し、それを開始残高に追加することができます。だから、コード内であろうとnewBalance = startingBalance + sum(deposits) - sum(withdrawals)
sepp2k

1
@Gnijuohz回答にサンプルプログラムを追加しました。
sepp2k

回答の作成と書き換えに費やした時間と労力に感謝します。:)
グニジューズ

継続を使用することは、スキームでそれを達成するための手段になる可能性があることを追加します(継続に引数を渡すことができる限り)
-dader51

11

オブジェクトのメソッドによく似ているのは確かです。それは本質的にそれがそうであるからです。このlambda関数は、外部変数balanceをスコープにプルするクロージャーです。同じ外部変数を閉じる複数のクロージャーと同じオブジェクト上の複数のメソッドを持つことは、まったく同じことを行うための2つの異なる抽象概念であり、両方のパラダイムを理解すれば、どちらか一方を他方の観点から実装できます。

純粋な関数型言語が状態を処理する方法は、不正行為です。たとえば、Haskellでは、外部ソースから入力を読み取りたい場合(もちろん、非決定的であり、繰り返しても同じ結果が得られるとは限りません)、モナドトリックを使用して「我々は残りの世界全体の状態を表すこの他のふり変数を取得し、直接調べることはできませんが、入力の読み取りは、外の世界の状態を取得し、その正確な状態の決定論的入力を返す純粋な関数です常にレンダリングされ、さらに外の世界の新しい状態が表示されます。」(もちろん、これは簡単な説明です。実際に動作する方法を読むと、脳がひどく壊れてしまいます。)

または、銀行口座の問題の場合、変数に新しい値を割り当てる代わりに、関数の結果として新しい値を返すことができます。その後、呼び出し元は、一般的にデータを再作成することにより、機能的なスタイルでそれを処理する必要があります更新された値を含む新しいバージョンでその値を参照します。(これは、データが適切な種類のツリー構造でセットアップされている場合に聞こえるかもしれないので、それほど大きな操作ではありません。)


私は本当に私は完全に(よく、また、第二部:()あなたの答えの最後の部分を理解することはできません、私たちの答えとはHaskellの例に興味があるが、それについての知識が不足しているためだ
Gnijuohz

3
@Gnijuohz最後の段落では、単にのように定義されb = makeWithdraw(42); b(1); b(2); b(3); print(b(4))ているb = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));場所で、代わりにを行うことができると言ってwithdrawwithdraw(balance, amount) = balance - amountます。
sepp2k

3

「複数代入演算子」は、一般的に言えば副作用があり、関数型言語のいくつかの有用なプロパティ(遅延評価など)と互換性のない言語機能の一例です。

ただし、それは、割り当てが一般に純粋な関数型プログラミングスタイルと互換性がないことを意味するものではなく(たとえば、この説明を参照)、一般的な割り当てのように見えるアクションを許可する構文を構築できないことも意味しませんが、副作用なしで実装されます。ただし、そのような構文を作成し、その中に効率的なプログラムを作成することは、時間がかかり困難です。

あなたの特定の例では、あなたは正しいです-セット!演算子割り当てです。これは副作用のない演算子ではなく、Schemeがプログラミングに対する純粋に機能的なアプローチで中断する場所です。

最終的には、任意の純粋な関数型言語は、純粋に機能的なアプローチのいつかに破るために持ってしようとしている-便利なプログラムの大半はやる副作用を持っています。どこでそれを行うかの決定は、通常、利便性の問題であり、言語設計者は、プログラムと問題の領域に応じて、純粋に機能的なアプローチでどこにブレークするかを決定する際にプログラマに最高の柔軟性を与えようとします。


「最終的には、純粋に関数型の言語はいつか純粋に関数型のアプローチを破る必要があります-大半の有用なプログラムには副作用があります」本当ですが、あなたはIOなどについて話しているのです。可変変数なしで多くの有用なプログラムを作成できます。
sepp2k

1
...そして有用なプログラムの「大多数」とは、「すべて」を意味しますか?I / Oを実行しない「有用」と合理的に呼ばれるプログラムの存在の可能性を想像することさえ困難です。これは、双方向の副作用を必要とする行為です。
メイソンウィーラー

@MasonWheeler SQLプログラムは、そのようなIOを行いません。また、REPLを持つ言語でIOを実行しない関数の束を作成し、REPLから単純に呼び出すことも珍しくありません。これは、ターゲットオーディエンスがREPLを使用できる場合(特にターゲットオーディエンスがあなたである場合)に非常に便利です。
sepp2k

1
@MasonWheeler:単一の単純な反例:概念的にpiのn桁を計算するためにI / Oは必要ありません。それは「唯一の」数学と変数です。必要な入力はnのみで、戻り値はPi(n桁まで)です。
ヨアヒムザウアー

1
@Joachim Sauer最終的には、結果を画面に出力するか、ユーザーに結果を報告します。そして最初は、どこかからいくつかの定数をプログラムにロードする必要があります。あなたが知識をひけらかすようにしたいのであれば、すべての有益なプログラムは、それが暗黙の、常に環境によって、プログラマから隠されている些細な例だとしても、いくつかの点でIOをしなければならない
blueberryfields

3

純粋に機能的な言語では、銀行口座オブジェクトをストリーム変換関数としてプログラムします。このオブジェクトは、アカウント所有者(または誰でも)からの無限のリクエストストリームから潜在的に無限のレスポンスストリームへの関数と見なされます。この関数は、最初のバランスから開始し、入力ストリーム内の各要求を処理して新しいバランスを計算し、その後、残りのストリームを処理するために再帰呼び出しにフィードバックされます。(私は、本書の別の部分で、SICPがストリーム変換器のパラダイムについて説明していることを思い出します。)

このパラダイムのより精巧なバージョンは、ここでStackOverflowで説明されている「関数型リアクティブプログラミング」と呼ばれます

ストリーム変換を行う単純な方法には、いくつかの問題があります。(実際、かなり簡単に)古いリクエストをすべて保持して、スペースを浪費するバグのあるプログラムを作成することは可能です。さらに深刻なのは、現在のリクエストへの応答を将来のリクエストに依存させることです。これらの問題の解決策は現在取り組んでいます。 ニール・クリシュナスワミは彼らの背後にある力です。

免責事項:私は純粋関数型プログラミングの教会に属していません。実際、私はどの教会にも属していません:-)


お寺に属していると思いますか?:-P
グニジューズ

1
自由な思考の神殿。説教者はいません。
Uday Reddy

2

何か有用なことをすることになっているプログラムを100%機能させることはできません。(副作用が不要な場合は、全体を一定のコンパイル時間に短縮できます)withdraw-exampleのように、ほとんどの手順を機能させることができますが、最終的には副作用のある手順が必要になります(ユーザーからの入力、コンソールへの出力)。つまり、ほとんどのコードを機能させることができ、その部分は自動的にテストすることも簡単になります。次に、デバッグを必要とするinput / output / database / ...を実行するためのいくつかの命令型コードを作成しますが、ほとんどのコードをクリーンな状態に維持するのはあまり面倒ではありません。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をプログラムした後、あなたはどんな言語でもより良いプログラマになると信じています:)


プログラムの機能部分と非純粋な機能部分についての広範な例と現実的な説明、およびそれにもかかわらずFPが重要である理由についての+1。
ゼルフィアカルトシュタール

1

割り当ては、状態空間を割り当て前と割り当て後の2つの部分に分割するため、不適切な操作です。これにより、プログラムの実行中に変数がどのように変更されるかを追跡するのが難しくなります。関数型言語では、次のものが割り当てを置き換えています。

  1. 戻り値に直接リンクされた関数パラメーター
  2. 既存のオブジェクトを変更する代わりに、返される異なるオブジェクトを選択します。
  3. 新しい遅延評価された値の作成
  4. メモリ内にある必要があるオブジェクトだけでなく、可能なすべてのオブジェクトをリストする
  5. 副作用なし

これは提起された質問に対処していないようです。純粋な関数型言語で銀行口座オブジェクトをどのようにプログラムしますか?
Uday Reddy

ある銀行口座レコードから別の銀行口座レコードに変換するだけの機能です。重要なのは、このような変換が発生すると、既存のオブジェクトを変更する代わりに新しいオブジェクトが選択されるということです。
tp1

ある銀行口座レコードを別の銀行口座レコードに変換する場合、顧客に古いレコードではなく新しいレコードで次のトランザクションを実行してもらいたいと考えます。顧客の「連絡先」は、現在のレコードを指すように常に更新する必要があります。それが「修正」の基本的な考え方です。銀行口座の「オブジェクト」は銀行口座の記録ではありません。
Uday Reddy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.