web-dev-qa-db-ja.com

上位の種類はいつ便利ですか?

私はしばらくの間F#で開発を行ってきましたが、気に入っています。ただし、F#には存在しないと知っている流行語の1つは、より種類の高いタイプです。私は、より種類の高い種類の資料を読みましたが、その定義を理解していると思います。なぜ有用なのか、私にはわかりません。 ScalaまたはHaskellで、F#で回避策が必要な、より高い種類の型の例を簡単に提供できますか?また、これらの例では、より高い種類の型がない場合の回避策は何でしょうか(またはF#でその逆ですか?)私はたぶんそれを回避することに慣れているので、その機能がないことに気付かないでしょう。

(私は思う)代わりにmyList |> List.map fまたはmyList |> Seq.map f |> Seq.toListより高い種類の型を使用すると、単純にmyList |> map fそしてListを返します。それは素晴らしい(それが正しいと仮定して)が、ちょっとささいなように見える? (そして、単に関数のオーバーロードを許可するだけではできませんか?)私は通常Seqに変換しますが、その後、必要なものに変換できます。繰り返しますが、多分私はそれを回避することにあまりにも慣れています。しかし、より高度な型reallyがキーストロークまたは型安全のいずれかであなたを救う例はありますか?

83
lobsterism

HaskellのFunctor型クラスを考えてみましょう。ここで、fはより種類の高い型変数です。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

このタイプシグネチャは、fmapがfのタイプパラメーターをaからbに変更しますが、fをそのまま残すことを示しています。したがって、リストでfmapを使用するとリストが得られ、パーサーでそれを使用するとパーサーが得られます。そして、これらはstatic、コンパイル時の保証です。

私はF#を知りませんが、Functor抽象化をJavaまたはC#のような言語で表現し、継承とジェネリックを使用して、それ以上ではない場合に何が起こるかを考えてみましょう。 -kinded generics。最初の試行:

interface Functor<A> {
    Functor<B> map(Function<A, B> f);
}

この最初の試みの問題は、インターフェイスの実装がFunctorを実装するクラスanyを返すことが許可されることです。誰かがFunnyList<A> implements Functor<A>を書き、そのmapメソッドが異なる種類のコレクション、またはコレクションではないがFunctorのままである何かを返すことができます。また、mapメソッドを使用する場合、実際に期待している型にダウンキャストしない限り、結果に対してサブタイプ固有のメソッドを呼び出すことはできません。したがって、2つの問題があります。

  1. 型システムでは、mapメソッドが常にレシーバと同じFunctorサブクラスを返すという不変式を表現することはできません。
  2. したがって、Functorの結果に対して非mapメソッドを呼び出す静的なタイプセーフな方法はありません。

他にももっと複雑な方法を試すことができますが、どれも実際には機能しません。たとえば、結果の型を制限するFunctorのサブタイプを定義することにより、最初の試行を強化することができます。

interface Collection<A> extends Functor<A> {
    Collection<B> map(Function<A, B> f);
}

interface List<A> extends Collection<A> {
    List<B> map(Function<A, B> f);
}

interface Set<A> extends Collection<A> {
    Set<B> map(Function<A, B> f);
}

interface Parser<A> extends Functor<A> {
    Parser<B> map(Function<A, B> f);
}

// …

これにより、これらのより狭いインターフェースの実装者がFunctorメソッドから間違ったタイプのmapを返すことを禁止できますが、Functor実装の数に制限はありません。 、必要な幅の狭いインターフェイスの数に制限はありません。

EDIT:そして、これはFunctor<B>が結果タイプとして表示されるためにのみ機能し、子インターフェースはそれを狭めることができることに注意してください。したがって、Monad<B>次のインターフェース:

interface Monad<A> {
    <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

Haskellでは、より高いランクの型変数では、これは(>>=) :: Monad m => m a -> (a -> m b) -> m bです。

さらに別の試みは、再帰ジェネリックを使用して、サブタイプの結果タイプをサブタイプ自体に制限するようにインターフェースに試行させることです。おもちゃの例:

/**
 * A semigroup is a type with a binary associative operation.  Law:
 *
 * > x.append(y).append(z) = x.append(y.append(z))
 */
interface Semigroup<T extends Semigroup<T>> {
    T append(T arg);
}

class Foo implements Semigroup<Foo> {
    // Since this implements Semigroup<Foo>, now this method must accept 
    // a Foo argument and return a Foo result. 
    Foo append(Foo arg);
}

class Bar implements Semigroup<Bar> {
    // Any of these is a compilation error:

    Semigroup<Bar> append(Semigroup<Bar> arg);

    Semigroup<Foo> append(Bar arg);

    Semigroup append(Bar arg);

    Foo append(Bar arg);

}

しかし、この種の手法(これは、おおざっぱなOOP開発者、おおざっぱな機能的開発者にとってもややこしい)にとってはややこしい)はまだできません。目的のFunctor制約を次のいずれかで表現します。

interface Functor<FA extends Functor<FA, A>, A> {
    <FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}

ここでの問題は、これはFBFと同じFAを持つことを制限しないため、タイプList<A> implements Functor<List<A>, A>を宣言すると、mapメソッドはstillNotAList<B> implements Functor<NotAList<B>, B>を返すことができます。

Javaでの未加工の型(パラメーター化されていないコンテナー)を使用した最終試行:

interface FunctorStrategy<F> {
    F map(Function f, F arg);
} 

ここで、Fは、ListMapのようなパラメータ化されていない型にインスタンス化されます。これにより、FunctorStrategy<List>Listのみを返すことができますが、リストの要素タイプを追跡するためにタイプ変数の使用を放棄しました。

ここでの問題の核心は、JavaやC#では型パラメーターにパラメーターを持たせないことです。Javaでは、Tが型変数の場合、次のように記述できます。 TおよびList<T>であり、T<String>ではありません。より種類の高い型はこの制限を削除するため、次のようなことができます(完全には考えられません)。

interface Functor<F, A> {
    <B> F<B> map(Function<A, B> f);
}

class List<A> implements Functor<List, A> {

    // Since F := List, F<B> := List<B>
    <B> List<B> map(Function<A, B> f) {
        // ...
    }

}

特にこのビットのアドレス指定:

(私は思う)myList |> List.map fmyList |> Seq.map f |> Seq.toListの代わりに、より高い種類の型を使用すると、単純にmyList |> map fと書くことができ、Listを返します。それは素晴らしい(それが正しいと仮定して)が、ちょっとささいなように見える? (そして、単に関数のオーバーロードを許可するだけではできませんか?)私は通常Seqに変換しますが、その後、必要なものに変換できます。

このようにmap関数の考え方を一般化する多くの言語があります。それは、まるでシーケンスがマッピングであるかのようにモデル化することによってです。この発言はその精神に基づいています。Seqとの間の変換をサポートする型がある場合、Seq.mapを再利用することで、マップ操作を「無料で」取得できます。

ただし、Haskellでは、Functorクラスはそれより一般的です。シーケンスの概念とは関係ありません。 fmapアクション、パーサーコンビネータ、関数など、シーケンスへの適切なマッピングを持たない型に対してIOを実装できます。

instance Functor IO where
    fmap f action =
        do x <- action
           return (f x)

 -- This declaration is just to make things easier to read for non-Haskellers 
newtype Function a b = Function (a -> b)

instance Functor (Function a) where
    fmap f (Function g) = Function (f . g)  -- `.` is function composition

「マッピング」の概念は、実際にはシーケンスに関連付けられていません。ファンクターの法則を理解することが最善です:

(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs

非常に非公式:

  1. 最初の法則では、ID/Noop関数を使用したマッピングは何もしないことと同じです。
  2. 2番目の法則では、2回マッピングすることで生成できる結果は1回マッピングすることでも生成できるとしています。

fmapで型を保持する必要があるのはこのためです。異なる結果の型を生成するmap操作を取得するとすぐに、このような保証を行うのがはるかに難しくなります。

62
Luis Casillas

ここで既にいくつかの優れた回答で情報を繰り返したくありませんが、追加したい重要なポイントがあります。

通常、特定のモナドまたはファンクター(または適用可能なファンクター、矢印、または...)を実装するために、より高い種類の型は必要ありません。しかし、そうすることはほとんどポイントを欠いています。

一般に、ファンクター/モナド/その他の有用性を人々が見ないとき、それらは一度に1つを考えているためだということがわかりました。 Functor/monad/etcオペレーションは、実際にはどのインスタンスにも何も追加しません(bind、fmapなどを呼び出すのではなく、使用したオペレーションを呼び出すことができますimplementバインド、fmapなど)。これらの抽象化が本当に必要なのは、any functor/monad/etcで一般的に機能するコードを使用できるようにすることです。

このような汎用コードが広く使用されているコンテキストでは、これは新しいモナドインスタンスを作成するたびに、タイプがすぐに多数の有用な操作にアクセスできることを意味します既に作成されているそれはどこでもモナド(およびファンクター、...)を見るポイントです。 bindconcatではなくmapを使用してmyFunkyListOperationを実装できるようにするためではありません(それ自体では何も得られません)。 myFunkyParserOperationmyFunkyIOOperationが必要になります。実際にモナドジェネリックであるため、リストの観点で最初に見たコードを再利用できます。

しかし、モナドのようなパラメーター化された型を型安全性で抽象化するには、より種類の高い型が必要です(他の回答でも説明されています)。

27
Ben

Haskellでのより高度な型の多型の最もよく使用される例は、Monadインターフェイスです。 FunctorApplicativeも同じように上位であるため、簡潔にするためにFunctorを示します。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

次に、その定義を調べて、型変数fの使用方法を調べます。 fは値を持つ型を意味できないことがわかります。それらは関数の引数であり、結果であるため、その型シグネチャの値を特定できます。したがって、型変数aおよびbは、値を持つことができる型です。型式f aおよびf bも同様です。ただし、f自体ではありません。 fは、より高い種類の型変数の例です。 *が値を持つことができる型の種類であるとすると、fには種類* -> *が必要です。つまり、以前の調査からabに値が必要であることがわかっているため、値を持つことができる型を取ります。また、f aおよびf bには値が必要であるため、値が必要な型を返します。

これにより、fの定義で使用されるFunctorがより種類の高い型変数になります。

ApplicativeおよびMonadインターフェイスはさらに追加されますが、互換性があります。これは、種類* -> *を持つ型変数でも機能することを意味します。

より高い種類の作業を行うと、抽象化のレベルが追加されます-基本的な型の上に抽象化を作成するだけに制限されません。他のタイプを変更するタイプを抽象化することもできます。

13
Carl