Functor
Haskell の型クラスについて考えてみましょうf
。
class Functor f where
fmap :: (a -> b) -> f a -> f b
これは何型シグネチャの言うことはFMAPがの型パラメータを変更することであるf
からa
にb
、しかし、葉f
、それはあったように。したがってfmap
、リストを介して使用するとリストが取得され、パーサーを介して使用するとパーサーが取得されます。そして、これらは静的なコンパイル時の保証です。
F#Functor
はわかりませんが、JavaやC#などの言語で、継承とジェネリックを使用して抽象化を表現しようとするとどうなるかを考えてみましょう。初挑戦:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
この最初の試みの問題は、インターフェイスの実装が実装するクラスを返すことが許可されていることFunctor
です。だれかが、FunnyList<A> implements Functor<A>
whot 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
ますが、使用できる実装の数に制限がないため、必要となるより狭いインターフェースの数に制限はありません。
(編集:これは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
あなたは型を宣言すると、その-SO List<A> implements Functor<List<A>, A>
、map
方法ができるまだリターンをNotAList<B> implements Functor<NotAList<B>, B>
。
生の型(パラメータ化されていないコンテナ)を使用したJavaでの最後の試み:
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
ここでF
は、List
またはのようなパラメータ化されていない型にインスタンス化されますMap
。これにより、a 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
非常に非公式:
- 第1法則では、identity / noop関数を使用したマッピングは、何もしないことと同じです。
- 2番目の法則は、2回マッピングすることによって生成できる結果は1回マッピングすることによっても生成できると述べています。
これがfmap
、型を保持したい理由です。map
異なる結果型を生成する操作を取得するとすぐに、このような保証を行うことははるかに困難になります。