昨日のネタの続き。
「動的言語はちょこちょこ書いてサクッと実行できるから便利」ってのは、開発環境が軽快だからというところに起因する結果であり、動的言語自体に起因することではないということがわかった。
「大規模な開発には向かないけど、小規模な開発には便利」ってのも疑問があるところ。
静的型チェックがないから大規模な開発には向かないのはそうだとしても、じゃあ静的言語は小規模な開発に向かないのかと言うと、そういうことはないような気がする。
小規模開発なら静的言語でも動的言語でも大差はなく、大規模開発では静的言語に軍配が上がるということだと思う。
「型宣言が要らない」ことをメリットとして聞く場合もあるけれど、それもねぇ…。
単純な型ならともかくとして、ユーザ定義型とか関数型みたいな複雑な型の場合、コンパイラが行う型推論と並行して、同じ推論を脳内でもしなきゃいけない。これを大きな負担と感じるのは、俺が静的言語脳だからか?
さて、何だか動的言語にはいい所がないような論調になってしまったので、ちょっと挽回しておこう。
動的言語の特徴とは何かと言えば、ずばり「動的であること」に尽きる。つまり、ダックタイピングができること。それだけと言ってしまってもいい。
ダックタイピングができると何が嬉しいかというと、抽象化の、ひいてはモデリングの新しい次元が開けるというのがあるだろう。
「サブクラス」と「サブタイプ」を区別するという考え方がある。
唐突ではあるが、C++ の template と C# の Generic を対比してみよう。
それぞれで、「コレクションの要素をすべて足し合わせる」というコードを書いてみる。
C++ではこうだろう。たぶん。
template< typename T, typename Iterator >
T accumlate( Iterator begin, Iterator end )
{
T temp = T();
for( ; begin != end; ++begin )
{
temp += *begin;
}
return temp;
}
#include <iostream>
#include <vector>
#include <string>
int main()
{
int ints[] = { 2, 5, 8 };
int iret = accumlate< int >( ints, ints + 3 );
std::cout << iret << std::endl;
std::vector< std::string > strings;
strings.push_back( "ABC" );
strings.push_back( "DEF" );
strings.push_back( "GHI" );
std::string sret = accumlate< std::string >( strings.begin(), strings.end() );
std::cout << sret << std::endl;
return 0;
}
対して、C# では、もちろんこうだ。
using System;
using System.Collections.Generic;
namespace Hoge
{
class Program
{
static T accumlate< T >( IEnumerable< T > iter )
{
T t = default( T );
foreach( T temp in iter )
{
t += temp;
}
return t;
}
static void Main( string[] args )
{
int[] ints = new int[] { 2, 5, 8 };
int iret = accumlate( ints );
Console.WriteLine( iret );
List< string > strings = new List< string >();
strings.Add( "ABC" );
strings.Add( "DEF" );
strings.Add( "GHI" );
string sret = accumlate( strings );
Console.WriteLine( sret );
}
}
}
…違うね。C# の方はコンパイルエラーだ。
accumlate の型引数 T について何の制約も課していないので、T 型のオブジェクト同士を足すことができるということがコンパイラにはわからない。
何か制約を課そうと思っても、C# では演算子は継承できないから不可能だ。
仮に IAccumlatable などというインターフェイスを定義したところで、int も string もそんなものを実装していないから、やはり使えない。
C# ではサブクラスとサブタイプは同一だ。継承しなければサブタイプが定義できない。
C++ では違う場合がある。上記の例で言えば、int と std::string には何ら継承関係が無いにもかかわらず、同じ += 演算子を持っているから同じように扱える。
この場合、テンプレート引数 T が += を持っていることを要請するが、int と std::string はともに T のサブタイプであると考えることができる。だから統一した方法で扱える。Iterator も同様だ。
C++ は動的言語ではないが、ダックタイピングができるという点で、動的言語と同じ利点を持つと言っていい。
その利点とは、このように、クラスの継承関係にかかわらず、サブタイプの関係に基づいて抽象化したコードを書くことができる、というものだ。
オブジェクト指向の三本柱の一つにポリモーフィズムというのがあるが、クラスの継承関係に基づいたものは特に「アドホックポリモーフィズム」と呼び、対して、サブタイプ関係に基づくものを「パラメトリックポリモーフィズム」と呼んで区別することがある。
動的言語や C++ ではどちらも可能だが、C# ではアドホックポリモーフィズムしか使えない。
とは言うものの、だ。
上記の例で言えば、コレクションの要素が何であるかによって、「足し合わせる」という行為の意味も、足し合わせた結果の意味も変わるわけで、accumlate のようなのは「過ぎた抽象化」であるような気もする。
この印象は、Scheme のコードを読んだりするとより顕著に感じる。
こういったことが可能なのは、ビジネスロジックから切り離された、ユーティリティー関数のような場合だけなのではないだろうか。
C++ の std::for_each のように、標準ライブラリに入っていればいいが、そうでない場合、サブクラス関係にないもの同士を統一的に扱う関数を自分で定義するほどのものではないと思うのだが、どうなのだろうか。
…む? なんだか「C++サイキョー!」という結論に落ち着いてしまった気もするが…まぁいいか。