Haskellのreturn-type-(only)-polymorphismは良いことですか?


28

Haskellで私がまったく思いつかなかったことの1つは、次のように、入力型では戻り型を決定できない多相定数と関数をどのように持つことができるかということです。

class Foo a where
    foo::Int -> a

私はこれが好きではない理由のいくつか:

参照の透明性:

「同じ入力が与えられたHaskellでは、関数は常に同じ出力を返します」が、それは本当ですか?コンテキストでread "3"使用すると3を返しIntますが、たとえば(Int,Int)コンテキストで使用するとエラーをスローします。はい、あなたはそれreadも型パラメータを取っていると主張することができますが、型パラメータの暗黙性は、私の意見ではその美しさの一部を失うことになります。

単相性の制限:

Haskellで最も厄介なことの1つ。私が間違っている場合は修正してください。しかし、MRの全体的な理由は、共有されているように見える計算は、型パラメーターが暗黙的であるためではない可能性があることです。

デフォルトのタイプ:

繰り返しになりますが、Haskellで最も厄介なことの1つです。たとえば、出力で多相関数の結果を入力で多相関数に渡す場合に起こります。繰り返しますが、間違っている場合は修正してください。ただし、入力タイプ(および多態定数)で戻り値のタイプを判別できない関数がなければ、これは必要ありません。

だから私の質問は(「議論の質問」としてスタンプされるリスクを実行している):型チェッカーがこれらの種類の定義を許可しないHaskellのような言語を作成することは可能でしょうか?もしそうなら、その制限の利点/欠点は何でしょうか?

私はいくつかの差し迫った問題を見ることができます:

、たとえば、場合2のみのタイプを持っていたInteger2/3現在の定義ではもうチェックを入力しないでしょう/。しかし、この場合、機能的な依存関係を持つ型クラスが助けになると思います(はい、これは拡張機能であることがわかります)。さらに、入力タイプが制限されている関数を使用するよりも、異なる入力タイプを使用できる関数を使用する方がはるかに直感的であると思いますが、ポリモーフィック値をそれらに渡すだけです。

[]およびのNothingような値の入力は、クラックするのが難しいナットのように思えます。私はそれらを処理する良い方法を考えていません。

回答:


37

実際、戻り値の型の多相性は、型クラスの最も優れた機能の1つだと思います。しばらく使用した後、OOPスタイルモデリングに戻らない場合があります。

代数のエンコーディングを検討してください。Haskellには型クラスがありますMonoid(無視しますmconcat

class Monoid a where
   mempty :: a
   mappend :: a -> a -> a

これをオブジェクト指向言語のインターフェースとしてどのようにエンコードできますか?簡単な答えはできません。これmemptyは、の型が(Monoid a) => a別名であるため、戻り値の型ポリモーフィズムです。代数をモデル化する能力を持つことは、非常に便利なIMOです。*

「参照の透明性」に関する苦情から投稿を開始します。これは重要なポイントを提起します:Haskellは価値志向の言語です。したがって、式read 3は値を計算するものとして理解する必要はなく、値としても理解できます。これが意味することは、実際の問題は戻り型の多相性ではなく、多相型([]およびNothing)を持つ値であるということです。言語にこれらが必要な場合、一貫性のためにポリモーフィックな戻り値の型が実際に必要です。

[]型であると言えforall a. [a]ますか?私はそう思う。これらの機能は非常に便利であり、言語をより簡単にします。

Haskellにサブタイプ多型[]があれば、すべてのサブタイプになり[a]ます。問題は、空のリストの型を多態的にしないでエンコードする方法がわからないことです。Scalaでどのように行われるかを検討してください(標準的な静的型付きOOP言語であるJavaで行うよりも短いです)

abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]

ここでも、Nil()タイプNil[A]**のオブジェクトです

戻り型ポリモーフィズムのもう1つの利点は、Curry-Howard埋め込みがはるかに簡単になることです。

次の論理定理を考慮してください。

 t1 = forall P. forall Q. P -> P or Q
 t2 = forall P. forall Q. P -> Q or P

これらをHaskellの定理として簡単に捉えることができます。

data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right

要約すると、私は戻り型のポリモーフィズムが好きで、値の概念が限られている場合にのみ参照の透明性を損なうと考えています(ただし、アドホック型クラスの場合はあまり説得力がありません)。一方、私はあなたのMRとタイプのデフォルトに関する説得力のあるポイントを見つけます。


*。ysdxのコメントでは、これは厳密には真実ではありません。代数を別の型としてモデル化することにより、型クラスを再実装できます。javaのように:

abstract class Monoid<M>{
   abstract M empty();
   abstract M append(M m1, M m2);
}

次に、このタイプのオブジェクトを渡す必要があります。Scalaには、これらのことを明示的に管理するオーバーヘッドの一部を回避する暗黙的なパラメーターの概念がありますが、私の経験ではすべてを回避するわけではありません。ユーティリティメソッド(ファクトリメソッド、バイナリメソッドなど)を別のFバインド型に配置することは、ジェネリックをサポートするOO言語で物事を管理する非常に優れた方法であることがわかりました。そうは言っても、タイプクラスを使って物事をモデル化した経験がなかったら、このパターンをひそかにしたかどうかはわかりません。

また、制限があり、そのままでは、任意の型の型クラスを実装するオブジェクトを取得する方法はありません。値を明示的に渡すか、Scalaの暗黙のようなものを使用するか、何らかの形式の依存関係注入テクノロジーを使用する必要があります。人生はいです。一方、同じ型に対して複数の実装を行えることは素晴らしいことです。何かが複数の方法でモノイドになります。また、これらの構造を別々に持ち歩くと、IMOはより数学的にモダンで建設的な感じになります。だから、私はまだこれを行うにはHaskellの方法を一般的に好むが、おそらく私の主張を誇張した。

戻り型ポリモーフィズムを持つ型クラスにより、この種の処理が容易になります。それはそれを行うための最良の方法だとは思わない。

**。 JörgW Mittagは、これは実際にはScalaでこれを行う標準的な方法ではないことを指摘しています。代わりに、次のような標準ライブラリを使用します。

abstract class List[+A] ...  
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...

これは、Scalaのボトム型と共変型パラメーターのサポートを利用しています。したがって、NilタイプNilはnot Nil[A]です。この時点では、Haskellからかなり離れていますが、Haskellがボトム型をどのように表しているかに注目するのは興味深いことです。

undefined :: forall a. a

つまり、これはすべてのタイプのサブタイプではなく、すべてのタイプのポリモーフィック(sp)メンバーです。
さらに多くの戻り型ポリモーフィズム。


4
「これをオブジェクト指向言語のインターフェースとしてどのようにエンコードできますか?」ファーストクラスの代数を使用できます:interface Monoid <X> {X empty(); X append(X、X); }しかし、明示的に渡す必要があります(これはHaskell / GHCの舞台裏で行われます)。
ysdx

@ysdxそれは本当です。また、暗黙的なパラメーターをサポートする言語では、(Scalaのような)haskellの型クラスに非常に近いものが得られます。私はそれを知っていました。私のポイントは、これは人生をかなり難しくするということでした:私は自分自身を「タイプクラス」をその場所全体に格納するコンテナを使用しなければならないことに気づきました(うん!)。それでも、私はおそらく私の答えで双曲線が少なかったはずです。
フィリップJF

+1、素晴らしい答え。しかし、1つの無関係なnitpickは、Nilおそらくa case objectではなくa であるべきcase classです。
ヨルグWミットタグ

@JörgW Mittagそれは完全に無関係ではありません。コメントを修正するために編集しました。
フィリップJF

1
とてもいい答えをありがとう。おそらくそれを消化/理解するのに少し時間がかかります。
大日

12

誤解に注意してください:

「同じ入力が与えられたHaskellでは、関数は常に同じ出力を返します」が、それは本当ですか?「3」を読み取ると、Intコンテキストで使用すると3が返されますが、たとえば(Int、Int)コンテキストで使用するとエラーがスローされます。

妻がスバルを運転し、スバルを運転しているからといって、同じ車を運転しているわけではありません。2つの名前の関数があるからといってread、それが同じ関数であることを意味するわけではありません。Indeed read :: String -> Intread :: String (Int, Int)(Int、Int)のReadインスタンスで定義されているIntのReadインスタンスで定義されています。したがって、それらは完全に異なる機能です。

この現象はプログラミング言語では一般的であり、通常はオーバーロードと呼ばれます。


6
質問のポイントを見逃したと思います。オーバーロードとも呼ばれるアドホックなポリモーフィズムを持つほとんどの言語では、呼び出す関数の選択は、戻り値の型ではなく、パラメータの型のみに依存します。これにより、関数呼び出しの意味について簡単に推論することができます。単に式ツリーのリーフから開始し、「上方向」に動作します。Haskell(および戻り値型のオーバーロードをサポートする少数の他の言語)では、小さなサブ式の意味を理解するために、式ツリー全体を一度に考慮する必要があります。
ローレンスゴンサルベス

1
質問の核心を完全に打ったと思います。シェークスピアでさえ「他の名前の機能も同様に機能する」と述べた。+1
トーマスエディング

@Laurence Gonsalves-Haskellの型推論は参照的に透過的ではありません。コードの意味は、情報を内側に引っ張る型推論のために、使用されるコンテキストに依存します。それは返品タイプの問題に限定されません。Haskellは、Prologをそのタイプシステムに効果的に組み込みました。これにより、コードがわかりにくくなりますが、大きな利点もあります。個人的には、最も重要な参照の透明性は、実行時に起こることだと思います。なぜなら、それなしでは怠inessを扱うことは不可能だからです。
Steve314

@ Steve314参照的に透過的な型推論がなくてもコードが不明瞭にならない状況はまだ見たことがありません。複雑なものについて推論するには、物事を精神的に「チャンク」できる必要があります。あなたが猫を飼っていると言ったら、私は何十億もの原子や個々の細胞の雲については考えません。同様に、コードを読み取るとき、式をサブ式に分割します。Haskellは、これを2つの方法で打ち負かす:「間違った」型推論と、過度に複雑な演算子のオーバーロード Haskellコミュニティには、カレンに対する嫌悪感もあり、カレンをさらに悪化させています。
ローレンスゴンサルベス

1
@LaurenceGonsalvesあなたは、infix機能が悪用される可能性があることは正しいです。しかし、これはユーザーの失敗です。Javaのように、制限、OTOHは私見ではありません。これを確認するには、BigIntegerを処理するコードを探すだけです。
インゴ

7

「戻り型ポリモーフィズム」という用語が作成されなかったことを本当に望みます。何が起こっているのか誤解を助長します。「戻り型の多型」を排除することは非常にアドホックで表現力を損なう変更であるが、問題で提起された問題をリモートで解決することさえできないと言うだけで十分です。

戻り値の型は決して特別なものではありません。考慮してください:

class Foo a where
    foo :: Maybe a -> Bool

x = foo Nothing

このコードは「戻り型ポリモーフィズム」とまったく同じ問題を引き起こし、OOPと同じ種類の違いも示します。しかし、「第一引数多分型多型」については誰も話していない。

重要な考え方は、実装が使用するインスタンスを解決するために型を使用しているということです。どんな種類の(実行時)値も、それとは何の関係もありません。実際、値を持たない型でも機能します。特に、Haskellプログラムはそのタイプがなければ意味がありません。(皮肉なことに、これによりHaskellはカレー風の言語ではなく教会風の言語になります。これについて詳しく説明しているブログ記事があります。)


「このコードは、「戻り型ポリモーフィズム」と同じ問題をすべて引き起こします」。いいえ、そうではありません。「foo Nothing」を見て、そのタイプが何であるかを判断できます。それはブールです。コンテキストを見る必要はありません。
大日

4
実際には、コンパイラはa「戻り型」の場合のようなものを知らないため、コードは型チェックを行いません。繰り返しますが、戻り値の型について特別なものはありません。すべての部分式のタイプを知る必要があります。検討してくださいlet x = Nothing in if foo x then fromJust x else error "No foo"
デレクエルキンズ

2
「第二引数多型」は言うまでもありません。のような関数Int -> a -> Boolは、実際にカリー化Int -> (a -> Bool)することにより、戻り値のポリモーフィズムに戻ります。どこでも許可する場合は、どこにでもある必要があります。
ライアンライク

4

ポリモーフィック値の参照透過性に関する質問については、次の方法が役立ちます。

名前を 考えてください1。多くの場合、異なる(ただし固定)オブジェクトを示します。

  • 1 整数のように
  • 1 本物のように
  • 1 正方正方行列のように
  • 1 恒等関数のように

ここで、各コンテキストの下で1は、固定値であることに注意することが重要です。つまり、名前とコンテキストの各ペアは一意のオブジェクトを示します。コンテキストがなければ、どのオブジェクトを示しているのかわかりません。したがって、コンテキストを推論する(可能であれば)か、明示的に提供する必要があります。(便利な表記法を除く)すばらしい利点は、汎用コンテキストでコードを表現できることです。

ただし、これは単なる表記であるため、代わりに次の表記を使用できます。

  • integer1 整数のように
  • real1 本物のように
  • matrixIdentity1 正方正方行列のように
  • functionIdentity1 恒等関数のように

ここでは、明示的な名前を取得します。これにより、名前からコンテキストを導き出すことができます。したがって、オブジェクトを完全に示すには、オブジェクトの名前のみが必要です。

Haskell型クラスは、前の表記法を選択しました。これがトンネルの終わりにあるライトです。

read値ではなく名前(コンテキストを持たない)read :: String -> aですが、値(名前とコンテキストの両方を持っています)です。したがって、関数read :: String -> Intread :: String -> (Int, Int)は文字通り異なる関数です。したがって、入力に同意しない場合、参照の透明性は損なわれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.