Класс типов — это концепция в Scala и Haskell (и в некоторых других типизированных языках). Лучший способ думать о классе типов — это интерфейс, который определяет функции. Отлично. Как правило, все действительно это понимают. Проблема возникает, когда мы должны применить концепцию к практическому коду, особенно если мы выросли на Scala и теперь хотим попробовать Haskel.
Как я уже говорил выше, класс типов по сути является интерфейсом, который определяет некоторое поведение. Рассмотрим Monoid
класс типов для некоторого типа a
. В Хаскеле пишут:
class Monoid a where mempty :: a mappend :: a -> a -> a
Это эквивалент trait
в Scala, параметризованный с помощью переменной типа:
trait Monoid[A] { def mempty: A def mappend(a1: A, a2: A): A }
Если вы посмотрите на эти определения классов типов, то увидите, что это только интерфейсы. Они не содержат поведения. Давайте теперь выясним, как определить экземпляры классов типов; Экземпляр класса типов является реализацией интерфейса (скажем, Monoid
выше) для определенного типа. Рассматривать:
instance Monid Int where mempty = 0 mappend a b = a + b
Конечно, будучи Хаскеллом, мы можем исключить значения mappend
и просто написать mappend = (+)
. Теперь в Scala мы можем иметь:
class IntMonoid extends Monoid[Int] { def mempty = 0 def mappend(a: Int, b: Int) = a + b }
Если мы теперь определим полиморфную функцию (относительно ее параметров), мы можем потребовать, чтобы для типа был экземпляр класса типов.
sigma :: (Monoid a) => Int -> Int -> (Int -> Int) -> (Int -> a) -> a sigma a b inc comp = if a > b then mempty else mappend (comp a) (sigma (inc a) b inc comp)
Прежде чем углубляться в детали, позвольте мне показать эквивалентный код в Scala:
def sigma[A](a: Int, b: Int, inc: Int => Int, comp: Int => a) (implicit m: Monoid[A]): A = if (a > b) m.mempty else m.append(comp(a), sigma(inc(a), b, inc, comp))
Итак, в Scala мы должны быть немного более явными; нам нужно сообщить компилятору, какой экземпляр monoid (здесь экземпляр — это экземпляр OO, значение, которое реализует Monoid[A]
). Однако компилятор может искать неявную область видимости и находить экземпляр Monoid[Int]
; конечно сообщая об ошибках, если он не может найти точно один экземпляр.
Теперь class IntMonoid extends Monoid[Int]
это не пример. Это определение класса, и поэтому компилятор будет жаловаться, что неявного экземпляра класса не существует Monoid[Int]
. В Scala мы должны предоставить неявное значение. Мы можем сделать это, написав:
implicit val intMonoid = new IntMonoid
Или, в данном конкретном случае, мы можем сделать Monoid[Int]
синглтон!
implicit object IntMonoid extends Monoid[Int] { def mempty = 0 def mappend(a: Int, b: Int) = a + b }
Если это object IntMonoid
находится в текущей области видимости, компилятор Scala сможет предоставить его в качестве значения implicit m: Monoid[A]
параметра; и наша функция компилируется и работает, как и ожидалось.
Haskell для хорошего блага
Сравните код в Haskell:
sigma :: (Monoid a) => Int -> Int -> (Int -> Int) -> (Int -> a) -> a sigma a b inc comp = if a > b then mempty else mappend ...
И код Скала
// option 1: explicit implicits def sigma[A](a: Int, b: Int, inc: Int => Int, comp: Int => A) (implicit m: Monoid[A]): A = if (a > b) m.mempty else m.append(...) // option 2.1: type bounds def sigma[A : Monoid](a: Int, b: Int, inc: Int => Int, comp: Int => A): A = if (a > b) implicitly[Monoid[A]].mempty else implicitly[Monoid[A]].append(...) // option 2.2: type bounds and then consume implicit def sigma[A : Monoid](a: Int, b: Int, inc: Int => Int, comp: Int => A): A = { val m = implicitly[Monoid[A]] if (a > b) m.mempty else m.mappend(...) }
Поскольку Scala — это ОО, существует больше способов предоставления экземпляров классов типов; хотя в конечном итоге вам необходимо иметь неявное значение, доступное в неявной области Вы можете, однако, написать
// implicit singleton value implicit object IntMonoid extends Monoid[Int] { ... } // implicit anonymous implementation implicit val intMonoid = new Monoid[Int] { ... } // implicit function that returns a new Monoid[Int] every time implicit def intMonoid = new Monid[Int] { ... } // implicit named implementation class IntMonoid extends Monoid[Int] { ... } implicit val intMonoid = new IntMonoid
Теперь можно подумать, что в Haskell экземпляры классов типов эквивалентны implicit object
конструкции в Scala. Ну, есть некоторые условия!
Тип класса «наследство»
Мы можем иметь своего рода наследование в наших классах типов. Давайте добавим класс типов для Group
.
class Group a where mempty :: a mappend :: a -> a -> a inverse :: a -> a
Это похоже на слишком много печатать. Нам бы очень хотелось «наследовать» все функции Monoid a
и предоставлять только inverse
. Наша концепция «наследования» гласит, что если у меня есть экземпляр Group
for a
, то у меня также есть экземпляр Monoid
for a
. Это не традиционная ситуация с подклассами ОО.
class (Monoid a) => Group a where inverse :: a -> a
Здесь у нас есть класс Group
для некоторого типа a
, который «наследует» функции Monoid
для того же типа a
. Чтобы создать экземпляр Group Int
, мы можем теперь предоставить только одну оставшуюся функцию.
instance Group Int where inverse = ((-1) *)
И теперь мы можем с радостью потребовать эти случаи:
example :: (Group a) => a -> a example a = inverse (mappend a a)
Конечно, мы можем сделать то же самое в Scala; хотя синтаксис немного сложнее, и иногда нам приходится иметь дело с неприятными побочными эффектами. Давайте определимся Group[A]
, что расширяется Monoid[A]
.
trait Group[A] extends Monoid[A] { def inverse(a: A): A }
Теперь, как мы можем создавать экземпляры, Group[A]
если у нас уже есть Monoid[A]
? Мы могли бы, конечно, написать все это:
implicit object IntGroup extends Group[Int] { def mempty = 0 def mappend(a: Int, b: Int) = a + b def inverse(a: Int) = -a }
Или (неэффективно!) Мы можем написать код, похожий на код на Haskell:
implicit def intGroup(implicit m: Monoid[Int]): Group[Int] = new Group[Int] { def mempty = m.mempty def mappend = m.mappend def invserse(a: Int) = -a }
Здесь вы можете увидеть, что компилятор создаст экземпляр класса Group[Int]
, если у него уже есть экземпляр класса Monoid[Int]
. Именно здесь классы типов Хаскелла действительно сияют; Подход Scala не такой эффективный или элегантный.
Богоявление
Во время недавней учебной поездки мой друг сказал мне: «Я получаю классы типов в Scala, но как я могу вызывать функции на экземплярах классов типов в Haskell?» Позвольте мне вернуться к sigma
функции. В Scala пишем
def sigma[A](a: Int, b: Int, inc: Int => Int, comp: Int => a) (implicit m: Monoid[A]): A = if (a > b) m.mempty else m.append(comp(a), sigma(inc(a), b, inc, comp))
В Хаскеле мы пишем:
sigma :: (Monoid a) => Int -> Int -> (Int -> Int) -> (Int -> a) -> a sigma a b inc comp = if a > b then mempty else mappend (comp a) (sigma (inc a) b inc comp)
Разница в том, что в Haskell компилятор делает все функции из класса типов доступными в теле sigma
функции; и он знает, какой из них вызывать, основываясь на типах.