上位の型はいつ役立つのですか?


87

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

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


2
Control.Monadの関数の多くは上位の種類を使用するため、いくつかの例をここで確認することをお勧めします。F#では、具体的なモナドタイプごとに実装を繰り返す必要があります。
Lee

1
@Leeあなただけのインターフェイスを作ることができなかったIMonad<T>し、その後、例えばそれをバックキャストIEnumerable<int>またはIObservable<int>完了したら?これはキャストを避けるためだけですか?
ロブスター主義2014年

4
キャストは安全ではないので、型の安全性に関する質問に答えます。別の問題はreturn、それが実際にはモナド型に属していて、特定のインスタンスに属していないため、どのように機能するかであり、IMonadインターフェイスに配置したくないでしょう。
Lee

4
@リーうん、私はあなたが式の後に最終結果をキャストしなければならないだろうとただ考えていました。しかし、bind別名の各実装内にもキャストする必要があるようですSelectMany。これは誰かがにAPIを使用できることを意味し、それはケースだと、その周りに方法はありません場合はどのええ不潔、それが働くだろうと仮定します。それを回避する方法がないことを100%確信していません。bindIObservableIEnumerable
ロブスター主義2014年

5
すばらしい質問です。この言語機能が有用なIRLであるという説得力のある実用的な例を1つも見たことがない。
JD

回答:


78

したがって、型の種類はその単純型です。例えば、それは基本型であることを意味するInt種類*を持ち、値によってインスタンス化することができます。種類の緩い定義によると(そしてF#がどこに線を引くのかわからないので、それを含めましょう)、ポリモーフィックコンテナーは種類の高い良い例です。

data List a = Cons a (List a) | Nil

型コンストラクタは、List種類がある* -> *:それは具体的な形になるためには、具体的なタイプを渡さなければならないことを意味List Intのような住民を持つことができます[1,2,3]が、Listそれ自体はできませんが。

多態的なコンテナーの利点は明白ですが* -> *、コンテナーだけではなく、より便利な種類のタイプが存在すると仮定します。たとえば、関係

data Rel a = Rel (a -> a -> Bool)

またはパーサー

data Parser a = Parser (String -> [(a, String)])

両方とも親切* -> *です。


ただし、Haskellではさらに高次の型を持つ型を使用することで、これをさらに進めることができます。たとえば、kindで型を探すことができます(* -> *) -> *。これの簡単な例はShape、種類のコンテナをいっぱいにしようとするかもしれません* -> *

data Shape f = Shape (f ())

[(), (), ()] :: Shape List

Traversableたとえば、形状と内容に分割できるため、Haskellでを特徴付けるのに役立ちます。

split :: Traversable t => t a -> (Shape t, [a])

別の例として、ある種類のブランチでパラメーター化されたツリーを考えてみましょう。たとえば、通常のツリーは

data Tree a = Branch (Tree a) a (Tree a) | Leaf

しかし、我々は、分岐の種類が含まれていることがわかりますPairTree aSをので、私たちはパラメトリックタイプのうち、その部分を抽出することができます

data TreeG f a = Branch a (f (TreeG f a)) | Leaf

data Pair a = Pair a a
type Tree a = TreeG Pair a

このTreeG型コンストラクタには種類があり(* -> *) -> * -> *ます。これを使用して、興味深い他のバリエーションを作成できます。RoseTree

type RoseTree a = TreeG [] a

rose :: RoseTree Int
rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]

またはのような病理学的なもの MaybeTree

data Empty a = Empty
type MaybeTree a = TreeG Empty a

nothing :: MaybeTree a
nothing = Leaf

just :: a -> MaybeTree a
just a = Branch a Empty

または TreeTree

type TreeTree a = TreeG Tree a

treetree :: TreeTree Int
treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))

これが現れる別の場所は、「ファンクタの代数」です。抽象化のレイヤーをいくつか落とすと、のように、これを折りたたみと考える方がよいでしょうsum :: [Int] -> Int。代数はファンクターキャリア上でパラメーター化されます。ファンクタは種類あり* -> *、キャリア親切*そう完全に

data Alg f a = Alg (f a -> a)

親切(* -> *) -> * -> *です。Algデータタイプとその上に構築された再帰スキームとの関係のために便利です。

-- | The "single-layer of an expression" functor has kind `(* -> *)`
data ExpF x = Lit Int
            | Add x x
            | Sub x x
            | Mult x x

-- | The fixed point of a functor has kind `(* -> *) -> *`
data Fix f = Fix (f (Fix f))

type Exp = Fix ExpF

exp :: Exp
exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4

fold :: Functor f => Alg f a -> Fix f -> a
fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)

最後に、それらは理論的には可能ですが、さらに種類の高い型コンストラクタは見たことがありません。そのような型の関数が時々見られますが、型mask :: ((forall a. IO a -> IO a) -> IO b) -> IO bの複雑さのレベルを確認するには、型のプロローグまたは依存型の文献を掘り下げる必要があると思います。


3
数分でコードをタイプチェックして編集します。今は電話を使用しています。
J.アブラハムソン2014年

12
@J。アブラハムソン+1良い答えとそれを電話で入力する忍耐力O_o
Daniel Gratzer

3
@lobsterism A TreeTreeは単なる病理ですが、実際には、2種類のツリーが相互に織り込まれていることを意味します。その考えを少し進めると、静的に安全なred /などの非常に強力なタイプセーフな概念が得られます黒い木と端正な静的バランスのとれたFingerTreeタイプ。
J.アブラハムソン2014年

3
@JonHarrop標準の実世界の例は、たとえばmtlスタイルのエフェクトスタックを使用したモナドの抽象化です。ただし、これが現実の世界では貴重であることに同意できない場合があります。HKTがなくても言語が正常に存在できることは一般に明らかだと思います。そのため、他の言語よりも洗練されたある種の抽象化を提供する例があります。
J.アブラハムソン2015

2
たとえば、さまざまなモナドで許可された効果のサブセットを持ち、その仕様を満たすモナドを抽象化できます。たとえば、文字レベルの読み取りと書き込みを可能にする「テレタイプ」をインスタンス化するモナドには、IOとパイプの抽象化の両方が含まれる場合があります。別の例として、さまざまな非同期実装を抽象化できます。HKTなしでは、そのジェネリックピースから構成されるタイプを制限します。
J.アブラハムソン

62

FunctorHaskell の型クラスについて考えてみましょうf

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

これは何型シグネチャの言うことはFMAPがの型パラメータを変更することであるfからab、しかし、葉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つの問題があります。

  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);
}

// …

これは、より狭いインターフェースの実装者Functormapメソッドからの間違ったタイプを返すことを禁止するのに役立ち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 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. 第1法則では、identity / noop関数を使用したマッピングは、何もしないことと同じです。
  2. 2番目の法則は、2回マッピングすることによって生成できる結果は1回マッピングすることによっても生成できると述べています。

これがfmap、型を保持したい理由です。map異なる結果型を生成する操作を取得するとすぐに、このような保証を行うことははるかに困難になります。


だから私はあなたの最後のビットに興味があります、それがすでに操作を持っているときにfmapオンがあるのはなぜ便利ですか?なぜop の定義が理にかなっているのか理解していますが、の代わりに使用する必要がある場所がわかりません。それが役立つ例を挙げていただければ、理解に役立つでしょう。Function a..fmapfmap.
ロブスター主義2014年

1
ああ、わかった。doubleファンクターのfn を作ることができる。そこでdouble [1, 2, 3]は、罪の2倍のfnを与え[2, 4, 6]たりdouble sin与えたりする。ここで配列に取り組んでいるので、その考え方で考え始めると、配列でマップを実行すると、配列ではなく配列が返されることが期待できます。
ロブスター主義2014年

@lobsterism:aを抽象化しFunctorてライブラリのクライアントに選択させることに依存するアルゴリズム/テクニックがあります。J.アブラハムソンの答えは1つの例を示しています。ファンクタを使用すると、再帰的な折り畳みを一般化できます。別の例はフリーモナドです。これらは、クライアントが「命令セット」を任意のとして提供する、一種の汎用インタープリター実装ライブラリーと考えることができますFunctor
Luis Casillas 2014年

3
技術的には正しい答えですが、なぜこれが実際に必要なのか疑問に思います。Haskell Functorやに手を伸ばすことはできませんでしたSemiGroup。実際のプログラムがこの言語機能を最も使用するのはどこですか?
JD

27

ここでは、いくつかの優れた答えの情報を繰り返し説明したくありませんが、追加したい重要な点があります。

通常、特定のモナド、ファンクタ(またはアプリケーションファンクタ、矢印、...)を実装するために、種類の高いタイプは必要ありません。しかし、そうすることはほとんどポイントを逃しています。

一般的に、ファンクター/モナド/何でも有用性が分からないときは、これらのことを一度に1つずつ考えていることが原因であることがよくあります。Functor / monad / etc操作は、実際には1つのインスタンスに何も追加しません(bind、fmapなどを呼び出す代わりに、bind、fmapなどを実装するために使用した操作を呼び出すことができます)。これらの抽象化が本当に必要なのは、一般的に動作するコードを使用できるようにするためですファンクター/モナドなどで。

このような汎用コードが広く使用されている状況では、これは、新しいモナドインスタンスを作成するたびに、そのタイプがすでに作成されている多数の便利な操作にすぐにアクセスできることを意味しますそれが、モナド(およびファンクタなど)をどこでも見られるポイントです。ないので、私が使用できるbindのではなく、concatおよびmap実装するためにmyFunkyListOperation(私自身では何も獲得しない)のではなく、そのため、私は必要に来たときmyFunkyParserOperationmyFunkyIOOperation、それは、一般的なモナド実際にいますので、私はできるリストの面でコードIもともとのこぎりを再利用。

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


9
これは、これまでに読んだ他のどの回答よりも有用な回答に近いですが、それでも、より高い種類が役立つ単一の実用的なアプリケーションを見たいと思います。
JD

「これらの抽象化を本当に望んでいるのは、どんなファンクター/モナドでも一般的に機能するコードを使えるようにするためです。」F#は13年前にモナドを計算式の形式で取得しましたが、元々はseqおよびasyncモナドを使用していました。今日、F#は3番目のモナド、クエリを楽しんでいます。あまり共通点のないモナドがほとんどないのに、なぜそれらを抽象化したいのですか?
JD

@JonHarrop他の人々がHKTをサポートする言語で膨大な数のモナド(およびファンクタ、矢印など。HKTはモナドだけではない)を使用してコードを記述し、それらを抽象化するための使用法を見つけたことを明確に知っています。そして明らかに、あなたはそのコードのどれも実際的な用途があるとは考えていません。5年前にコメントした6歳の投稿についての議論を始めるために戻って、どのような洞察を得たいと思いますか?
ベン

「6歳のポストで議論を始めるために戻って来ることを望んでいる」。回顧。後知恵のおかげで、モナドに対するF#の抽象化はほとんど使用されないままであることがわかりました。したがって、3つ以上の大きく異なるものを抽象化する機能は魅力的です。
JD

@JonHarrop私の回答の要点は、個々のモナド(またはファンクタなど)は、遊牧的なインターフェイスなしで表現された同様の機能よりも実際には有用ではないが、多くの異なるものを統合することです。私はF#に関する専門知識を尊重しますが、3つのモナドしかないと言っている場合は(失敗、ステートフル性、解析など、1つの可能性があるすべての概念にモナディックインターフェイスを実装するのではなく)、はい、これら3つのことを統合しても、あまりメリットが得られないのは当然です。
ベン

15

.NET固有の観点から、これについてしばらく前にブログ投稿しました。それの核心は、あなたが潜在的に同じLINQブロックを再利用することができ、より高kindedタイプで、ある間IEnumerablesおよびIObservables、より高い-kinded種類せずに、これは不可能です。

あなたは(私がブログを投稿した後に考え出し)得ることができる最も近い自分を作ることであるIEnumerable<T>IObservable<T>してからそれらの両方を拡張しましたIMonad<T>。これは、それらが示されている場合、あなたのLINQのブロックを再利用できるようになるIMonad<T>、それはあなたがミックス&マッチすることができますので、しかし、それはもはやタイプセーフませんIObservablesし、IEnumerablesそれは、これを有効にするために魅力的に聞こえるかもしれないが、同じブロック内にあなたがしたいです、基本的には、未定義の動作が発生するだけです。

Haskellがこれを簡単にする方法については、後の投稿で書きました。(実際には何もしない、ブロックを特定の種類のモナドに制限するにはコードが必要です。再利用を有効にすることがデフォルトです)。


2
実用的なことを説明する唯一の回答であることを+1しますが、実IObservables稼働コードで使用したことはないと思います。
JD

5
@JonHarropこれは真実ではないようです。F#ではすべてのイベントはIObservableであり、自分の本のWinFormsの章のイベントを使用します。
Dax Fohl、2016年

1
マイクロソフトはその本を書くように私に支払い、その機能をカバーするように私に要求しました。量産コードでイベントを使用したことを思い出せませんが、見ていきます。
JD、

IQueryableとIEnumerableの間の再利用も可能だと思います
KolA

4年後、私は探し終えました。Rxを生産から外しました。
JD

13

Haskellで最も種類の多い型多型の最もよく使用される例は、Monadインターフェースです。FunctorそしてApplicative、私が表示されますので、同じように高い-kindedされているFunctor何かを簡潔に示すために。

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

次に、その定義を調べて、型変数fがどのように使用されているかを調べます。あなたはそれが表示されますf値を持つタイプを意味することはできません。それらは関数の引数であり、関数の結果であるため、その型シグネチャの値を識別できます。したがって、型変数はab値を持つことができる型です。だから、型の式があるf af b。しかし、fそれ自体ではありません。 fは、種類の高い型変数の例です。それ*が値を持つことができる種類の種類であることを考えると、種類がf必要* -> *です。それは我々がいることを、以前の調査から知っているので、それは、値を持つことができ型を取る、であるab値を持っている必要があります。そして、我々はまた、知っているf aし、f b 値を持つ必要があるため、値を持つ必要がある型を返します。

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

インターフェイスは、より多くの追加、しかし、彼らは互換性です。これは、それらが種類のある型変数にも作用することを意味します。ApplicativeMonad* -> *

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


4
上位の種類が何かについてのもう1つの優れた技術的な説明は、それらが何のために役立つのか疑問に思います。これを実際のコードのどこで活用しましたか?
JD
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.