読み始める前に:この質問はモナドを理解することではなく、Monad
インターフェースの宣言を妨げるJava型システムの制限を特定することです。
私が読んだモナドを理解するための努力の中で this SO-モナドの簡単な説明について尋ねる質問に対するEricLippertの回答。そこで、彼はモナドで実行できる操作もリストしています。
- 増幅されていないタイプの値を取得し、それを増幅されたタイプの値に変換する方法があること。
- 増幅されていないタイプの操作を、前述の関数合成の規則に従う増幅されたタイプの操作に変換する方法があること
- 通常、増幅されていないタイプを増幅されたタイプから戻す方法があります。 (この最後の点は、モナドには厳密には必要ありませんが、そのような操作が存在する場合がよくあります。)
モナドについて詳しく読んだ後、最初の操作を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つの問題を特定しました。
問題は、Java型システムに欠けている機能は何ですか?モナドで動作する言語があるため、これらの言語はどういうわけかMonad
型を宣言する必要があります。これらの他の言語はどのようにMonad
型を宣言しますか?これに関する情報を見つけることができませんでした。Maybe
モナドのような具体的なモナドの宣言に関する情報しか見つかりませんでした。
私は何かを逃しましたか? Java型システムでこれらの問題の1つを適切に解決できますか?Java型システムで問題2を解決できない場合、=の理由はありますか? Javaインスタンス化できない型宣言について警告しませんか?
すでに述べたように、この質問はモナドを理解することについてではありません。私のモナドの理解が間違っているなら、あなたはそれについてのヒントを与えるかもしれませんが、説明をしようとしないでください。モナドの私の理解が間違っている場合、説明されている問題が残ります。
この質問は、JavaでMonad
インターフェースを宣言できるかどうかについても問題ではありません。この質問は、上記のリンク先のSO-answerでEricLippertによる回答をすでに受け取っています。そうではありません。この質問は、私がこれを行うことを妨げる制限が正確に何であるかについてです。エリック・リペットはこれをより高いタイプと呼んでいますが、私はそれらの周りに頭を悩ませることができません。
ほとんどのOOP言語には、モナドパターン自体を直接表すのに十分な豊富な型システムがありません。汎用型よりも高い型をサポートする型システムが必要です。したがって、私は試しません。むしろ、各モナドを表す汎用型を実装し、必要な3つの操作(値を増幅された値に変換する、増幅された値を値に変換する、増幅されていない値で関数を変換する)を表すメソッドを実装します。増幅された値の関数に。
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 int
とIFrob
と_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
にはbind
とreturn
の2つの演算が必要です。それらの操作の署名は何ですか?最初にreturn
を見てみましょう。
_ a -> m a
_
_m a
_は、Javaは_M<A>
_になります。つまり、m
はジェネリック型a
であることを意味します。は型で、_m a
_はm
でa
でパラメーター化されます。
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)
です。
AspectJ プロジェクトが何をしているのかを見ると、Javaにモナドを適用するのと似ています。彼らがそれを行う方法は、追加機能を追加するためにクラスのバイトコードを後処理することです-そして彼らがそれをしなければならない理由は、言語内になしの方法がないからです必要なことを実行するためのAspectJ拡張機能。言語は十分に表現力がありません。
具体的な例:クラスAから始めるとします。M(A)はAと同じように機能するクラスですが、すべてのメソッドの入口と出口はlog4jにトレースされるようなモナドMがあります。 .AspectJはこれを行うことができますが、Java言語自体にはそれを可能にする機能はありません。
このペーパーでは、AspectJのようなアスペクト指向プログラミングがモナドとして形式化される方法について説明します
特に、Java言語内で、プログラムでタイプを指定する方法はありません(バイトコード操作a laAspectJ)すべてのタイプは、プログラムの起動時に事前定義されています。
確かに良い質問です! :-)
@EricLippertが指摘したように、Haskellで「型クラス」として知られているポリモーフィズムの型は、Javaの型システムの理解を超えています。ただし、少なくとも Frege プログラミング言語の導入以来、Haskellのような型システムを実際にJVM上に実装できることが示されています。
Java言語自体でより種類の多い型を使用する場合は、 highJ や Cyclops などのライブラリを使用する必要があります。両方のライブラリHaskellの意味でモナド型クラスを提供します(モナド型クラスのソースについては、それぞれ ここ および ここ を参照してください)。どちらの場合も、いくつかの準備をしてください。 major構文上の不便。このコードはまったく見栄えが悪く、この機能をJavaの型システムに組み込むために多くのオーバーヘッドが発生します。どちらのライブラリも「 John McCleanが彼の 優れた紹介 で説明しているように、データ型とは別にコア型をキャプチャするための「type witness」。ただし、どちらの実装でも、Maybe extends Monad
またはList extends Monad
。
Javaインターフェイスでコンストラクタまたは静的メソッドを指定するという2番目の問題は、静的メソッドを非静的メソッドとして宣言するファクトリ(または「コンパニオン」)インターフェイスを導入することで簡単に克服できます。私は常に静的なものを避け、代わりに注入されたシングルトンを使用しようとします。
簡単に言えば、はい、HKTをJavaで表すことは可能ですが、現時点では非常に不便で、あまりユーザーフレンドリーではありません。