29

When I first learned Haskell, I very quickly came to love parametric polymorphism. It's a delightfully simple idea that works astonishingly well. The whole "if it compiles it usually works right" thing is mostly due to parametric polymorphism, IMHO.

But the other day, something occurred to me. I can write foo as a polymorphic function. But when bar calls foo, it will do so with a specific set of argument types. Or, if bar itself is polymorphic, then its caller will assign definite types. By induction, it seems that if you were to take any valid Haskell program and analyse the entire codebase, you can statically determine the type of every single thing in the entire program.

This, in a sense, is a bit like C++ templates. There is no run-time polymorphism, only compile-time polymorphism. A Haskell compiler could choose to generate separate machine code for every type at which each polymorphic function is called. Most Haskell compilers don't, but you could implement one if you wanted to.

Only if you start adding Haskell extensions (ExistentialQuantification is the obvious one) do you start to get real run-time polymorphism, where you have values who's type cannot be statically computed.

Oh, yeah, my question?

  1. Are the statements above actually correct?

  2. Is there a widely-used name for this property?

4

3 に答える 3

31

Haskell(拡張機能なし)は多形再帰を許可し、この機能だけではプログラムを完全に単形に静的に特殊化することは不可能です。これは、Nの深いネストされたリストを出力するプログラムです。ここで、Nはコマンドラインパラメーターです。

import System

foo :: Show a => Int -> a -> IO ()
foo 0 x = print x
foo n x = foo (n-1) [x]

main = do [num_lists] <- getArgs
          foo (read num_lists) 0

への最初の呼び出しfooaは、タイプはIntです。次の再帰呼び出しでは、タイプ[Int]が、次に[[Int]]、などになります。

多形再帰が禁止されている場合は、プログラムを静的に特殊化することは可能だと思います。

于 2012-05-10T14:00:54.470 に答える
14

うん、私もこれについて考えました。基本的には、Haskell 98を実装できるように見えますが、ボクシングによるポリモーフィズムの代わりにマルチインスタンス化によるポリモーフィズムを使用して、言語拡張機能の一部を実装することはできないようです。

いくつかのHaskell機能をC++ライブラリとして実装しようとすると、これについての洞察を得ることができます(ご存知のように、C ++はポリモーフィズムによるマルチインスタレーションを実行します)。あなたが見つけたのは、ポリモーフィック関数への参照を含むポリモーフィック値を持つことが不可能であることを除いて、Haskellができることはすべてできるということです。

これがどのように見えるかは、

template<typename T>
void f(T);           // f :: a -> IO ()

特定のインスタンス化のアドレスを取得して、実行時に関数ポインターとして渡すことができます。

&f<int>

ただし、テンプレートのアドレスを取得することはできません(&f)。これは理にかなっています。テンプレートは純粋にコンパイル時の構成です。また、マルチインスタンス化によってポリモーフィズムを実行している場合、特定のインスタンス化へのポインターを持つことはできますが、マシンコードレベルではポリモーフィズム関数自体へのポインターを持つことはできません。

では、Haskellはどこで多形値を使用するのでしょうか?一見すると、「明示的なforallを記述しなければならない場所ならどこでも」という経験則が適切であるように思われます。したがってPolymorphicComponents、、、、およびRank2Typesは明らかなノーノーです。これをC++に変換することはできません。RankNTypesImpredicativeTypes

data MkList = MkList (forall a. a -> [a])
singleton = MkList (\x -> [x])

一方、ExistentialQuantification少なくともいくつかの場合に実行可能です。これは、テンプレートコンストラクター(または、より一般的には、コンストラクターがクラス自体よりも多くのものにテンプレート化されているクラス)を持つ非テンプレートクラスを持つことを意味します。

Haskellの場合:

data SomeShow = forall a. Show a => SomeShow a
instance Show SomeShow where show (SomeShow a) = show a

これは、C++で次のように実装できます。

// a function which takes a void*, casts it to the given type, and
// calls the appropriate show() function (statically selected based
// on overload resolution rules)
template<typename T>
String showVoid(void *x)
{
    show(*(T*)x);
}

class SomeShow
{
    private:
        void *m_data;
        String (*m_show)(void*); // m_show :: Any -> String

    public:
        template<typename T>
        SomeShow(T x)
            : m_data(new T(x)) // memory management issues here, but that's orthogonal
            , m_show(&showVoid<T>)
        {
        }

        String show()
        {
            // alternately we could declare the top-level show() as a friend and
            // put this there
            return m_show(m_data);
        }
};

// C++ doesn't have type classes per se, but it has overloading, which means
// that interfaces are implicit: where in Haskell you would write a class and
// instances, in C++ you just write a function with the same name for each type
String show(SomeShow x)
{
    return x.show();
}

どちらの言語でも、ポリモーフィックコンストラクターを持つ非ポリモーフィック型があります。

実装できる言語拡張機能と実装できない言語拡張機能があることを示しましたが、コインの裏側についてはどうでしょうか。Haskell98に実装できないものはありますか?forallを書くためにも言語拡張()が必要であるという事実から判断するExplicitForAllと、答えはノーだと思うでしょう。そして、あなたはほとんど正しいでしょうが、2つのしわがあります:型クラスと多形再帰です。型クラスは通常、ディクショナリの受け渡しを使用して実装されます。各インスタンス宣言により、関数のレコードが生成され、必要な場所に暗黙的に渡されます。

たとえば、モナドの場合、次のようになります。

data MonadDict m = MonadDict { 
    return :: forall a. a -> m a,
    (>>=)  :: forall a b. m a -> (a -> m b) -> m b
}

さて、あなたはそれらのすべてを見てください!それらを明示的に書くことはできませんが、辞書を渡す実装では、Haskell 98でも、ポリモーフィックメソッドを持つクラスはポリモーフィック関数を含むレコードになります。マルチインスタンスを使用してすべてを実装しようとしている場合、これは明らかに問題になります。Haskell 98に固執する場合、インスタンスはほとんどの場合グローバルで静的に認識されているため、辞書を渡すことなくほとんど逃げることができます。各インスタンスはいくつかのポリモーフィック関数を生成しますが、どちらを呼び出すかはコンパイル時にほとんど常にわかっているため、実行時にそれらへの参照を渡す必要はほとんどありません(これはできないので良いことです)。トレードオフは、プログラム全体のコンパイルを行う必要があることです。そうしないと、インスタンスが静的に認識されなくなるためです。それらは別のモジュールにある可能性があります。また、例外は多態的な再帰です。これには、実行時に辞書を作成する必要があります。を参照してください詳細については、他の回答をご覧ください。多形再帰は、型クラスがなくてもマルチインスタンス化アプローチを無効にします。sに関するコメントを参照してくださいBTree。(またExistentialQuantification、ポリモーフィックメソッドへのポインターの格納を再度開始する必要があるため、ポリモーフィックメソッドを使用する* plus *クラスは実行できなくなりました。)

于 2012-05-10T16:03:25.197 に答える
8

上記で説明したように、プログラム全体のコンパイラは、型情報へのグローバルアクセスを利用して、非常に積極的な最適化を行います。例としては、JHCMLtonがあります。同様の理由で、インライン化されたGHCも部分的に「プログラム全体」です。グローバル情報を利用する他の手法には、スーパーコンパイルが含まれます。

使用されているすべてのタイプでポリモーフィック関数を特殊化することで、コードサイズを大幅に増やすことができることに注意してください。これには、コードを通常の値に戻すために大量のインライン化が必要です。これを管理することは挑戦です。

于 2012-05-10T13:28:32.057 に答える