material
DESCRIPTION
2012/5/19 関数型言語勉強会発表資料( ̄ω ̄*) 最後のページ、「終止」ってなってるのは「終始」の間違いですね、ハイTRANSCRIPT
関数型脳になろう!関数型脳になろう!
どうも!
( ̄ω ̄*)ちゅーんでーす
自己紹介
東京都在住の下っ端プログラマ 7ヶ月ほど前にHaskellと運命の出会いを果たす 誰かに語りたくてしょうがない所にたまたまこの
会を知って何も考えずにノミネート
自己紹介
東京都在住の下っ端プログラマ 7ヶ月ほど前にHaskellと運命の出会いを果たす 誰かに語りたくてしょうがない所にたまたまこの
会を知って何も考えずにノミネート
初心者です!お手柔らかに><
ちゅーんと関数型言語
Haskell大好きです ちょっとだけLisp書きました Scalaもちょっとだけ書きました モナドって可愛いですね 言ってるほどHaskell使えてません でもHaskell大好きです HaskellハァハァHaskellハァハァHaskellハァハァHaskellハァハァHaskellハァ
ハァHaskellハァハァHaskellハァハァクンカクンカ(;´Д`)スーハースハー
と、いうわけで
Java
を書いてきました
皆さんも好きですよね、Java
こんな感じに
こんな感じに
mainメソッドにこんなのを書くと・・・
こんなんが出てきます
ようは ループ構文禁止 局所変数使用禁止 フィールドは全てfinal
if、switch分岐禁止
三項演算子と再帰処理と、コンストラクタのフィールドの初期化だけでBrainF*ckのインタプリタ作った
BrainF*ck
かの有名な難解プログラミング言語
+-><[]., の8つの命令で構成されシンプルながらも完全チューリング
ちなみに、参照透明とかなんとかの関係で入力はここに書きます
何が言いたいかというと
ループなんか無くなって 変数の再代入ができなくなって 手続き的に逐次処理を書かなくたって if/switch文なんか使わなくたって
BrainF*ckBrainF*ckが実装できる=完全チューリングが実装できる=完全チューリング
なのだ!(Java凄い!)
何が言いたいかというと
ループなんか無くなって 変数の再代入ができなくなって 手続き的に逐次処理を書かなくたって if/switch文なんか使わなくたって
BrainF*ckBrainF*ckが実装できる=完全チューリングが実装できる=完全チューリング
なのだ!(Java凄い!)
( ̄ω ̄*)え?スタックオーバーフロー?なんですかそれwww
それにしても・・・
とっても読みにくいです
ファイルもやたら多くなっちゃったし・・・
全体で446行あります(´;ω;`)ブワッ(普通に書けば100行以内)
やっぱり変数への再代入やループ構文は必要ですね!
手続き型最高!!
じゃなくて・・・( ̄ω ̄;)
一生懸命書いたんです!評価点を探しましょう
例えばループ
素直に書くとこんな風に冗長で何をやっているか解らなくなりやすい。
ネスト数を保持するための変数 l が邪魔
例えばループ
ループの開始や終端を一文字づつ探しているのでループ内のステップ数が多くなればなるほど高コスト。
予めプログラムを解析しておけば問題解決?
→どうやって???
実は・・・
最初は同じように1文字づつループの端を検索する方法を考えてみた
でも再帰でそれをやるとスタック領域の消費がハンパねー
もうちょっと効率よく制御する方法は無いものか →BrainF*ckのコードを連結リストで表現してみたらどうだろう
連結リスト
1 2 ×3
car
cdr
nil
BrainF*ckのコードを連結リストに
++[]
+ + ×
-
- ×
こうすれば(わりと)低コストでループを制御できる
×
-
- ×
メモリの値が 0 ならこっち
0 以外ならこっちを実行して戻ってきた返り値を元に
再び実行する
連結リストとS式
連結リストはS式にして出力すれば
デバッグに便利!
++[>++<]
↓
('+' . ('+' . (('>' . ('+' . ('+' . ('<' . ('' . Nil))))) . ('+' . ('+' . Nil)))))
S式は複雑なリストを再帰で簡単に出力できる
再帰と連結リスト
このように、再帰処理と連結リストは相性が良い! というか、再帰的なデータ構造が再帰処理で操
作しやすい。(木構造とか)
という事がわかった(`・ω・´)
もう一つ問題が・・・
メモリとポインタはどうやって表現しよう?
配列の値の書き換えは禁止してるけどポインタの指し示す値を書きかえる度に
配列を作り直すのはナンセンス
メモリも連結リストのペア!
B A ×
C D ×
ここが現在のポインタ
メモリも連結リストのペア!
ポインタを右に移動する場合
B A ×C
D ×
副作用のある処理は禁止なのでcdrを書き換えられないから捨てる
C
新しく作ってつなげる
メモリも連結リストのペア!
ポインタを左に移動する場合(逆の事をする)
C D ×B
A ×捨てる
B
ここまでのまとめ
●ループ/副作用なしでもチューリング完全(スタック上限を無視すればだけど・・・)
●一見難しそうな部分も、連結リストを使って(思ったよりは)簡単に実装できる
●でも、Javaでは二度と同じことやりたくない(´・ω・`)面倒くさいし読みにくいし
ところで
ループ/副作用が無いってどういう事なんだろう
改めてコードを見なおしてみよう(`・ω・´)
このへんとか
このへんに注目
あとコレは最初に決めたルールだけどとっても大事
全体の特徴
基本的にJavaなので全体を通してはオブジェクト指向してるけど
全てのメソッドが必ず何か値を返す必要があり
(あるインスタンスの)メソッドが返却する値は引数によって決定する
言葉遊びは好きですか( ̄ω ̄)?
言い換えてみよう
メソッドが返却する値は引数によって決定する
引数→入力 x
変数 x がありメソッドが返却する値は入力 x によって決定する
返却する値→出力 y
二つの変数 x と y がありメソッドの出力 y は
入力 x によって決定する
X と y の語順を入れ替える
二つの変数 x と y がありメソッドは、入力 x に対して
出力 y の値を決定する
ちょっと補完
二つの変数 x と y がありメソッドは、入力 x に対して
出力 y の値を決定する規則が与えられている
倒置法
二つの変数 x と y があり、入力 x に対して、出力 y の値を決定する規則
が与えられているメソッド
W ikiped ia 「関数(数学)」より
二つの変数 x と y があり、入力 x に対して、出力 y の値を決定する規則
が与えられているとき、
変数 y を 「xを独立変数とすると関数」
或いは簡単に「xの関数関数」という。
まぁ、あの・・・
半分こじつけなので、細かい定義についてツッコミ入れられると
泣いちゃうしか無いんですが。
詳しいことはWikipedia見てください
とにかく
引数によって決まった値を返すメソッド(の返却値)は
数学的な意味数学的な意味での関数と表現できる
つまり
今回、Javaを使って「関数」の組み合わせによってプログラミングをした
とゆー事です(`・ω・´)
関数と言えばラムダ計算
λ ← こんなん出てきました
W ikiped ia先生再登場
ラムダ計算(lambda calculus)は、理論計算機科学や数理論理学における、
関数の定義と実行を抽象化した計算体型である。
ラムダ算法とも言う。
(´・ω・`)?
よーわからん
あの、あれ、
α変換とか、β簡約とかめんどくせー話は置いといて
簡単な例
(λx . 5 + x) 3
こいつが関数
(λx . 5 + x) 3
これが引数
(λx . 5 + x) 3
引数 x に 3 を束縛
(λx . 5 + x) 3
結果
5 + 3 = 8
この作業を簡約簡約といいます
2つの引数を取る関数
(λx . (λy . x + y))は、略記で
(λxy . x + y)とか書けます
高階関数
(λfx . f (x * 2)) (λn . n + 2) 5
高階関数
(λfx . f (x * 2)) (λn . n + 2) 5
高階関数
(λn . n + 2) (5 * 2)
= 10 + 2 = 12
部分適用
(λf . f 2)((λxy . x * y) 5)
= (λf . f 2)(λy . 5 * y)
= (λy . 5 * y) 2 = 10
部分適用
(λf . f 2)((λxy . x * y) 5)
= (λf . f 2)(λy . 5 * y)
= (λy . 5 * y) 2 = 10
ここに注目!
注:カリー化≠部分適用
閑話休題
先ほどのJavaプログラム関数の組み合わせでプログラミングしてるなら
ラムダ式で表現できるんじゃなかろーか
private Container parseProgram(int idx,char odr){return
odr == ';' || odr == ']' ? new Container(idx, new Nil()) :odr == '[' ? packLoop(parseProgram(idx+1, program.charAt(idx+1))) :
packOrder(odr, parseProgram(idx+1, program.charAt(idx+1)));}
private Container packLoop(Container sorce){return
_packLoop(sorce, parseProgram(sorce.getIdx()+1, program.charAt(sorce.getIdx()+1)));}
private Container _packLoop(Container loopin, Container loopout){return new Container(loopout.getIdx(),
new ProgramList(loopin.getState(),loopout.getState()));}
BrainF*ckのプログラムを線形リストに変換する部分のコード片
private Container parseProgram(int idx,char odr){odr == ';' || odr == ']' ? new Container(idx, new Nil()) :odr == '[' ? packLoop(parseProgram(idx+1, program.charAt(idx+1))) :
packOrder(odr, parseProgram(idx+1, program.charAt(idx+1)))}
private Container packLoop(Container sorce){_packLoop(sorce, parseProgram(sorce.getIdx()+1, program.charAt(sorce.getIdx()+1)))
}
private Container _packLoop(Container loopin, Container loopout){new Container(loopout.getIdx(),
new ProgramList(loopin.getState(),loopout.getState()))}
Retun文はかならず最初に来るので省略
どうせ1ステップなのでセミコロンいらないです
private Container parseProgram(int idx,char odr){odr == ';' || odr == ']' ? Container(idx, Nil) :odr == '[' ? packLoop(parseProgram(idx+1, program.charAt(idx+1))) :
packOrder(odr, parseProgram(idx+1, program.charAt(idx+1)))}
private Container packLoop(Container sorce){_packLoop(sorce , parseProgram(sorce.idx+1, program.charAt(sorce.idx+1)))
}
private Container _packLoop(Container loopin, Container loopout){Container(loopout.idx, ProgramList(loopin.state, loopout.state))
}
アクセッサはgetterだけなのでgetHoge()はhogeだけで良いですね
コンストラクタも初期化したインスタンスを取得する関数と見なせるので
newは省略してしまいましょう
PARSEPROGRAM := λ idx odr .odr == ';' || odr == ']' ? Container idx Nil :odr == '[' ? PACKLOOP (PARSEPROGRAM (idx+1) (program.charAt (idx+1)) :
packOrder odr PARSEPROGRAM (idx+1) (program.charAt (idx+1))
PACKLOOP := λ sorce ._PACKLOOP sorce (PARSEPROGRAM (sorce.idx+1) (program.charAt (sorce.idx+1) )
_PACKLOOP := λloopin loopout .Container loopout.idx (ProgramList loopin.state loopout.state)
ラムダ式っぽく体裁
詳しい説明はしませんが推論が可能なので型も省略します
PARSEPROGRAM := λ idx odr .odr == ';' || odr == ']' ? Container idx Nil :odr == '[' ? PACKLOOP (PARSEPROGRAM (idx+1) (program.charAt (idx+1)) :
packOrder odr (PARSEPROGRAM (idx+1) (program.charAt (idx+1)))
PACKLOOP := λ sorce ._PACKLOOP sorce (PARSEPROGRAM (sorce.idx+1) (program.charAt (sorce.idx+1) )
_PACKLOOP := λloopin loopout .Container loopout.idx (ProgramList loopin.state loopout.state)
副作用が無いのでこれらのラムダ式は展開しても等価です
PARSEPROGRAM := λ idx odr . odr == ';' || odr == ']' ? Container idx Nil : odr == '[' ? (λ sorce . (λloopin loopout . Container loopout.idx (ProgramList loopin.state loopout.state)) sorce (PARSEPROGRAM (sorce.idx+1) (program.charAt (sorce.idx+1))) (PARSEPROGRAM (idx+1) (program.charAt (idx+1)) : packOrder odr (PARSEPROGRAM (idx+1) (program.charAt (idx+1)))
できたはできたけど
これは酷いこれは酷いどこか間違えてそうな気もする
PARSEPROGRAM := λ idx odr . odr == ';' || odr == ']' ? Container idx Nil : odr == '[' ? (λ sorce . (λloopin loopout . Container loopout.idx (ProgramList loopin.state loopout.state)) sorce (PARSEPROGRAM (sorce.idx+1) (program.charAt (sorce.idx+1))) (PARSEPROGRAM (idx+1) (program.charAt (idx+1)) : packOrder odr (PARSEPROGRAM (idx+1) (program.charAt (idx+1)))
ちなみに、自分自身をこうやって再帰呼び出しするのは
正式なラムダ計算としてはルール違反正式なラムダ計算としてはルール違反です(´・ω・`)
誰かがYコンビネータの話をしてくれるに違いないその他色々と説明のために無視してる事あるです
と・・・とにかく!
関数的に書いたプログラムは
ラムダ式で表現できる ラムダ式で表現できる というイメージが伝わればOKです
(´・ω・`)わざわざ宣言的に書かれているものを
一行にまとめれば読めなくなるのは当たり前
結局何が言いたかったかと言うと
関数型プログラミングの基本的なテクニック 高階閑数 カリー化 部分適用
これらは全て「ラムダ計算」の上に成り立っているという事。
(´・ω・`)特定の言語に拘らずに関数型の考え方そのものを勉強しようという事だったので、Javaを採用してみたらひどい目にあいました
結局何が言いたかったかと言うと
関数型プログラミングの基本的なテクニック 高階閑数 カリー化 部分適用
これらは全て「ラムダ計算」の上に成り立っているという事。
(´・ω・`)特定の言語に拘らずに関数型の考え方そのものを勉強しようという事だったので、Javaを採用してみたらひどい目にあいました
【注】発表後読み返してみたんですが
カリー化はちょっと違うかもです(´・ω・`)
でも実際には副作用が無いと辛い事も多いですよね
計算途中の状態を引数として引き回してる
なので・・・
ScalaやLispは、副作用を認めています Haskellにはモナドがあります Haskellにはモナドがあります Haskellにはモナドがあります Haskellにはm(ry
でもやっぱり
プログラムに副作用が無く、式として表現できる事によって得られるものは大きいです
(`・ω・´)関数型言語を学ぶ事によって「純粋なプログラム」を心がけるようになれば、スパゲッティなプログラムから解放される
・・・はず!! 何より、関数型言語は面白い!!何より、関数型言語は面白い!!
なんか、終止Javaの話ばっかりしてた気もしますが多分それは気のせいです
さぁみなさん、一緒に
関数型脳になりましょう関数型脳になりましょう((`・`・ωω・・´)´)
おしまい