web-dev-qa-db-ja.com

MonadインターフェースをJavaで宣言できないのはなぜですか?

読み始める前に:この質問はモナドを理解することではなく、Monadインターフェースの宣言を妨げるJava型システムの制限を特定することです。


私が読んだモナドを理解するための努力の中で this SO-モナドの簡単な説明について尋ねる質問に対するEricLippertの回答。そこで、彼はモナドで実行できる操作もリストしています。

  1. 増幅されていないタイプの値を取得し、それを増幅されたタイプの値に変換する方法があること。
  2. 増幅されていないタイプの操作を、前述の関数合成の規則に従う増幅されたタイプの操作に変換する方法があること
  3. 通常、増幅されていないタイプを増幅されたタイプから戻す方法があります。 (この最後の点は、モナドには厳密には必要ありませんが、そのような操作が存在する場合がよくあります。)

モナドについて詳しく読んだ後、最初の操作をreturn関数として識別し、2番目の操作をbind関数として識別しました。 3番目の操作で一般的に使用される名前を見つけることができなかったので、それをunbox関数と呼びます。

モナドをよりよく理解するために、私は先に進み、Javaで汎用のMonadインターフェースを宣言しようとしました。このために、私は最初に上記の3つの関数のシグネチャを調べました。モナドMの場合、次のようになります。

return :: T1 -> M<T1>
bind   :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox  :: M<T1> -> T1

return関数はMのインスタンスでは実行されないため、Monadインターフェースに属していません。代わりに、コンストラクターまたはファクトリメソッドとして実装されます。

また、今のところ、unbox関数は必須ではないため、インターフェイス宣言から省略しています。インターフェイスの実装ごとに、この関数の実装は異なります。

したがって、Monadインターフェースにはbind関数のみが含まれます。

インターフェイスを宣言してみましょう。

public interface Monad {
    Monad bind();
}

2つの欠陥があります:

  • bind関数は具体的な実装を返す必要がありますが、インターフェイスタイプのみを返します。具体的なサブタイプでボックス化解除操作が宣言されているため、これは問題です。これを問題1と呼びます。
  • bind関数は、関数をパラメーターとして取得する必要があります。これについては後で説明します。

インターフェイス宣言で具象型を使用する

これは問題1に対処します。モナドの理解が正しければ、bind関数は常に、呼び出されたモナドと同じ具象タイプの新しいモナドを返します。したがって、MonadというMインターフェースの実装がある場合、M.bindは別のMを返しますが、Monadは返しません。ジェネリックを使用してこれを実装できます。

public interface Monad<M extends Monad<M>> {
    M bind();
}

public class MonadImpl<M extends MonadImpl<M>> implements Monad<M> {
    @Override
    public M bind() { /* do stuff and return an instance of M */ }
}

最初はこれでうまくいくようですが、これには少なくとも2つの欠陥があります。

  • これは、実装クラスがそれ自体を提供せず、タイプパラメータMonadとしてMインターフェイスの別の実装を提供するとすぐに機能しなくなります。これは、bindメソッドが間違ったタイプを返すためです。たとえば、

    public class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
    

    MonadImplのインスタンスを返しますが、FaultyMonadのインスタンスを返す必要があります。ただし、ドキュメントでこの制限を指定し、そのような実装をプログラマーエラーと見なすことができます。

  • 2番目の欠陥は解決がより困難です。私はそれを問題2と呼びます:クラスMonadImplをインスタンス化しようとすると、Mのタイプを指定する必要があります。これを試してみましょう:

    new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
    

    有効な型宣言を取得するには、これを無限に続ける必要があります。別の試みがあります:

    public static <M extends MonadImpl<M>> MonadImpl<M> create() {
        return new MonadImpl<M>();
    }
    

    これは機能しているように見えますが、問題を呼び出された人に任せました。これが私のために働くその関数の唯一の使用法です:

    public void createAndUseMonad() {
        MonadImpl<?> monad = create();
        // use monad
    }
    

    これは本質的に

    MonadImpl<?> monad = new MonadImpl<>();
    

    しかし、これは明らかに私たちが望んでいることではありません。

シフトされた型パラメーターを使用して、独自の宣言で型を使用する

ここで、関数パラメーターをbind関数に追加しましょう。上記のように、bind関数のシグネチャは次のようになります:T1 -> M<T2>。 Javaでは、これはタイプFunction<T1, M<T2>>です。これは、パラメーターを使用してインターフェースを宣言する最初の試みです。

public interface Monad<T1, M extends Monad<?, ?>> {
    M bind(Function<T1, M> function);
}

ジェネリック型パラメーターとして型T1をインターフェース宣言に追加して、関数シグネチャで使用できるようにする必要があります。最初の?は、返されたタイプMのモナドのT1です。これをT2に置き換えるには、T2自体をジェネリック型パラメーターとして追加する必要があります。

public interface Monad<T1, M extends Monad<T2, ?, ?>,
                       T2> {
    M bind(Function<T1, M> function);
}

ここで、別の問題が発生します。 Monadインターフェースに3番目のタイプのパラメーターを追加したため、その使用法に新しい?を追加する必要がありました。新しい?は今のところ無視して、最初の?を調査します。これは、タイプMの返されたモナドのMです。 Mの名前を?に変更し、別のM1を導入して、このM2を削除してみましょう。

public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
                       T2, M2 extends Monad< ?,  ?, ?, ?>> {
    M1 bind(Function<T1, M1> function);
}

別のT3を導入すると、次のようになります。

public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
                       T2, M2 extends Monad<T3,  ?,  ?, ?, ?>,
                       T3> {
    M1 bind(Function<T1, M1> function);
}

別のM3を導入すると、次のようになります。

public interface Monad<T1, M1 extends Monad<T2, M2, T3, M3, ?, ?>,
                       T2, M2 extends Monad<T3, M3,  ?,  ?, ?, ?>,
                       T3, M3 extends Monad< ?,  ?,  ?,  ?, ?, ?>> {
    M1 bind(Function<T1, M1> function);
}

すべての?を解決しようとすると、これは永遠に続くことがわかります。これは問題3です。

すべてをまとめる

3つの問題を特定しました。

  1. 抽象型の宣言で具象型を使用する。
  2. ジェネリック型パラメーターとして自分自身を受け取る型をインスタンス化します。
  3. シフトされた型パラメーターを使用して、宣言でそれ自体を使用する型を宣言します。

問題は、Java型システムに欠けている機能は何ですか?モナドで動作する言語があるため、これらの言語はどういうわけかMonad型を宣言する必要があります。これらの他の言語はどのようにMonad型を宣言しますか?これに関する情報を見つけることができませんでした。Maybeモナドのような具体的なモナドの宣言に関する情報しか見つかりませんでした。

私は何かを逃しましたか? Java型システムでこれらの問題の1つを適切に解決できますか?Java型システムで問題2を解決できない場合、=の理由はありますか? Javaインスタンス化できない型宣言について警告しませんか?


すでに述べたように、この質問はモナドを理解することについてではありません。私のモナドの理解が間違っているなら、あなたはそれについてのヒントを与えるかもしれませんが、説明をしようとしないでください。モナドの私の理解が間違っている場合、説明されている問題が残ります。

この質問は、JavaでMonadインターフェースを宣言できるかどうかについても問題ではありません。この質問は、上記のリンク先のSO-answerでEricLippertによる回答をすでに受け取っています。そうではありません。この質問は、私がこれを行うことを妨げる制限が正確に何であるかについてです。エリック・リペットはこれをより高いタイプと呼んでいますが、私はそれらの周りに頭を悩ませることができません。

ほとんどのOOP言語には、モナドパターン自体を直接表すのに十分な豊富な型システムがありません。汎用型よりも高い型をサポートする型システムが必要です。したがって、私は試しません。むしろ、各モナドを表す汎用型を実装し、必要な3つの操作(値を増幅された値に変換する、増幅された値を値に変換する、増幅されていない値で関数を変換する)を表すメソッドを実装します。増幅された値の関数に。

36
Stefan Dollase

Java型システムに欠けている機能は何ですか?これらの他の言語はどのようにモナド型を宣言しますか?

良い質問!

エリック・リペットはこれをより高いタイプと呼んでいますが、私はそれらの周りに頭を悩ませることができません。

あなた一人じゃありません。しかし、実際には、思ったほどクレイジーではありません。

Haskellがモナドの「タイプ」をどのように宣言するかを見て、両方の質問に答えましょう。引用符がなぜすぐにわかるのかがわかります。私はそれをいくらか単純化しました。標準のモナドパターンには、Haskellで他にもいくつかの操作があります。

_class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a
_

信じられないほどシンプルでありながら完全に不透明に見える少年ですね。

ここで、もう少し簡単にしましょう。 Haskellでは、bindに対して独自の中置演算子を宣言できますが、これを単にbindと呼びます。

_class Monad m where
  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a
_

さて、少なくとも、そこには2つのモナド演算があることがわかります。これの残りはどういう意味ですか?

ご存知のように、最初に頭を動かすのは「より親切なタイプ」です。 (ブライアンが指摘しているように、私は元の回答でこの専門用語をいくらか単純化しました。また、あなたの質問がブライアンの注目を集めたことは非常に面白いです!)

Javaでは、「クラス」は「タイプ」の種類であり、クラスはジェネリックである可能性があります。したがって、Java intIFrobと_List<IBar>_があり、それらはすべてタイプです。

この時点から、キリンが動物のサブクラスであるクラスであるという直感を捨ててください。それは必要ありません。相続のない世界について考えてみてください。二度とこの議論に入るわけではありません。

Javaのクラスとは何ですか?さて、クラスを考える最も簡単な方法は、それが名前共通の何かを持つ値のセットであり、これらの値のいずれかがインスタンスのときに使用できるようにすることです。クラスが必要です。たとえば、クラスPointがあり、タイプPointの変数がある場合は、Pointの任意のインスタンスをそれに割り当てることができます。 Pointクラスは、ある意味ですべてのPointインスタンスのセットを記述するための単なる方法です。クラスはインスタンスよりも高いものです

Haskellには、ジェネリック型と非ジェネリック型もあります。 Haskellのクラスはnot一種の型です。 Javaでは、クラスは値のセットを記述します;クラスのインスタンスが必要なときはいつでも、そのタイプの値を使用できます。 Haskellでは、クラスはタイプのセットを記述します。これがJava型システムが欠落している重要な機能です。Haskellではクラスは型よりも高く、インスタンスよりも高くなっています。 Java階層は2レベルのみ、Haskellには3レベルあります。Haskellでは、「特定の操作を持つ型が必要なときはいつでも、このクラスのメンバーを使用できます」という考えを表現できます。

(補足:ここで、少し単純化しすぎていることを指摘したいと思います。Javaたとえば、_List<int>_と_List<String>_を考えてみてください。これらは2つの「タイプ」です。 "、しかしJavaはそれらを1つの「クラス」と見なすので、ある意味でJavaには、型よりも「高い」クラスもあります。しかし、再び、Haskellでも同じことが言えます。_list x_と_list y_は型であり、listは型よりも高いものであり、型を生成できるものです。 。したがって、実際には、Javaには3レベルがあり、Haskellには4レベルがあると言う方が正確です。ただし、要点は残ります。Haskellには記述の概念がありますJavaよりも単純に強力な型で利用可能な操作。これについては以下で詳しく説明します。)

では、これはインターフェースとどう違うのですか?これは、Javaのインターフェイスのように聞こえます-特定の操作を持つタイプが必要です。それらの操作を説明するインターフェイスを定義します。Javaインターフェース。

これで、このHaskellの意味を理解し始めることができます。

_class Monad m where
_

では、Monadとは何ですか?それはクラスです。クラスとは何ですか?これは、特定の操作を行う型が必要なときはいつでもMonad型を使用できるように、共通点がある型のセットです。

このクラスのメンバーである型があるとします。それをmと呼びます。この型がクラスMonadのメンバーになるためには、この型に対して必要な操作は何ですか?

_  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a
_

操作の名前は_::_の左側にあり、署名は右側にあります。したがって、Monadになるには、タイプmにはbindreturnの2つの演算が必要です。それらの操作の署名は何ですか?最初にreturnを見てみましょう。

_  a -> m a
_

_m a_は、Javaは_M<A>_になります。つまり、mはジェネリック型aであることを意味します。は型で、_m a_はmaでパラメーター化されます。

Haskellの_x -> y_は、「タイプxを取り、タイプyを返す関数」の構文です。 _Function<X, Y>_です。

まとめると、returnは、型aの引数を取り、型_m a_の値を返す関数です。またはJavaで

_static <A>  M<A> Return(A a);
_

bindは少し難しいです。 OPはこの署名をよく理解していると思いますが、簡潔なHaskell構文に慣れていない読者のために、これについて少し詳しく説明します。

Haskellでは、関数は1つの引数しか取りません。 2つの引数の関数が必要な場合は、1つの引数を取り、1つの引数の別の関数を返す関数を作成します。だからあなたが持っているなら

_a -> b -> c
_

では、何を手に入れましたか? aを受け取り、_b -> c_を返す関数。したがって、2つの数値を取り、それらの合計を返す関数を作成するとします。最初の数値を受け取り、2番目の数値を取り、それを最初の数値に追加する関数を返す関数を作成します。

Javaあなたは言うだろう

_static <A, B, C>  Function<B, C> F(A a)
_

したがって、Cが必要で、AとBが必要な場合は、次のように言うことができます。

_F(a)(b)
_

意味がありますか?

大丈夫、そう

_  bind :: m a -> (a -> m b) -> m b
_

は事実上、_m a_と_a -> m b_の2つを取り、_m b_を返す関数です。または、Javaでは、次のようになります。

_static <A, B> Function<Function<A, M<B>>, M<B>> Bind(M<A>)
_

または、Javaではもっと慣用的に:

_static <A, B> M<B> Bind(M<A>, Function<A, M<B>>) 
_

これで、Javaがモナド型を直接表すことができない理由がわかります。「このパターンを共通に持つ型のクラスがあります」と言うことはできません。

これで、Javaで必要なすべてのモナド型を作成できます。あなたができないことは、「このタイプはモナドタイプです」という考えを表すインターフェースを作ることです。あなたがする必要があることは次のようなものです:

_typeinterface Monad<M>
{
  static <A>    M<A> Return(A a);
  static <A, B> M<B> Bind(M<A> m, Function<A, M<B>> f);
}
_

型インターフェースがジェネリック型自体についてどのように話しているかをご覧ください。モナド型は、1つの型パラメーターで一般的な任意の型Mであり、にはこれらの2つのstaticメソッドがあります。ただし、JavaまたはC#型システムではそれを行うことはできません。もちろん、Bindは、_M<A>_をthis。しかし、Returnを静的にする方法はありません。Javaは、(1)非構築汎用型によってインターフェースをパラメーター化する機能を提供しません。 、および(2)静的メンバーがインターフェイスコントラクトの一部であることを指定する機能がありません。

モナドで動作する言語があるので、これらの言語はどういうわけかモナドタイプを宣言しなければなりません。

そう思うでしょうが、実際にはそうではありません。まず、もちろん、十分な型システムを備えた言語であれば、モナド型を定義できます。 C#またはJavaで必要なすべてのモナド型を定義できますが、型システムでそれらすべてに共通していることを言うことはできません。たとえば、モナド型によってのみパラメータ化できるジェネリッククラスを作成することはできません。

次に、他の方法でモナドパターンを言語に埋め込むことができます。 C#には「このタイプはモナドパターンに一致する」と言う方法はありませんが、C#にはクエリ内包表記(LINQ)が言語に組み込まれています。クエリ内包表記は、どのモナドタイプでも機能します。バインド操作をSelectManyと呼ぶ必要があるだけですが、これは少し奇妙です。しかし、SelectManyの署名を見ると、それがbindであることがわかります。

_  static IEnumerable<R> SelectMany<S, R>(
    IEnumerable<S> source,
    Func<S, IEnumerable<R>> selector)
_

これは、シーケンスモナド_IEnumerable<T>_のSelectManyの実装ですが、C#では次のように記述します。

_from x in a from y in b select z
_

その場合、aのタイプは、_IEnumerable<T>_だけでなく、anyモナドタイプにすることができます。必要なのは、aが_M<A>_であり、bが_M<B>_であり、モナドパターンに従う適切なSelectManyがあることです。つまり、これは、型システムで直接表現せずに、言語に「モナド認識機能」を埋め込む別の方法です。

(前の段落は実際には過度に単純化されています。このクエリで使用されるバインディングパターンは、パフォーマンス上の理由から標準のモナドバインドとは少し異なります。概念的にはこれはモナドパターンを認識します。実際には詳細が少し異なります。ここにそれら http://ericlippert.com/2013/04/02/monads-part-twelve/ 興味があれば。)

さらにいくつかの小さなポイント:

3番目の操作で一般的に使用される名前を見つけることができなかったので、それをunbox関数と呼びます。

良い選択;これは通常、「抽出」操作と呼ばれます。 モナド抽出操作を公開する必要はありませんが、もちろん、bindを呼び出すには、_M<A>_からAを取得できる必要があります。 _Function<A, M<B>>_であるため、論理的には通常、何らかの抽出操作が存在します。

comonad-ある意味では後方モナド-はextract操作を公開する必要があります。 extractは本質的にreturn後方です。 comonadにも、逆方向に向けられたextendのようなbind操作が必要です。署名はstatic M<B> Extend(M<A> m, Func<M<A>, B> f)です。

39
Eric Lippert

AspectJ プロジェクトが何をしているのかを見ると、Javaにモナドを適用するのと似ています。彼らがそれを行う方法は、追加機能を追加するためにクラスのバイトコードを後処理することです-そして彼らがそれをしなければならない理由は、言語内になしの方法がないからです必要なことを実行するためのAspectJ拡張機能。言語は十分に表現力がありません。

具体的な例:クラスAから始めるとします。M(A)はAと同じように機能するクラスですが、すべてのメソッドの入口と出口はlog4jにトレースされるようなモナドMがあります。 .AspectJはこれを行うことができますが、Java言語自体にはそれを可能にする機能はありません。

このペーパーでは、AspectJのようなアスペクト指向プログラミングがモナドとして形式化される方法について説明します

特に、Java言語内で、プログラムでタイプを指定する方法はありません(バイトコード操作a laAspectJ)すべてのタイプは、プログラムの起動時に事前定義されています。

3
antlersoft

確かに良い質問です! :-)

@EricLippertが指摘したように、Haskellで「型クラス」として知られているポリモーフィズムの型は、Javaの型システムの理解を超えています。ただし、少なくとも Frege プログラミング言語の導入以来、Haskellのような型システムを実際にJVM上に実装できることが示されています。

Java言語自体でより種類の多い型を使用する場合は、 highJCyclops などのライブラリを使用する必要があります。両方のライブラリHaskellの意味でモナド型クラスを提供します(モナド型クラスのソースについては、それぞれ ここ および ここ を参照してください)。どちらの場合も、いくつかの準備をしてください。 major構文上の不便。このコードはまったく見栄えが悪く、この機能をJavaの型システムに組み込むために多くのオーバーヘッドが発生します。どちらのライブラリも「 John McCleanが彼の 優れた紹介 で説明しているように、データ型とは別にコア型をキャプチャするための「type witness」。ただし、どちらの実装でも、Maybe extends MonadまたはList extends Monad

Javaインターフェイスでコンストラクタまたは静的メソッドを指定するという2番目の問題は、静的メソッドを非静的メソッドとして宣言するファクトリ(または「コンパニオン」)インターフェイスを導入することで簡単に克服できます。私は常に静的なものを避け、代わりに注入されたシングルトンを使用しようとします。

簡単に言えば、はい、HKTをJavaで表すことは可能ですが、現時点では非常に不便で、あまりユーザーフレンドリーではありません。

2
user1932890