マルチパラダイム時代におけるデータ構造 第7回 ー 宣言的プログラミングの力を感じよう
いよいよ、本格的なデータ構造であるコンセンスセルオブジェクトを実装します。実装する方法は様々ですが、C#はマルチパラダイム言語なので、宣言型プログラミングで実装することにします。マルチパラダイム言語では、始めに宣言型プログラミングをし、必要に応じて命令型プログラミングをする方法が最適です。その理由は、これらから判明します。
宣言型プログラミングで、コンセンスセルオブジェクトの発生・更新・消滅のアルゴリズムを考えと、アルゴリズムがほとんど同じになります。何故ならば、指定された位置で処理を行うだけだからです。文章ではわかりにくいと思いますので、実際のプログラムを見てください。
//1つの要素とそれ以外の要素への参照を持つデータ構造。
public class ConsCell<T> : IDataFreeManager<T>
{
//指定位置で任意の関数を実行する。
private ConsCell<T> Common(
ConsCell<T> old,
T value,
int index,
Func<ConsCell<T>, T, ConsCell<T>> hitIndexFunc,
int count )
{
if ( index == count ) {
return hitIndexFunc( old, value );
} else {
if ( old == null ) return null;
return new ConsCell<T>(
old.Car,
this.Common(
old.Cdr,
value,
index,
hitIndexFunc,
++count ) );
}
}
}
ずいぶんシンプルで驚いた人もいると思います。宣言型プログラミングに慣れていない人は、アルゴリズムの意味が分かりにくいと思いますので解説します。この共通処理のアルゴリズムは再帰処理構造です。指定された位置がくるまで、共通処理を呼び出しつつ、新しいインスタンスを生成しています。何故ならば、このオブジェクトが不変だからです。オブジェクトが不変の場合、データライフサイクルは、全て新しいオブジェクトを生成することになります。具体的には、CarとCdrプロパティが書き換えできないため、新しく必要があります。不変オブジェクトは、一見制約に見えますが、こうした新しい表現方法を生み出します。
共通処理があればあとは簡単です。その共通処理を呼び出すだけです。Insert、Update、Delteのアルゴリズムはすごく単純です。
public class ConsCel<T> : IDataFreeManager<T>
{
//cdrを初期化する
private ConsCell<T> InitCdr( ConsCell<T> old )
{
return old == null
? null
: new ConsCell<T>(
old.Car,
this.InitCdr( old.Cdr ) );
}
//最後尾へデータを追加する。
public void Insert( T value )
{
this.Insert( value, this.Count );
}
//指定位置へデータを発生させる。
public void Insert( T value, int index )
{
this._cons = this.Common(
this,
value,
index,
( ConsCell<T> obj, T element ) =>
new ConsCell<T>(
element,
this.InitCdr( obj ) ),
0 );
}
//指定位置のデータを更新する。
public void Update( T newValue, int index )
{
this._cons = this.Common(
this,
newValue,
index,
( ConsCell<T> obj, T element ) =>
new ConsCell<T>(
element,
this.InitCdr( obj.Cdr ) ),
0 );
}
//最後尾のデータを消滅する。
public void Delete( )
{
this.Delete( this.Count - 1 );
}
//指定位置のデータを消滅する。
public void Delete( int index )
{
this._cons = this.Common(
this,
default( T ),
index,
( ConsCell<T> obj, T element ) =>
obj.Cdr == null
? null
: new ConsCell<T>(
obj.Cdr.Car,
this.InitCdr(
obj.Cdr.Cdr ) ),
0 );
}
//暗黙的にコンスセルをタプルに変換する。
//読みやすくするために実装
public static implicit operator
Tuple<T, ConsCell<T>>( ConsCell<T> obj )
{
return new Tuple<T, ConsCell<T>>(
obj.Car, obj.Cdr );
}
}
見ての通り複雑な事を考えなくてもOKです。目的だけを考えて実装すればよくなるので、簡単に考えられます。命令型プログラミングで実装する場合は、本質とは関係がない余計な処理を考えなくてはなりませんが、宣言型プログラミングでは本質だけを考えられるようなるので簡潔明瞭になります。あとは要素の個数を数えるだけです。再帰処理で考えれば、カウント処理も簡単です。
//cdrの数を数える。
private int GetCdrCount( ConsCell<T> target, int index )
{
if ( target.Cdr == null ) return index;
else return GetCdrCount( target.Cdr, ++index );
}
//要素の数を返す。
public int Count
{
get
{
return this.GetCdrCount( this, 1 );
}
}
おっと、忘れるところでした、選択も同様に実装します・・・
//最初のデータを選択する。
public T Select( )
{
return this.Car;
}
//指定位置のデータを選択する。
private ConsCell<T> Select(
ConsCell<T> target, int index, int count )
{
if ( index >= this.Count )
throw new IndexOutOfRangeException(
"指定された位置" + index +
"にデータは存在しません。" );
return index == count
? target
: this.Select(
target.Cdr,
index,
++count );
}
内容は指定位置(インデックス)にヒットするまで再帰的に呼び出すだけです。こちらもずいぶん簡単ですね♪今回の記事で、宣言型プログラミングの強力さが伝わったと思います。マルチパラダイム言語を使うのですから、使わない手はありません。次回へ続く。