アドホック多相と型クラスらへん
タイトルや残す場所は後で考えるmrsekut.icon
この記事とそのコメントを読んで、楽しく見ていくためにはmrsekut.iconのあらゆる面での知識が足りていない気がしてきた 個々の多相性の理論的背景
parametric
ad hoc
sub typing
実際の機能の能力
type class
trait
interface
protocol
concepts
その他、関連する項目
HKT
動的dispatch、静的dispatch
なのでいったん保留
型クラスでもinterfaceでもできること
Hogeを実装する型が満たすべき条件を記述
実装時に、それらの条件を満たすことを強制する
genericsの型引数の制約として用いる
interfaceでしかできないこと
Animalという(Interface|type class)な値を作ることはできない
Interface、classは型だが、
type classは型ではない
型の上位概念
型クラス出来しかできないこと
self的なもの
code:hs
class Eq a where
(==) :: a -> a -> Bool
code:swift
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
code:rs
trait Equatable {
fn equals(&self, other: &Self) -> bool;
}
protocolは、Selfを使わない場合はsubtyping多相として、値に対する型として使えるが、Selfを使うと型クラス的になり、subtypyng多相はできなくなる
traitも多分そう
traitでSelfを使うと次元が上がった風に感じたのはそのためかmrsekut.icon
たしかに、これ言われてみれば普通のinterfaceでは無理なのか
code:ts
interface Equatable {
equals(other: Equatable): boolean
}
class Cat implements Equatable {
equals(other: ???): boolean {
return true;
}
}
equalsの引数をCatに強制できない
実装者が明示的に書くことはできるが、interfaceで強制できない
これ、型理論で言うと何になるんだろう
mapの抽象化
code:hs
class Functor f where
fmap :: (a -> b) -> f a -> f b
たぶん
型クラス自体は、ad hoc多相
型クラス制約は、ad hoc多相 + parametric多相
Javaでは書けない
https://www.youtube.com/watch?v=E-2y1qHQvTg
おもろ
型クラスはインターフェースと似ているが
目的が異なるものなので、そもそも比べるものではない
たしかに型クラスはInterface
型はclassと考えれば、そうだなってなるがそうでもないのか
目的が異なる
インターフェースは、それを継承したクラスに実装を強制する仕組み
型クラスは紐付いている型クラスのmethodを自動的に挿入するもの
定義と実装の場所
後から型クラスのインスタンスにできる
自作型でも既存の型でも、途中で、あ、これあの型クラスのインスタンスにしたいな、って思ったタイミングで、その型クラスのインスタンスにできる
新たな性質がほしければ新しく型クラスを定義してそのインスタンスにすればいい
Javaなどではclassを定義するタイミングでinterfaceをimplementsしないといけない
あとから付け足したければそのclassをextendsした新しいclassを作るとか?
型クラスを見ても、interfaceと似てるとはあまり思わなかったけど、traitをみるとかなりそう見える
Rustのtraitの復習
ad hoc多相と、interfaceの違い
型クラスの継承と、OOPの継承の類似点、差異
要はoverloadは型クラスの下位互換のようなものだと
code:疑似.hs
sum [] = 0
sum (x:xs) = x + sum xs
sum [] = ""
sum (x:xs) = x ++ sum xs
sum ...
同名の関数を型ごとに定義するoverloadを使うことで同じ様な実装ができる
嬉しい点
既存の型を触らずに済む
class的な実装の場合は、上の例ならばHogeclassの中にmethodを追加することになる
問題点
実装が重複する。ほとんど同じなのに型の数だけ実装を書く必要がある
例えば空配列が来た場合はどちらもその型のdefault値を返している
新しい型Piyoを作った際にこれに対してsumが欲しければPiyoの実装を追加で書く必要がある
型をさらに抽象化した概念である型クラスを導入する
code:hs
class Addable a where
unit :: a
add :: a -> a -> a
instance Addable Int where
unit = 0
add = (+)
sum :: (Addable a) => a -> a sum [] = unit
sum (x:xs) = add x (sum xs)
とある型をこの型クラスのインスタンスにするためには、methodを実装する必要がある
それさえ用意しておけば、Addableのインスタンス向けの関数(ここではsum)の実装を1回かけば、あとはそれを使い回すことができる
既存のAddbaleのインスタンスの型全てで使えるし、
まだ存在していない未来のAddabelのインスタンスの型全てでも使える
拡張性
Addable向けに新たな関数がほしいとき
Addable向けに一つ実装すれば、他の型に対しても使える
ex. double
Piyo型でsumを使いたいとき
型クラスがあれば、overloadは不要?
そうじゃないなら何故必要?
ちなみにHaskellにはoverloadはない
overloadのデメリットと、型クラスがそれをどう解決したか
「型クラス」「interface」「アドホック多相」「overload」の関係性
多相的な関数を効率よく作れるのが嬉しいのか?
これが本質?
アドホック多相の上位互換が本質ということ?
アドホック多相の限界
SMLのoverloadの問題点
算術演算子は数値型に対して多相だが、それを用いた関数を定義する事ができない
(+)など
code:疑似.hs
add a b = a + b
add 1 2 // できない
add 1.0 2.0 // できない
これを、裏でコンパイラがoverloadした関数を取りうるように扱うとすると、
上の例の場合は以下の2つが自動的に生成されることになる
code:hs
add :: Int -> Int -> Int
add :: Float -> Float -> Float
しかし、こういう関数を複数とる関数を定義すると、その数は指数関数的に増える
code:hs
adds (x, y, z) = (add x x, add y y, add z z)
上の例だけでも$ 2^3個のadds関数を生成しないといけない
Mirandaはそもそも数値型がnumしかないのでこういう問題は起きない が、その分表現力が低い
==の多相性にはいくつかのアプローチがある
overloadする
うえのaddの例と同じで、裏で複数の型に対応した関数を生成する
対象となる型は、primitiveな単一の型
関数型や、抽象型は含まない
これを使った多相関数は定義できない
code:hs
member [] y = False
member (x:xs) y = (x == y) \/ member xs y
これは算術演算子の例と同等の理由で定義できない
任意の型に適用するために生成しまくると増えすぎるから
算術演算子は特別扱いで免れているのでいけているだけ、って感じなのかなmrsekut.icon
==を全面的に多相にする
(==) :: a -> a -> Boolにする
aは任意の型なので、型システム的には関数型も許容されることになる
静的検査を通過する
しかし、実装はそうはなっていないのでランタイムエラーになる
抽象型も許容する
データ表現同士の比較をする
なにが問題なのかわからんmrsekut.icon
==を部分的に多相にする
(==) :: a(==) -> a(==) -> Boolにする
a(==)は等値比較を許す型を表す
つまりa(==)型には、関数型や抽象型は含まれな
なのでその辺は静的検査時に弾くことができる
''aとかく
eqtype variableと呼ばれる
OOPの話がいまいち理解できていない
型クラスのアイディアの背景にこれがある
そもそもなぜoverloadが必要になるのか
code:hs
-- in Library
class Addable a where
unit :: a
add :: a -> a -> a
sum' :: (Addable a) => a -> a sum' [] = unit
sum' (x : xs) = add x (sum' xs)
-- in User
instance Addable Int where
unit = 0
add = (+)
main :: IO ()
code:ts
interface Addable<T> {
value: T;
sum: (a: T[]) => Addable<T>;
}
class AddableInt implements Addable<number> {
value: number;
constructor(a: number) {
this.value = a;
}
sum(a: number[]) {
return new AddableInt(a.reduce((acc, cur) => acc + cur, 0));
}
}
Monad作るときに、returnと>>=を定義するのもめちゃくちゃアドホック多相だもんな
Concepts
この論文のことかわからないけど、C++のConceptsの初出(?)の論文で型クラスを引き合いに出し点ものがあるらしい ref 『Masterminds of Programming』.icon p.206
以下の二つは同じことを意味する
Javaの型変数がインターフェースを継承すると宣言すること
code:java
public static <T extends Comparable<T>> T min (T x, T y) {
if (x.compare(y) < 0)
return x;
else
return y;
}
Haskellの型変数が型クラスに属すると定義すること
code:hs
min :: Ord a => a -> a -> a
min x y = if x < y then x else y
Rustのトレイと
参考
↑には2つの嬉しい点について言及されている
1つ目は上に書いたようなoverloadの話
2つ目は等価判定の話
こっちがよくわからないmrsekut.icon
MLの問題点の方をまず理解しチア
Haskellの型クラスの定義は雑すぎる
CoqやIsabelleの型クラスの定義はかなり厳密らしい