Статьи

Как вы думаете, мы сочиняем?

Строительные системы для композиции

Перспектива объектно-ориентированных языков состояла в том, что мы сможем использовать объекты для соединения и объединения в более крупные и более сложные системы.

Каноническим примером является Personиерархия:

class Person {
  def getName: String
  def getAddress: Address
  def getAge: Int
}

class Student extends Person {
  def getCurrentClasses: List[Clazz]
  def getGrades: List[Grades]
}

class Teacher extends Person {
  def getStudents: List[Student]
  def getClasses: List[Clazz]
}

У нас есть люди, которые могут быть учениками или учителями. Но как насчет студентов-преподавателей? А как насчет людей, которые имеют ключи от здания?

Даже в тривиальном примере мы начинаем interfacesсталкиваться с ситуациями, когда иерархии классов не сочетаются друг с другом, поэтому нам необходимо добавить и решить проблему поддержания актуальности реализации интерфейсов во всех классах, которые реализуют данный интерфейс.

Это все неприятные вещи и приводит к коду, который не может быть легко составлен или поддержан.

Скрытая проблема

В большинстве ОО-проектов есть еще одна скрытая проблема. Поскольку экземпляры данного класса не могут создавать копии только с одним измененным фрагментом данных, большинство ОО-систем спроектированы на основе изменения экземпляров на месте … или изменения данных в данном объекте, а не на создании копии только с одним измененным значением.

Это приводит к огромной проблеме, когда вы пытаетесь распределить вычислительную проблему по нескольким потокам в одном адресном пространстве. Должно быть какое-то утверждение о том, что ссылки на объекты (блокировки) являются достаточно детальными, чтобы позволить нескольким потокам воздействовать на данные, но недостаточно детализированными, чтобы иметь проблемы с тупиковой блокировкой или высокую стоимость получения и снятия блокировок.

Таким образом, в дополнение к отсутствию композиции, традиционная ОО-парадигма не работает в параллельных ситуациях.

Проблема обратного вызова

В Java это обычный шаблон для использования интерфейса обратного вызова. Что-то типа:

interface Foo {
  def computeSomething(in: SomeType): SomeOtherType
}

Java-код завален этими обратными вызовами. В Java 1.1 мы даже получили анонимные внутренние классы, чтобы мы могли писать простые обратные вызовы на месте без необходимости явно определять класс для решения проблемы.

Разве не было бы неплохо иметь единственный интерфейс, который можно было бы использовать (с правильными типами ввода и вывода) в любом месте, где у вас есть один из этих интерфейсов обратного вызова?

Функциональность и состав

Многие из нас знакомы с каналами Unix. Это простые, мощные способы составления операций преобразования данных (обычно текстовых). Утилиты , как sed, awk, grepи т.д. являются инструментами преобразования образуют вход к выходу.

Мы можем использовать эту модель преобразования для множества различных типов данных. И оказывается, что модель преобразования (а не модель мутации) позволяет составлять конвейеры преобразования, которые имеют ту же сигнатуру, что и отдельные элементы конвейера.

Итак, если мы определим функцию, которая принимает Stringи возвращает a Stringas, String => Stringи функцию, которая принимает a Stringи возвращает Intas String => Int, мы также можем составить функции в функцию, которая принимает a Stringи возвращает Int:

def f1: String => String
def f2: String => Int
def composed: String => Int = f1 andThen f2

Вы даже можете составить весь список функций в одну функцию, которая представляет конвейер преобразования:

def lof: List[String => String]
def composed: String => String = lof.foldLeft(s => s)((a, b) => a andThen b)

Речь идет об операциях, а не о данных

Ключевым принципом ОО является объединение данных с операциями, которые могут выполняться над данными. Классы сопоставляют подпись вызова (имя и параметры) с функцией.

Отделив операции кода от данных, мы получаем операции кода, которые можно использовать намного эффективнее, чем классы ОО.

Средний возраст учеников в классе

Если бы мы хотели быть абсурдными, мы могли бы создать специальный класс — ListOfStudents. Это так же, как обычный список, за исключением того, что он принимает только студентов, и у него есть метод, который даст нам средний возраст студентов в данном классе.

class ListOfStudents extends List[Student] {
  def averageAgeByClass(cl: Clazz): Int = {
    var cnt = 0
    var sum = 0

    foreach(s => if (s.getClasses.contains(cl)) {cnt += 1; sum += s.getAge})

    sum / cnt
  }
}

Итак, мы переписали функцию усреднения (плохо) и у нас есть специальный класс, который является просто контейнером для студентов.

Что если бы мы сделали универсальные функции для фильтрации по классам и для усреднения по списку элементов, которые можно преобразовать в Int.

def genericFilter[A, B](f: A => List[B],b: B)(lst: List[A]): List[A] = lst.filter(a => f(a).contains(b))
def average[A](f: A => Int)(lst: List[A]): Option(Int) = 
  if (lst.isEmpty) None else Some(lst.map(f).reduceLeft(_ + _) / lst.length)

Мы создали полуобобщенный фильтр и очень универсальную (и безопасную с нулевым счетом) функцию усреднения.

Как мы их составляем?

def studentAgeByClass(cl: Clazz, lst: List[Student]): Option[Int] = 
 (genericFilter(_.getCurrentClasses, cl) andThen average(_.getAge)).apply(lst)

Обратите внимание на то, как мы создали частично примененные функции (функции, у которых были некоторые, но не все предоставленные параметры), скомпоновали эти функции и применили эти функции к нашему набору данных.

Эта форма композиции позволяет нам гораздо больше гибкости в определении блоков логики, а затем объединении этих блоков вместе. Когда речь идет о специфике применения наших функций, нам нужно только создать простые функции (например, преобразовать a Studentв an Int), которые соединяют логику обобщенной функции и специфику наших данных.

Больше

В будущем сообщении в блоге я расскажу о частичных функциях Scala (функциях, определенных для некоторых значений типа) и о том, как они используются в Lift для компоновки в глобальные обработчики HTTP-запросов.