見出し画像

関数型プログラミング事始め (38) 状態

関数型プログラミングがはじめての方へ贈る入門の書
前節:イミュータブル(2) 次節:評価(1)
参考書:
・五味 弘「はじめてのLisp関数型プログラミング」技術評論社(2016)
・大山口 通夫、五味 弘「プログラミング言語論」コロナ社(2008)
・五味 弘「関数型プログラミングと数学(ITと数学)」技術評論社(2021)

プログラムで状態を持つと、それは副作用の原因になります。状態を誰かがどこかで勝手に変更されると、状態が変わらないことを仮定しているプログラムではバグの原因になります。この状態はグローバル変数で管理しているのが一般的で、結局、グローバル変数がバグの原因になります。
この状態を副作用なしに扱うには、要素を持つ構成型のデータはコピーか追加型のデータにしてイミュータブルにして引数渡しにし、即値型は単に引数渡しするだけでスタック上にコピーすることになり、これだけで大丈夫です。

3.6 状態の繰り込み

プログラムがグローバル変数やファイル、画面などで状態を持つようになると、その状態が原因でバグになります。状態を誰かがどこかで状態を勝手に変えると、状態が変わらないと仮定していたプログラムではバグとなります。

この場合は勝手に状態を変えたプログラムか、状態が変わらないと仮定していたプログラムのどちらかがバグの犯人です。いわゆるインタフェースミスマッチというバグになります。このインタフェースミスマッチの原因になる舞台がグローバル変数が持っている状態です。

そこで状態をなんらかの方法でプログラムの関数の一部に繰り込んで、状態の影響をその関数に閉じ込める必要があります。この方法としては以下のようなものがあります。
(1) 要素を持つ構成型のデータはコピーか追加型にしてイミュータブルデータにして、それを引数渡しにする
(2) 即値型のデータはそのまま引数渡しにする

ここではOK! ISLispを使ってこの状態の繰り込みの例を見ていきます。まずはislispを起動してください。なおISLisp処理系はLisp処理系の導入で紹介していますので参照してください。

> ISLisp Version 0.80 (1999/02/25)
>
ISLisp>

(1) 豚の貯金箱

ここでは「豚の貯金箱」のプログラムを見ていきます。
この貯金箱の機能としては、deposit は貯金箱にお金を入れて、現在の貯金額を返す関数であり、withdrawは貯金箱からお金を取り出した金額を返し、また貯金箱の金額を変更する関数とします。

(a) 手続き型プログラミング

それではこの貯金箱を手続き型のプログラミングで作成してみます。

ISLisp>(defun deposit (money) (setf piggy-bank (+ money piggy-bank)) )
DEPOSIT
ISLisp>
(defun withdraw (money)
    (if (<= money piggy-bank)
        (progn
            (setf piggy-bank (- piggy-bank money))
            money )
        0 ))

WITHDRAW

depositやwithdrawではグローバル変数piggy-bankに対して、直接操作する関数です。整数の即値型を持つグローバル変数piggy-bankに対して、操作、つまり副作用をすることで、豚の貯金箱プログラムを作っています。
それではこのプログラムを使ってみます。

ISLisp>(defglobal piggy-bank 0)
PIGGY-BANK
ISLisp>(deposit 600)
600
ISLisp>(withdraw 500)
500
ISLisp>piggy-bank
100

(b) オブジェクト指向プログラミング

次にこの豚の貯金箱のプログラムをオブジェクト指向プログラミングで作ってみましょう。
クラスpiggy-bankはスロット変数moneyでお金を保持していて、そのゲット関数も同名のmoneyにしています。
プログラムの構造は手続き型プログラミングと同等です。つまり手続き型とオブジェクト指向は、プログラムの構造としては同じになります。

ISLisp>(defclass piggy-bank () ((money :accessor money :initform 0)))
PIGGY-BANK
ISLisp>(defgeneric deposit (pbank money))
DEPOSIT
ISLisp>(defgeneric withdraw (pbank money))
WITHDRAW
ISLisp>
(defmethod deposit ((pbank piggy-bank) (money <integer>))
    (setf (money pbank) (+ money (money pbank))) )

DEPOSIT
ISLisp>
(defmethod withdraw ((pbank piggy-bank) (money <integer>))
    (if (<= money (money pbank))
        (progn
            (setf (money pbank) (- (money pbank) money))
        money )
       0 ))

WITHDRAW

このオブジェクト指向プログラムを実行すると以下のようになります。これは手続き型と同じです。
つまりインスタンスオブジェクトのpbankに対して、スロット変数moneyを操作(副作用)することで、プログラムを実行することになります。

ISLisp>(defglobal pbank (create (class piggy-bank)))
PBANK
ISLisp>(deposit pbank 600)
600
ISLisp>(withdraw pbank 500)
500
ISLisp>(money pbank)
100

(c) 関数型プログラミング

ここでいよいよ関数型プログラミングで豚の貯金箱を作ってみます。豚の貯金箱を引数として関数に繰り込むプログラムになります。

ISLisp>(defun deposit (money piggy-bank) (+ money piggy-bank))
DEPOSIT
ISLisp>
(defun withdraw (money piggy-bank)
    (if (<= money piggy-bank)
        (cons money (- piggy-bank money))
        (cons 0 piggy-bank) ))

WITHDRAW

関数depositは貯金箱に入れる金額だけでなく、豚の貯金箱そのものを引数渡しにしています。
関数withdrawは貯金箱から取り出す金額だけでなく、貯金箱も引数渡しにしています。またwithdrawの返す値は取り出した金額と貯金箱のドット対になっています。Common Lispであれば、多値(multiple value)が扱えますが、ISLispにはありませんので、メモリを消費しますがドット対で返すようにしています。

ISLisp>(defglobal pbank (deposit 600 0))
PBANK
ISLisp>pbank
600
ISLisp>(defglobal pbank-values (withdraw 500 pbank))
PBANK-VALUES
ISLisp>pbank-values
(500 . 100)

withdwawで返す値は取り出した金額と貯金箱のドット対になっていますので、それぞれを取り出して、その後のプログラムで使うことになります。
なおpbankやpbank-valueは一時的変数ですので、これは生成後は参照のみに使われる変数なので副作用は生じません(弁解じみていますが、決して言い訳ではありません!)。

(予告) 4章. 関数型プログラミングの評価

今回までで「3章. 関数型プログラミング手習い」は一応の完になります。
次回からは関数型プログラミングの評価として、関数型プログラムの評価と関数型プログラミング自身の評価を見ていくことにします。はたして、関数型プログラミングはいいものなのか?

いいなと思ったら応援しよう!

五味弘
よろしければサポートをお願いします!