最近組み込み系の仕事から離れているせいかあまり聞かなくなった気がするのでもはや流行っていないかもしれないが、一昔前に SystemC というか ESL が結構流行ったときにハードのエンジニアと一緒に SystemC の講習を聞きに行った。
ソフトのエンジニアはどちらかというと利用する側なのであればありがたいぐらいの話だったが、講習が終わった後にハードのエンジニアになんとなく感想を聞いたら、「二重開発(SystemCとRTL)になるからやりたくないなー」という感じであまり乗り気ではないようだった。
そのときはまだペーペーだったので、「ふ~ん、そうですか」ぐらいしか思わなかったが、なにか引っかかるものがあった。その正体に気づいたのが数年前。その頃にはすでに組み込み関係から離れていたので誰にも話すことなく過ぎてしまったが、ちょうどいい場所をみつけたのでここで吐き出させてもらう。
ソフト屋と同じくハード屋も概してコード(ハードの場合はHDL)を書くのが好きだ。仕様検討もそこそこにコーディングに移る人も少なくない。しかしながら、楽しいコーディングもいずれは終わりその後に待ち構えているのは長く苦しい検証作業になる。
自分はハードのエンジニアではないのであまり詳しくはないが、横で見ていたところだと、ハードの検証は単体検証から始まる。単体検証では開発した機能モジュールに対してRTLシミュレーター上で入力のテストセットを入力し、出力データが期待値と一致するかを検証する。
単体検証で問題がなければ結合検証に移行する。ハードの場合は機能モジュール同士が配線により物理的に繋がるので、それに基づいて複数のモジュールを繋げて複数のモジュール全体に対して入力を与えて期待通りの出力を得ることが出来るか検証する。
SoCの開発になってくると、最終的にはシミュレータ上でシステム全体を組み上げてシステムとして想定通りに動くか検証を行って論理設計のフェーズは完了する。
ここでさらっと「期待値と一致する」と書いたが、じゃあどうやって期待値を作るかというと、C なり Perl なりでハードと等価な動作をするツールを作って、その出力と一致するかを見る。期待値不一致が起きたといって調べてみたら期待値生成用のソフトの方がバグってたなんてこともよくある。
システム検証でも CPU だとかメモリだとか外部のUSBデバイスなどを含めた全てをRTLシミュレータで動かすのは骨なので、通常はCPUの代わりにISSを使って、メモリや外部デバイスも仮想的な動作モデルを用意して組み合わせたりして現実的な時間で検証が終わるようにしている。(それでもRTOSのブートに余裕で一晩かかるが)
で、この辺をよくよく見返してみると、期待値生成用のツールとかシステム検証用のISSとか動作モデルって体系化されていないだけで部品部品で見たら ESL と同じじゃん。二重開発はいやだと言いながら、結局検証のフェーズで ESL と同じようなもの作ってるじゃん。ということに気がついた。
じゃあ、ESL をテストセットを生成するものと見なしたときに、開発フローはどうなるんだろうかと考えた。
そうすると、まず最初に作るべきなのはシステム全体の動作モデルになる。これ自体は SystemC でなくとも普通の PC 上のアプリケーションでかまわない。大切なのは、この段階で UI 含めてきちんと動作すること、ソフトならこのまま出荷できるレベルまで作り込みと検証を行い、このときのシステムの入力と出力(例えば UI 上の操作や表示)を記録しておく。
次に、このシステムモデルを幾つかのブロックに分けてもう一段抽象度を落としたモデルを開発する。このあたりから SystemC の出番となる。各ブロックが出来上がったら、全体を組み合わせて検証する。全体に対してシステムモデルと同じ入力を与えて同じ出力が得られれば全体としての動作の検証が出来たことになる。このとき、各ブロックの入出力を記録しておく。
さらに、各ブロックを分割してもう一段抽象度を落としたモデルを開発する。今度は1階層上のブロックの入出力と一致すればよいことになる。
このように、段階的に抽象度を落としながら1階層上の入出力と一致するように作っていって、最終的に RTL のレベルまで落とし込んでいく。
一見、非常にまだるっこしいことをしているように見えるが、結局のところは「抽象機能モデル」→「アンタイムドモデル」→「タイムドモデル」→「RTL」と段階的に落とし込んでいるだけだ。大切なのは、上位階層できちんと検証をして、その入出力を引き継いでいくことで最終的に全体を組み上げたときにシステム全体としての品質が担保できるという点にある。
こうやって淡々と書いていると、自分が不勉強なだけで非常に当たり前のことを書いているような気もしてくる(実際そうかもしれない)。しかしながら、抽象度の高いレイヤーから抽象度の低いレイヤーに入出力を引き継ぐときに、必ずしもそのまま引き継げるわけではなくタイミング等の時間的な曖昧さを厳密にしていく必要がありそうな気がする。
で、これだけ聞くと単に RTL 書いた後に行う検証を先に行うようにしただけで手間暇は変わらないように見えるが、検証の時間のかかり方は大きく変わる。
前にも書いたように RTL でシステム検証を行おうとするとすさまじく時間がかかる。シミュレーターだとまともなアプリの動作はほぼ不可能で、エミュレーターでも大金かけて部分的な動作しか出来ないだろう(最近は技術も進んでいるかもしれないが)。
この方法では RTL のレイヤーで行う検証はこれまでの単体検証か結合検証と同程度のものだ。じゃあ、ここで作った RTL のブロックを組み上げていったときに、もう一度結合検証やシステム検証が必要かというと、接続ミス確認用の疎通テストぐらいはしておいた方がいいかもしれないが、原理的には上位から継承してきた入出力を保証できれば全体組み上げたときに抽象機能モデルと同等の動作をすることが保証できていることになる。
何よりも、先にテストセットを作っておくというのは、ソフト開発で言うところのテストファーストとかテスト駆動開発とかと同じ発想で、その先にあるのはアジャイルだ。
これまでのハードウェア開発は基本的にウォーターフォール型なので、上位の仕様から落とし込んで各ブロックの仕様を決めていくが、上位の仕様が本当に正しいかは組み上げて全体を動かして見ないとわからない。加えて、システム検証は膨大な時間がかかるのでまともなテストは出来ず、結局のところチップが出来上がってから動かして見て、やっぱ想定と違うとなって ES 品の山が積み上がっていくことになる。
このやり方も一見すると抽象度の高いレイヤーから低いレイヤーに降りていくので、ウォーターフォールっぽく見えるが、実際のところ機能要件的なところはスピードが速くて変更が容易な抽象機能モデルで作り込んでしまう。あとは、同じ機能を維持しつつ抽象度を落としていけば等価な動きをする RTL が出来上がるので、チップを作ってからやっぱり違ったということをなくすことが出来るんじゃないかと思うんだがどうなんだろうか。
2015年6月3日水曜日
2015年5月23日土曜日
モナドまとめ
これまで List,State,Maybe の3種類のモナドを見てきたが、結局のところモナドでは表向きは
と単純に左から右に値を流しているように見せかけて(ドットによる通常の結合と逆向きなのは何か深い意味があるんだろうか)実のところは bind によりリストの展開とか、状態の受け渡しとか、条件による処理の変更などを行っている。
こういった関数の間の繋ぎをするのがモナドで、やはり目的としては「どう繋がるか」ではなく「何と何が繋がっているのか」を明確にしたいというのがあるのだろうか。そのために、「どう繋がるか」の部分はモナドにより ">>=" 記号の中に埋め込まれるというのが「文脈に意味を持たせる」ということなのだろう。
本当に状況によっていろんなことが出来るので、一口にモナドはこんなもんだと簡単に説明できないのはよくわかった。
まだ遅延評価や型などもあるが、とりあえずこんなもんで関数型言語についてなんとなくは理解できたかな。
しばらくは Haskeller になるつもりはないが、最近C++やC#なんかでもラムダ式とかに対応し始めたので、うまく使えそうな機会があれば活用してみようと思う。
m >>= f >>= g >>= h
こういった関数の間の繋ぎをするのがモナドで、やはり目的としては「どう繋がるか」ではなく「何と何が繋がっているのか」を明確にしたいというのがあるのだろうか。そのために、「どう繋がるか」の部分はモナドにより ">>=" 記号の中に埋め込まれるというのが「文脈に意味を持たせる」ということなのだろう。
本当に状況によっていろんなことが出来るので、一口にモナドはこんなもんだと簡単に説明できないのはよくわかった。
まだ遅延評価や型などもあるが、とりあえずこんなもんで関数型言語についてなんとなくは理解できたかな。
しばらくは Haskeller になるつもりはないが、最近C++やC#なんかでもラムダ式とかに対応し始めたので、うまく使えそうな機会があれば活用してみようと思う。
2015年5月20日水曜日
Maybe モナド
エラーを扱う
MaybeモナドはStateモナドに比べるとやりたいことはシンプルだ。ある関数fに入力がある範囲なら処理結果を出力させそれを後続の関数gの入力とし、範囲外ならエラーとして処理を打ち切りたいとする。
普通に考えればせっかくタプルがあるのだから、片方に結果を入れて、もう片方に結果が有効かを示す情報を格納すればよい。さすがに関数型でも手続き型でも後続の関数gの中で入力エラーをチェックするのはナンセンスなので、fとgの接合部分でエラーチェックをしてgを呼び出すか処理を終了するか決めるのが自然だ。
そうするのが自然なのだが、Cでドライバのコード書いた経験があるならわかると思うが、この手のエラーを出力するかもしれない関数を並べると、エラーチェックのためのif文が大量に並んで非常にコードが見づらくなる。普通の神経の人ならぶち切れてマクロを定義して無理矢理1行に納めるが、それでも見栄えはよくない。
C++やJavaのように例外が扱えるとこの手のコードはきれいに書けるが、関数型言語では例外は扱わないようだ。本では例外が危険だからと書いてあったが、個人的には前述の「制御を分割して部品化する」といった都合上、例外が扱いにくい(下位にどんな関数が来るかわからないし、制御を分割しているのでどこで例外を待てばいいのか決めにくい)仕様になっているんじゃないかと思っている。
そこで、Maybeモナドの出番となる。
2つのbind関数
ListモナドとStateモナドはそれぞれ"List", "State"のキーワードによりバインド関数が生成されるが、Maybeモナドでは1つのモナドで"Just"と"Nothing"の2つのキーワードを持っている。(逆にMaybeのくせに"Maybe"ではバインド関数は生成されない)下の図のように、Justの場合はバインドされた関数gを取り込んでfの戻り値を渡すbind関数を生成する。Nothingの場合はバインドされた関数は無視してそのままNothingを返すbind関数を生成する。
最終的な出力にはNothingを含むのでこれもMaybeモナドとなり、この後にさらに別の関数をバインドしていってもNothingの場合はひたすらNothingが伝搬していくことになる。
理屈がわかったところでこれまで同様JavaScriptで書いてみる。
var Maybe = {
return: function(a) {
return Maybe.Just(a)
},
Just: function(a) {
return {
bind: function(f) {
return f(a);
},
inner: a
}
},
Nothing: function() {
return {
bind: function() {
return Maybe.Nothing();
},
inner: 'Nothing'
}
}
}
var f = function(a) {
if(a < 0) return Maybe.Nothing();
else return Maybe.Just(a * a);
}
var g = function(a) {
return Maybe.Just(a * 2);
}
document.write(f(1).bind(g).inner+'<br>');
document.write(f(-1).bind(g).inner+'<br>');
fの引数が0以上ならJustにより生成されるbind関数でfの結果がgに渡される。0未満ならNothingにより何もせずにNothingを返すbind関数が生成され、Nothingが継承されていく。
Maybe.JustやMaybe.Nothingはなんか語呂が悪いので、Maybeの外に出すついでにinnerではなくvaluOfを利用するとすっきり書ける。もちろん前回のdo記法もどきもちゃんと使える。
var Maybe = {
return: function(a) {
return Just(a)
}
}
var Just = function(x) {
return {
bind: function(f) {
return f(x);
},
valueOf: function() {
return x;
}
}
}
var Nothing = function(x) {
return {
bind: function() {
return Nothing();
},
valueOf: function() {
return 'Nothing';
}
}
}
var f = function(a) {
if(a < 0) return Nothing();
else return Just(a * a);
}
var g = function(a) {
return Just(a * 2);
}
document.write((function() {
var a, b;
return MonaDo(f(2),
[
function(x_) { a = x_; return g(2); },
function(x_) { b = x_; return Maybe.return(a + b); }
]);
}())+'<br>');
2015年5月18日月曜日
State モナド
何が問題なのか
- 状態だけを入出力する関数を作っても意味が無い
なんとなく状態を入力としてそれに基づいて新しい状態を出力する関数を作って繋げていけば関数型プログラムでも状態の変化を表現できそうだが、関数型プログラムは副作用を及ぼさないため本当に状態だけぐるぐる回ることになりこれでは意味が無い - タプルは直接繋げられない
じゃあ、タプルを使えば状態と入出力データをペアで扱えると思うが、f→[タプル]→gと直接繋げることは出来ずf→(a,s)→gと一旦変数に格納しないといけない - 変数の破壊的代入が出来ない
別にタプルを変数に格納しても問題ないように思えるが、関数型プログラムでは変数の破壊的な代入が出来ないため、f→(a,s)→g→(b, s')→h→(c,s'')→と毎度状態を示す変数sをリネームしていかないといけなくなる
裏で状態の受け渡しを行う
そこで、Stateモナドでは表向きは入出力データの受け渡しのみを行い、その裏でStateモナドが状態の受け渡しを行う。具体的な動作を理解するため、前回と同じく同様の動作をするコードをJavaScriptで書くと以下のようになった。
var State = {
return: function(a) {
return State.State(function(s) {
return Touple(a, s);
});
},
State: function(x) {
return {
bind: function(f) {
return State.State(function(s) {
var tmp = x(s);
return runState(f(tmp.lhs), tmp.rhs);
});
},
inner: x
}
}
}
var runState = function(state, a) {
return state.inner(a);
}
var Touple = function(left, right)
{
return { lhs: left,rhs: right }
}
基本的には前回のListモナドと同じだが、Stateモナドの場合コンストラクタの引数(?)は関数で、これは値aと状態sを引数としてこれから新しい値a'と新しい状態s'のタプルを生成する関数f
f(a,s) -> (a',s')があったとして、これを第一引数aでカリー化した関数f'
f'(s) -> (a',s')を与えて生成する。
例えば、
var f = function(a) {
return State.State(function(s) {
return Touple(a+s, s+1);
});
}
var g = function(a) {
return State.State(function(s) {
return Touple(a*s, s+a);
});
}
var t = runState( f(1).bind(g), 2);
とすれば、
(a=1, s=2) →f()→(a'=a+s=3, s'=s+1=3)→g()→(a''=a'*s'=9,s''=s'+a'=6)
と期待通りの結果が得られる。
また、runStateはbindにより生成されたStateモナド内部の関数を実行して出力と状態を得る関数になる。
作りそのものはListモナドとあまり変わらないのだが、動きは非常に複雑なので頑張って図示すると以下のようになる。
これに関数gをバインドして新しいStateモナドを生成する。このモナドはbind関数の中で生成される関数(仮にinnerとする)を内部関数として持ち、innerはf'とgを内部に持っている。
このモナドに対して状態sを与えてrunStateすると、inner関数が呼び出される。inner関数では、状態sをf'に適用して、a'とs'を得、今度はa'をgに適用する。
gはfと同じくa'でカリー化したg'を持つStateモナドを生成する。
inner関数ではこの生成されたStateモナドに対して先ほどの状態s'を与えてrunStateすると最終的にg'にs'が適用されることになる。
このように、表向きはfとgの間では入出力だけをやりとりしているように見せつつ、StateモナドがrunStateを使って裏で状態の受け渡しを行っている。
今回もわかりやすいようにStateオブジェクトにbindをぶら下げたが、関数オブジェクトにbindをぶら下げればちょっとシンプルにかける。
var State = function(x) {
x.bind = function(f) {
return State(function(s) {
var tmp = x(s);
return runState(f(tmp.lhs), tmp.rhs);
});
}
return x;
}
State.return = function(a) {
return State(function(s) {
return Touple(a, s);
});
}
var runState = function(state, a) {
return state(a);
}
もののついでなので、get/put/gets/modify/push/popも実装してみた。大体ルールがわかってきたので、Haskellのコードをそのまま置き換えれば動いた。
var get = function() {
return State(function(s) {
return Touple(s, s);
});
}
var put = function(s) {
return State(function() {
return Touple(null, s);
});
}
var gets = function(f)
{
return get().bind(Combine(State.return, f));
}
var modify = function(f)
{
return get().bind(Combine(put, f));
}
var Concat = function(val, arr)
{
var newArr = new Array(arr.length+1);
newArr[0] = val;
for(var i = 0; i < arr.length; i++) {
newArr[i+1] = arr[i];
}
return newArr;
}
var Head = function(arr) {
return arr[0];
}
var Tail = function(arr) {
var newArr = new Array(arr.length-1);
for(var i = 0; i < newArr.length; i++) {
newArr[i] = arr[i+1];
}
return newArr;
}
var push = function(a)
{
return modify(function(arr)
{
return Concat(a, arr);
});
}
var pop = function()
{
var value;
return MonaDo(gets(Head),
[
function(x_) { value = x_; return modify(Tail); },
function(x_) { return State.return(value); }
]);
}
var t = runState( pop(), [1,4,5]);
popについては展開するのが面倒なのでさらについでにdo記方も作ってみたが、これもほぼほぼHakellの定義通りで出来る。(letはそのままは無理だがそれなりに書きようはある)
var MonaDo = function(m, fs) {
if(fs.length > 2) {
return m.bind(function(a) {
return MonaDo(fs[0](a), fs.slice(1));
});
} else {
return m.bind(function(a) {
return fs[0](a).bind(fs[1]);
});
}
}
2015年5月16日土曜日
モナドとは
関数を繋ぐ
読んだ本にはモナドとは「文脈を持つ計算を扱う」ための仕組みとあるが、何を言っているのかさっぱりわからない。ネットでもいろいろ勉強した結果、今のところの自分の理解だとモナドは関数を繋げるためのものだと解釈している。(間違っているかもしれないが)
単に関数を繋げるだけならHaskellにはすでに前回のCombineに相当する演算子が用意されている。これを使えば、単純に「
g . f x
」と書けば「
g(f(x))
」と等価な処理に変換してくれる。この場合は、単純に下図のように関数
fの出力をそのまま関数gの入力として利用する。
しかしながら、必ずしも毎回出力と入力が一対一対応するとは限らない。時には間に変換を挟んだりする必要が出てくる。そこでモナドが登場する。
イメージ的には下図のように先行の関数は出力の値を含んだモナドを出力して、モナドは必要により値を変換して後続の関数に渡す。
具体的な動きについて例としてListモナドを利用して見てみる。モナドの例としてはMaybeモナドがよく挙げられているが、Maybeモナドは動きがトリッキーなので個人的にはListモナドの方が入りやすいと思う。
Listモナド
Listモナドでは配列の要素を入力として配列を返す関数を想定している。例えば、値aを入力として、それを[a,a]という2要素の配列に変換する関数fがあったとして、これを前回のmapを利用して配列の各要素に適用すると、
f(1) → [1, 1]
map(f)([1,2,3]) → [[1,1],[2,2][3,3]]
これの出力を別の関数gに同じくmapで適用するためには、入れ子を外して
[1,1,2,2,3,3]
Listモナドはこのmapと変換を一括してfとgの接続部分で行ってくれる。個人的にはこの変換に需要があるかはよくわからないが、Listモナドが用意されていると言うことは関数型言語の世界ではそれなりに需要があるのだろう。
Listモナドの挙動を理解するために、試しにJavaScriptで正確かどうかはわからないがそれっぽい挙動をするコードを書いてみた。
var List = function(array) {
return {
// (>>=) = concatMap に相当
bind: function(f) {
return List(concatMap(f)(array));
},
// 中身の配列を取得
inner: array,
}
}
// return = (:[]) に相当
List.return = function(a) {
return List([a]);
}
var concatMap = function(f) {
return function(array) {
var newArray = [];
for(var i = 0; i < array.length; i++) {
var tmp = f(array[i]);
newArray = newArray.concat(tmp.inner);
}
return newArray;
}
}
これで、f,gを次のように定義して適用すると、期待通り入れ子を外した配列が出来上がる。
var f = function(a) {
return List([a, a+1]);
}
var g = function(a) {
return List([a, a*3]);
}
// f >>= g に相当
document.write( f(1).bind(g).inner + '<br>' );
自分の理解で作ったJavaScriptなのでHaskellと同じことになっているかはわからないが、これからListモナドの動作を見ていくと、まずListモナドでは前の関数の出力aを持ったbind関数を生成する。bind関数では右辺に来る関数を受け取ってこれと持っているaを組み合わせて出力(モナド)を生成する。
絵的に書くと下のようになるだろうか。
Listの場合はモナドを使わずにconcatMapだけで繋げることも出来るが、この場合は
concatMap(g)(concatMap(f)(1))とconcatMapの方が目立ってしまうので、関数型言語ではモナドを使うことで間の変換処理を隠蔽してfとgの関係性を見えやすくしたということだと思う。
ちなみに、うえの説明ではわかりやすさのためにListオブジェクトにbind関数をぶら下げたが、Array.prototypeに追加することでよりシンプルに使えるようになる。
Array.prototype.bind = function(f) { return concatMap(f)(this); } var List = { return: function(a) { return [a]; } } var f = function(a) { return [a, a+1]; } var g = function(a) { return [a, a*3]; } document.write( f(1).bind(g)+'<br>' );
モナドの大まかな挙動がわかったところで、今後はより複雑なMaybeモナドとStateモナドについても理解していく。
2015年5月13日水曜日
関数型言語をかじってみた
プラモのパーツが届かないこともあって、GW中の暇つぶしに「関数プログラミング実践入門」という本を買って関数型言語を勉強してみた。
CやJavaのような手続き型の言語における関数では関数内でstaticやglobal変数等アクセスすることが許されている。この結果として同じ入力を与えても出力が異なることが得る。このようにstaticやglobalな変数にアクセスすることを副作用と呼び関数型言語の関数ではこのような副作用を及ぼすことは許されていない。
このような考え方はオブジェクト指向目指す方向とは逆行しているように思える。オブジェクト指向におけるメソッド(関数)は基本的にはそのメソッドが属するオブジェクトのプロパティを参照もしくは変更するためのものなので、その関数から見て外部の状態を参照/変更するという副作用を及ぼすことを目的として作られているといっても過言ではない。
なぜオブジェクト指向がそうなっているかというと、端的に言えばデータの塊(オブジェクト)とそれに対する働きかけ(メソッド)をペアで取り扱うことで、複雑なデータ構造をよりシンプルな表現で操作できることを目指して作られているからと言える。
このため、手続き型言語で作った関数を単純に入出力を全て引数と戻り値で受け渡しするように変えれば関数型のプログラムになるかというと必ずしもそうではない。これだけでは単に引数と戻り値が大量にあって見づらいだけの手続き型プログラムの関数にしかならない。
お題となっている処理は、頂点座標の移動と回転を組み合わせる処理になる。これをJavascriptで従来通りの手続き型で書くとこのようになる。
これに対して同じくJavascriptで関数型っぽく書くと以下のようになる。
手続き型のプログラムでは「処理Aの結果を処理Bに代入する」、「これを配列の全要素に行う」といった制御をコードとしてべた書きしていたものを、関数型のプログラムではそれぞれを関数(部品)として部品の組み合わせで目的とする制御を表現することが出来るようになっている。
そう言うと手続き型の関数だって部品じゃないのかと思うかもしれないが、手続き型の関数は言ってしまえば制御と処理をごちゃ混ぜにして一塊にしたもの、いわゆるカプセル化であって、関数型のように「ループ」、「処理Aの後に処理Bを行う」といった細かい制御の単位で部品化出来るわけではない。
実際問題上記の手続き型の例を手続き型プログラムの手法で部品化しようとしても、for文全体を一つの関数にするぐらいしかやりようがない。これでは中の処理を変えようとしたら関数の中身を変える必要があるし、for文の部分だけ再利用するなんてことは普通の手続き型プログラムでは出来ない。
しかしながら、もっと大きなメリットとして検証の容易さがある。このように制御を小さくて単純な部品に分割していくことで、個々の部品に対する検証は容易になる。そして、品質が担保された部品を組み合わせていくことで全体として高品質なプログラムを組み上げることが出来る。
こう言うと勘のいい人なら「ちょっとまて」と思うかもしれない。検証や物作りの常識からいって高品質な部品を組み合わせたからといって高品質になるとは限らない。そこで重要になるのが最初に述べた「副作用を及ぼさない」という関数型プログラムの性質になる。
副作用を及ぼす部品を組み合わせるとお互いの副作用が干渉し合って単体では発生しなかった問題が発生する恐れがある。このため、副作用があることを前提とした世界では部品Aと部品Bを組み合わせて新しい部品Cを作ると、部品CはA,B含めた全体としてもう一度検証し直す必要がある。当然、どんどん部品を組み合わせていって大きな部品になってくるとその分検証の規模が増え中身も複雑になる。
一方で、関数型プログラムでは部品(関数)は副作用を及ぼさないため部品Aと部品Bを組み合わせても部品AとBの品質に変化は起きえない。このため、これらを組み合わせて部品Cを作っても、あくまでも部品Cとして新たに作った部分(たとえば部品の組み合わせ方は適切かとか、使う部品は合っているかとか)のみ検証すればよいことになる。
このように関数型プログラムでは、
で、どうやって部品を繋げていくのかという話からモナドについて書こうと思ったが、長くなりそうなのでまた別の記事で
関数型言語とは
いまさら書くまでもないが、関数型言語における関数は数学的な関数と同じで必ず入力により出力が一意に決まるものになっている。CやJavaのような手続き型の言語における関数では関数内でstaticやglobal変数等アクセスすることが許されている。この結果として同じ入力を与えても出力が異なることが得る。このようにstaticやglobalな変数にアクセスすることを副作用と呼び関数型言語の関数ではこのような副作用を及ぼすことは許されていない。
このような考え方はオブジェクト指向目指す方向とは逆行しているように思える。オブジェクト指向におけるメソッド(関数)は基本的にはそのメソッドが属するオブジェクトのプロパティを参照もしくは変更するためのものなので、その関数から見て外部の状態を参照/変更するという副作用を及ぼすことを目的として作られているといっても過言ではない。
なぜオブジェクト指向がそうなっているかというと、端的に言えばデータの塊(オブジェクト)とそれに対する働きかけ(メソッド)をペアで取り扱うことで、複雑なデータ構造をよりシンプルな表現で操作できることを目指して作られているからと言える。
このため、手続き型言語で作った関数を単純に入出力を全て引数と戻り値で受け渡しするように変えれば関数型のプログラムになるかというと必ずしもそうではない。これだけでは単に引数と戻り値が大量にあって見づらいだけの手続き型プログラムの関数にしかならない。
制御を分割する
関数型言語では前述のような「副作用を及ぼさない」という性質が注目されがちだが、個人的には関数型言語で重要な点は制御の部品化にあると感じた。これについて、読んだ本の例をベースに自分なりの解釈も含めて例を書き直してみる。お題となっている処理は、頂点座標の移動と回転を組み合わせる処理になる。これをJavascriptで従来通りの手続き型で書くとこのようになる。
for(var i = 0; i < points.length; i++) {
var p = move(points[i]);
newPoints[i] = rotate(p);
}
moveとrotateはそれぞれ頂点の移動と回転を行う処理だが、細かい説明をしなくても何の変哲も無いどこでも見かけるようなコードだと思う。これに対して同じくJavascriptで関数型っぽく書くと以下のようになる。
newPoints = map(combine(rotate, move))(array);
ここで、mapとcombineは以下のように定義されている。
var map = function(f) {
return function(array) {
var newArray = new Array(array.length);
for(var i = i; i < array.length; i++) {
newArray[i] = f(array[i]);
}
return newArray;
}
}
var combine = function(f, g) {
return function(x) {
var tmp = g(x);
return f(tmp);
}
}
関数を返す関数なので慣れないと直感的にわかりにくいかもしれないが、単にループと関数の結合を行う処理になっている。手続き型のプログラムでは「処理Aの結果を処理Bに代入する」、「これを配列の全要素に行う」といった制御をコードとしてべた書きしていたものを、関数型のプログラムではそれぞれを関数(部品)として部品の組み合わせで目的とする制御を表現することが出来るようになっている。
そう言うと手続き型の関数だって部品じゃないのかと思うかもしれないが、手続き型の関数は言ってしまえば制御と処理をごちゃ混ぜにして一塊にしたもの、いわゆるカプセル化であって、関数型のように「ループ」、「処理Aの後に処理Bを行う」といった細かい制御の単位で部品化出来るわけではない。
実際問題上記の手続き型の例を手続き型プログラムの手法で部品化しようとしても、for文全体を一つの関数にするぐらいしかやりようがない。これでは中の処理を変えようとしたら関数の中身を変える必要があるし、for文の部分だけ再利用するなんてことは普通の手続き型プログラムでは出来ない。
部品化のメリット
このように制御を部品化する一つのメリットとしてはコードの再利用性が向上することがあげられる。おそらく手続き型言語のプログラマなら人生で数千回もfor文を書いていただろうが、これがmapという部品として再利用出来るようになる。しかしながら、もっと大きなメリットとして検証の容易さがある。このように制御を小さくて単純な部品に分割していくことで、個々の部品に対する検証は容易になる。そして、品質が担保された部品を組み合わせていくことで全体として高品質なプログラムを組み上げることが出来る。
こう言うと勘のいい人なら「ちょっとまて」と思うかもしれない。検証や物作りの常識からいって高品質な部品を組み合わせたからといって高品質になるとは限らない。そこで重要になるのが最初に述べた「副作用を及ぼさない」という関数型プログラムの性質になる。
副作用を及ぼす部品を組み合わせるとお互いの副作用が干渉し合って単体では発生しなかった問題が発生する恐れがある。このため、副作用があることを前提とした世界では部品Aと部品Bを組み合わせて新しい部品Cを作ると、部品CはA,B含めた全体としてもう一度検証し直す必要がある。当然、どんどん部品を組み合わせていって大きな部品になってくるとその分検証の規模が増え中身も複雑になる。
一方で、関数型プログラムでは部品(関数)は副作用を及ぼさないため部品Aと部品Bを組み合わせても部品AとBの品質に変化は起きえない。このため、これらを組み合わせて部品Cを作っても、あくまでも部品Cとして新たに作った部分(たとえば部品の組み合わせ方は適切かとか、使う部品は合っているかとか)のみ検証すればよいことになる。
このように関数型プログラムでは、
- 大きなプログラムを小さな部品に分割する
- 小さくて単純な部品にすることで検証を容易にする
- 単純な部品を組み合わせて複雑な処理を実現する
(副作用を及ぼさないため、組み合わせても品質は落ちない)
で、どうやって部品を繋げていくのかという話からモナドについて書こうと思ったが、長くなりそうなのでまた別の記事で
2015年5月11日月曜日
武蔵を作る(完)
発注していた機銃がようやく届いたので仕上げに入る。
天下のAmazon先生でもゴールデンウィーク中は配達に時間がかかったようだ。
キットのパーツの場合は何も考えずに説明書に従って取り付けていけばいいが、自分でパーツ取り寄せてカスタマイズしようと思うと本当にこれでいいのか悩む。
とはいえ、資料調べてなんてやる根性もないのでとりあえずはこれでいいことにして進める。
んでまあ、さくっと完成
他の機銃もこれに換装しようかと思ったが、エアブラシもしまってしまったし今更面倒なのでこれで終了にする。
余った分は今度作るときに有効活用しよう。
せっかくなので手持ちの他の艦と比べてみるとやっぱりでかいね。
重巡の高雄はともかく、同じ戦艦の伊勢(初期型)よりも二回りほど大きい。
ただ、大きいこともあってか甲板上はかなりすっきりしているイメージ。個人的には高雄の艦橋周りのごてごて感が好きだ。
最後に、キットを組んだ感想としては、初心者向けなのか上級者向けなのかよくわからない感じだった。純粋にキットだけを見れば精度は高いしディティールも細かいし、その割には部品点数も少ないので、今回はエッチングパーツ付きなので時間がかかったが、プラキットのみならもっと手軽に早く作れたと思う。
なので最初は有名な艦だし初心者向けのキットなのかなと思ったが、作っていくと塗り分けとか細かいところで説明書の指示に抜けているところが何カ所かあって、これまで幾つか作ってきた人なら経験とカンで乗り切れると思うが、初めて作る人にとっては不親切だと思った。加えて、パーツの番号が通し番号(普通はAの何番、Bの何番とかいう感じにライナー毎に分かれているものが全て通しになっている)になっているうえに番号順に並んでいるわけでもないので非常にパーツが探しにくかったのも厄介だった。
とは言え、出来映えには満足です。
登録:
投稿 (Atom)















