私はしばらくの間F#で開発を行ってきましたが、気に入っています。ただし、F#には存在しないと知っている流行語の1つは、より種類の高いタイプです。私は、より種類の高い種類の資料を読みましたが、その定義を理解していると思います。なぜ有用なのか、私にはわかりません。 ScalaまたはHaskellで、F#で回避策が必要な、より高い種類の型の例を簡単に提供できますか?また、これらの例では、より高い種類の型がない場合の回避策は何でしょうか(またはF#でその逆ですか?)私はたぶんそれを回避することに慣れているので、その機能がないことに気付かないでしょう。
(私は思う)代わりにmyList |> List.map f
またはmyList |> Seq.map f |> Seq.toList
より高い種類の型を使用すると、単純にmyList |> map f
そしてList
を返します。それは素晴らしい(それが正しいと仮定して)が、ちょっとささいなように見える? (そして、単に関数のオーバーロードを許可するだけではできませんか?)私は通常Seq
に変換しますが、その後、必要なものに変換できます。繰り返しますが、多分私はそれを回避することにあまりにも慣れています。しかし、より高度な型reallyがキーストロークまたは型安全のいずれかであなたを救う例はありますか?
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つの問題があります。
map
メソッドが常にレシーバと同じFunctor
サブクラスを返すという不変式を表現することはできません。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);
}
ここでの問題は、これはFB
がF
と同じ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
は、List
やMap
のようなパラメータ化されていない型にインスタンス化されます。これにより、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 f
やmyList |> 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
非常に非公式:
fmap
で型を保持する必要があるのはこのためです。異なる結果の型を生成するmap
操作を取得するとすぐに、このような保証を行うのがはるかに難しくなります。
ここで既にいくつかの優れた回答で情報を繰り返したくありませんが、追加したい重要なポイントがあります。
通常、特定のモナドまたはファンクター(または適用可能なファンクター、矢印、または...)を実装するために、より高い種類の型は必要ありません。しかし、そうすることはほとんどポイントを欠いています。
一般に、ファンクター/モナド/その他の有用性を人々が見ないとき、それらは一度に1つを考えているためだということがわかりました。 Functor/monad/etcオペレーションは、実際にはどのインスタンスにも何も追加しません(bind、fmapなどを呼び出すのではなく、使用したオペレーションを呼び出すことができますimplementバインド、fmapなど)。これらの抽象化が本当に必要なのは、any functor/monad/etcで一般的に機能するコードを使用できるようにすることです。
このような汎用コードが広く使用されているコンテキストでは、これは新しいモナドインスタンスを作成するたびに、タイプがすぐに多数の有用な操作にアクセスできることを意味します既に作成されている。 それはどこでもモナド(およびファンクター、...)を見るポイントです。 bind
とconcat
ではなくmap
を使用してmyFunkyListOperation
を実装できるようにするためではありません(それ自体では何も得られません)。 myFunkyParserOperation
とmyFunkyIOOperation
が必要になります。実際にモナドジェネリックであるため、リストの観点で最初に見たコードを再利用できます。
しかし、モナドのようなパラメーター化された型を型安全性で抽象化するには、より種類の高い型が必要です(他の回答でも説明されています)。
Haskellでのより高度な型の多型の最もよく使用される例は、Monad
インターフェイスです。 Functor
とApplicative
も同じように上位であるため、簡潔にするためにFunctor
を示します。
class Functor f where
fmap :: (a -> b) -> f a -> f b
次に、その定義を調べて、型変数f
の使用方法を調べます。 f
は値を持つ型を意味できないことがわかります。それらは関数の引数であり、結果であるため、その型シグネチャの値を特定できます。したがって、型変数a
およびb
は、値を持つことができる型です。型式f a
およびf b
も同様です。ただし、f
自体ではありません。 f
は、より高い種類の型変数の例です。 *
が値を持つことができる型の種類であるとすると、f
には種類* -> *
が必要です。つまり、以前の調査からa
とb
に値が必要であることがわかっているため、値を持つことができる型を取ります。また、f a
およびf b
には値が必要であるため、値が必要な型を返します。
これにより、f
の定義で使用されるFunctor
がより種類の高い型変数になります。
Applicative
およびMonad
インターフェイスはさらに追加されますが、互換性があります。これは、種類* -> *
を持つ型変数でも機能することを意味します。
より高い種類の作業を行うと、抽象化のレベルが追加されます-基本的な型の上に抽象化を作成するだけに制限されません。他のタイプを変更するタイプを抽象化することもできます。