Статьи

Typeclases в Scala & Haskell

Класс типов — это концепция в 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 функции; и он  знает,  какой из них вызывать, основываясь на типах.