четверг, 15 декабря 2011 г.

Введение в Validation: часть 2


В прошлой статье я постарался рассмотреть наиболее простой подход к использованию класса Validation (искушенный читатель заметит, что не только его). Помимо этого, весьма прямолинейного, подхода к комбинированию значений внутри Validation у нас есть второй, несколько более хитрый. Именно из за него scalaz когда-то и проник в мой проект.

Основная идея второго подхода состоит в том, что если мы можем как-то агрегировать ошибки, то выполняя операции над завёрнутыми в Validation значениями можно автоматически это делать в случае обнаружения нескольких ошибок во входных данных. В отличии от ранее рассмотренного стиля, где ошибки вытаскиваются по одной, мы получаем стиль обработки (и соответственно извлечения из внешнего мира) ошибок "всё и сразу".

Именно в таком сценарии проявляется преимущество всех этих сложных штук, перед "проверенными временем" исключениями. У вас бывали в проекте классы "CompositeException" со списком других исключений в одном из полей? А милые кусочки кода, которые пробовали что-то делать, складывали ошибки в список и потом выбрасывали его в этой незатейливой оболочке? Тогда этот пост для Вас.

scalaz даёт нам операцию "собрать значения и если они успешны применить к ним функцию, а если нет - сагрегировать все возникшие ошибки". Реализуется она созданием временного объекта, который представляет собой выборку успешных данных. Эта выборка может быть преобразована применением к ней обычной функции (в смысле принимающей и возвращающей простые значения). Применение это похоже на обычный map, в том смысле, что при успешных входах на выходе появляется результат применения к ним функции. Но отличается тем, что оперирует сразу над набором значений и знает что делать с несколькими неудачами.

В scalaz для доступа к этому функционалу применяется без сомнения согревающий сердце истинного ценителя математики оператор |@|. Результатом его применения к паре завёрнутых в Validation значений является тот самый промежуточный объект в который мы можем или продолжить добавлять значения с помощью того же оператора или применить к нему функцию соответствующей арности.

Не смертельно сложно, давайте решим простую задачку: пусть теперь мы ходим, чтобы наш код из предыдущего поста теперь не вытаскивал и проверял параметры по одному а брал и проверял всё сразу. Таким образом, чтобы напортачившему клиенту приходил сразу весь пакет его ошибок и он мог над всеми сразу поработать.

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

case class SearchQuery(
  text: String,
  order: Option[Order],
  limit: Int)

def parseSearchQuery(req: Req): ReqVal[SearchQuery] =
  for {
    text  <- get(req, "text")
    order <- getOptOrder(req, "sort")
    limit <- getInt(req, "limit")
  } yield SearchQuery(text, order, limit)

def handleSearchPosts(req: Req): Response = 
  serialize(
    parseSearchQuery(req) flatMap searchPosts _
  )

Неплохо, наша программа несколько прибавила в кохесии, а основные умственные усилия были потрачены при удалении отступов после копипаста. Теперь пора притворить в жизнь задуманное. К сожалению сразу сделать это немного затруднительно. Дело в том, что для старого класса ошибок у нас нет разумного способа их объединять. Но это легко, исправить. Пусть в случае неудачи вместо одной ошибки у нас будет список ошибок. Да собственно библиотека нам даёт готовый такой тип, надо просто им воспользоваться.

type ReqVal[T] = ValidationNEL[String, T]

def get(req: Req, name: String): ReqVal[String] =
  req.param(name).toOption.toSuccess(
    ("Missed parameter "+name).failNel)

def parseInt(s: String): ReqVal[Int] =
  s.parseInt.fail.map { ne: NumberFormatException =>
      ("Value "+s+" is not an integer").failNel
    }.validation

def parseSearchQuery(req: Req): ReqVal[SearchQuery] =
  (   get(req, "text")
  |@| getOptOrder(req, "sort")
  |@| getInt(req, "limit"))
    apply SearchQuery _ 

А всего остального кода сделанное не касается. Основная проблема - это места, где создаются экземпляры ошибок. Они имеют теперь новый тип и должны создаваться иначе, с помощью метода failNel добавляемого ко всем объектам при импорте scalaz автоматически. К счастью с успешными результатами вывод типов вполне может управится сам.

Обратите внимание, мы не можем (и определённо не хотим) втянуть в мир "всего и сразу" операцию обращающуюся к БД. Но миры "шаг за шагом" и "всё сразу" совершенно интероперабельны. Как только что было показано, мы всегда можем выбрать удобный и даже потом поменять решение, чтоб так реализации SOAP жили.

Ну и для полноты картины можно привести пример с композицией. Давайте снова уроним на свои головы новый функционал. Пусть наши пользователи захотели смотреть не первые N результатов поиска, а все сколько их есть. С издавна на руси такую задачу решали пагинацией. И вот на смену отслужившему свое параметру limit приходит пара новых: slice_start и slice_end. И для обеспечения приемлемой производительности мы решили ограничить размер страницы 50 результатами.

Для начала как всегда подправим наши типы:

case class Slice(start: Int, end: Int)

case class SearchQuery(
  text: String,
  order: Option[Order],
  slice: Slice)

Имея необходимые типы в своём распоряжении, можно начать притворять функционал в жизнь. Очевидно, что потребуется новый строительный блок, функция вида Req => ReqVal[Slice]

def getSlice(req: Req): ReqVal[Slice] = {
  val start = getInt(req, "slice_start")
  val end   = getInt(req, "slice_end")
  if ((end | 0) - (start | 0) <= 50)
    (start |@| end) {Slice(_, _)}
  else
    "The page is too large".failNel
  }
}

Отлично, и теперь новая функция легко встаёт на своё место.

def parseSearchQuery(req: Req): ReqVal[SearchQuery] =
  (   get(req, "text")
  |@| getOptOrder(req, "sort")
  |@| getSlice(req))
    apply SearchQuery _ 

Вот и всё. В следующем посте я планирую наконец рассказать как пытался совместить эти изящные приёмы с грубым и жестоким миром баз данных и servlet контейнеров.

вторник, 8 ноября 2011 г.

Введение в Validation: часть 1

В этом посте я хочу подготовить почву для будущего рассказа о подходе к обработке ошибок, который как ни крути получился весьма скомканный без должной политической подготовки аудитории. Его темой будет класс Validation из библиотеки scalaz, некоторые (далеко не все) техники работы с ним и отдельные языковые конструкции, которые я нашёл наиболее полезными.

Этот класс по сути объявлен следующим образом:
sealed trait Validation[+E, +A] {
  def isSuccess : Boolean = this match {
    case Success(_) => true
    case Failure(_) => false
  }
  def isFailure : Boolean = !isSuccess
}
final case class Success[E, A](a: A) extends Validation[E, A]
final case class Failure[E, A](e: E) extends Validation[E, A]
В объявлении есть ещё ряд интересных методов, но давайте пока оставим их без внимания. Ничего не напоминает? Да это же брат-близнец Either. Опять велик? Спокойно, эта модель обладает рядом важных свойств.

Во-первых у него объявлены методы map и flatMap. В scala это означает, что мы можем использовать экземпляры этого класса (на самом деле trait, но как это блин по-русски сказать то?) в выражении for. Например так (это переписанный код метода login из моего давнего поста на эту же тему):
def login(login: Option[String], password: Option[String]): Validation[String, UserInfo] =
  for {
    login <- login.toSuccess( "You should provide login" )
    password <- password.toSuccess( "You should provide password" )
    user <- findUser(login)
    checkedUser <- checkUser(user)
    loggedUser <- doLogin(checkedUser, login, password)
  } yield loggedUser
Потратим немного времени и рассмотрим этот пример подробнее. Во-первых имеющиеся логин и пароль пользователя преобразуются из Option в Validation с помощью метода toSuccess, его реализация абсолютно очевидна. Об этой операции можно думать следующим образом: альтернатива из существующего или отсутствующего значения преобразуется в альтернативу из валидного значения или сообщения об ошибке. Во-вторых происходят вызовы методов findUser, checkUser, doLogin. Мне не хочется тратить время на детальное их описание, будем считать что они объявлены принимающими обычные значения (не Option или Validation, а строки и объекты) и возвращающими Validation[UserInfo, String]. То есть каждый из них принимает некоторые данные для проверки и возвращает либо информацию о прошедшем эту проверку пользователе либо сообщение о случившейся ошибке.

Теперь очередь магии внутри выражения for. <- в данном контексте работает как "Если у нас справа успешное вычисление связать имя слева с его результатом, если неудача - вернуть его как результат всего выражения." А yield в свою очередь работает как "Если все связывания были успешны вычислить выражение и вернуть его как успех". Обратите внимание, выбор всё ещё тут, но он спрятан в способе которым выражение for комбинирует свои элементы.

У Either методов map и flatMap нет, есть они только в его "проекциях" (правой и левой). На практике это значит, что использование Validation практически постоянно экономит нам 5 символов. Сравните код выше с кодом из моего старого поста.

Важно понимать, что этот подход позволяет унифицировано обрабатывать целый ряд проблемных ситуаций: (а) ошибку во входных данных (отсутствие пароля), (б) собственно отказ в обработке основанный на логике (пароль не верен) и (в) ошибку во внешней службе (БД легла в момент сверки пароля). При этом код верхнего уровня даже не упоминает никакие Failure, только использует <- вместо =.

Помимо приятного синтаксического сахара для сокрытия if'ов от нашего взгляда, такой подход к передаче ошибок обладает одним важным свойством - отдельные методы следующие паттерну "значения на вход - валидации на выход" могут легко объединяться вместе рядом других способов помимо for. В каком-то смысле они обладают свойством замыкания. Для примера я сейчас построю простой модуль для парсинга параметров http запросов из пары десятков строк (полнофункциональная, боевая её версия около 50, но её я естественно никому не покажу :) ).

Пусть у нас есть класс Req, несущий в себе параметры HTTP запроса в форме словаря <имя параметра> - <список значений>. Нам нужен набор функций, позволяющий типобезопасно извлекать из него параметры, при этом возможно применяя некие алгоритмы парсинга (скажем, стандартный toInt). Также желательно иметь возможность извлекать необязательный параметры и наборы параметров. Интерфейс такого модуля можно описать следующим образом:
trait ReqValidation {
  type ReqVal[T] = Validation[String, T]
  //возвращает значение параметра как есть или сообщение о его отсутствии
  def get(req: Req, name: String): ReqVal[String]
  //возвращает необязательное значение (его выполнение всегда успешно)
  def getOpt(req: Req, name: String): ReqVal[Option[String]]
  //возвращает целое значение или собщение об ошибке парсинга или сообщение об отсутствии параметра
  def getInt(req: Req, name: String): ReqVal[Int]
  //возвращает небязательное целое значение или собщение об ошибке парсинга
  def getOptInt(req: Req, name: String): ReqVal[Option[Int]]
}

Во-первых я ввожу новый тип для результата получения параметра. Этот тип параметризован типом успешного результата. Он является псевдонимом для Validation со строкой в качестве описания неудачи и его параметром в качестве типа успешного значения.

Давайте последовательно реализуем каждую из этих функций, начнём с get:
def get(req: Req, name: String): ReqVal[String] =
  req.param(name).toOption.toSuccess(
    Failure("Missed parameter "+name))

Она извлекает значение параметра по имени, преобразует его в стандартный Option. И уже его преобразует в Validation с соответствующим сообщением об ошибке.
Следующим шагом будет реализация метода getOpt. Его можно сделать уже совсем прямолинейным:
def getOpt(req: Req, name: String): ReqVal[Option[String]] =
  Success(req.param(name).toOption)

Реализацию второй пары методов, подразумевающих парсинг стоит начать с того, что собственно определить парсер:
def parseInt(s: String): ReqVal[Int] =
  s.parseInt.fail.map { ne: NumberFormatException =>
      Filure("Value "+s+" is not an integer")
    }.validation

Здесь используется стандартный метод parseInt, возвращающий Either. При помощи метода fail мы преобразуем его в FailProjection и преобразуем исключение из стандартной библиотеки Java в читаемое сообщение об ошибке. Кагда это сделано результат преобразуется назад в обычный Validation. FailProjection - это специальная форма Validation с инвертированной в каком-то смысле логикой: он сделан для явного манипулирования ошибкой и неявного игнорирования успешного результата.

А также вспомогательный метод для применения parseInt и ему подобных к необязательному значению:
def parseOption[T](parse: String => ReqVal[T])(opt: Option[String]): ReqVal[Option[T]] =
  opt match {
    case Some(s) => parse(s).lift[Option, T]
    case None => success(None)
  }

Метод lift берёт успешное значение и дополнительно заворачивает его в указанную "оболочку", в данном случае Option.

Теперь можно написать реализацию getInt:
def getInt(req: Req, name: String): ReqVal[Int] =
  get(req, name) flatMap parseInt _

Она предельно проста. Не особо задумываясь мы можем получить и getOptInt:
def getOptInt(req: Req, name: String): ReqVal[Option[Int]] =
  getOpt(req, name) flatMap parseOption(s, parseInt _)

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

Итак, следуя простому паттерну значения на вход - Validation на выход мы получили простой в реализации и использовании модуля для типобезопасного извлечения данных из параметров  HTTP запроса. Он может быть легко расширен как добавлением новых поддерживаемых типов (ну например пишем парсер дат какого-то формата и получаем тривиальное в реализации расширение модуля для работы с датами).

Он не идеален, в нём есть очевидные места дублирования (например необходимость набивать новое семейство функций getX, getOptX для каждого нового типа, да и от обязательности/необязательности значения кажется можно абстрагироваться), однако для затраченного количества умственных усилий мне кажется неплохо.

Для того, чтобы получить более правдоподобную картину давайте придумаем тестовый пример для этого модуля... Скажем запрос на поиск поста в блоге по названию (параметр запроса "text"), с опцией сортировки по дате публикации (параметр "sort") и указанием ограничения на количество результатов в ответе (параметр "limit").

И пусть у нас уже есть метод которые делает что надо в БД и метод сериализующий список результатов для передачи по HTTP, как-то так:
case class Post(...)

sealed abstract class Order
case object Asc extends Order
case object Desc extends Order

def searchPosts(text: String, order: Option[Sort], limit: Int): ReqVal[List[Post]] = {...}

def serialize(ReqVal[Object]): Response = {...}

Давайте подумаем что нам нужно чтобы соединить вместе вышележащую библиотеку для работы с сетевым протоколом и нижележащую функцию работы с БД. Кажется почти всё есть, вот только надо научиться вытаскивать параметр сортировки... А чем порядок хуже числа?
def parseOrder(value: String): ReqVal[Order] =
  value match {
    case "asc"  => Success(Asc)
    case "desc" => Success(Desc)
    case _      => Filure("Illegal order value "+value)
  }

def getOptOrder(req: Req, name: String): ReqVal[Option[Order]] =
  getOpt(req, name) flatMap parseOption(s, parseOrder _)

А теперь можно и написать требуемый код склейки, извлекаюий параметры из запроса и передающий их в функцию работы с БД:
def handleSearchPosts(req: Req): Response = 
  serialize(
    for {
      text  <- get(req, "text")
      order <- getOptOrder(req, "sort")
      limit <- getInt(req, "limit")
      data  <- searchPosts(text, order, limit)
    } yield data
  )

Вот и всё с первой частью моего введения. Далее я покажу как с помощь всё того-же класса Validation можно организовывать не только цепочки проверок до первой ошибки но и аккумуляцию нескольких возможных ошибок. Очень надеюсь, что он потребует меньшего времени.

среда, 2 ноября 2011 г.

Опыт работы с Lift: часть 2

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

Проблема с локализацией

В один светлый день функционал проекта показался близок к полному а архитектура к устоявшейся. В этот день было решено провести нагрузочное тестирование, чтобы понять как скоро станет плохо, если проектом кто-то начнёт пользоваться. Идея была с энтузиазмом подхвачена и воплощена в жизнь. К сожалению результат оказался не очень радужным. Сервер, использующий отличную кэширующую БД, написанный на замечательном, масштабируемом языке и использующий топовую EC2 конфигурацию оказался способен переварить не более 20 запросов в секунду (наиболее тяжёлых из возможных надо заметить).

Обдумывая возвращение и прекрасного мира стартапов в кровавый энтерпрайз я начал применять все подручные средства для поиска причин такой небодрой работы сервера. Первым попавшимся тулом был munin, отчёты о загрузке CPU если конкретно. Он был весьма обнадёживающим: загрузка была около 15% на сервере и 5% на БД. После недолгой медитации на эти отчёты мне пришла гениальная идея: погрепать код на слово "synchronized". Это правда ничего не дало, но немного подняло мораль.

Следующим шагом стало настроить на сервере доступа к JMX и воспользоваться им для подвода информации в VisualVM. И это был частичный win. Кусочек кода внутри Lift (ладно, не точно этот, в том момент использовался Lift 2.2, но его исходники кажется остались только в виде архивов) пользовался дикой популярностью среди заблокированных потоков. Собственно выбора особо небыло, все обращения к S.? были заменены на свежедобавленный ResourceBundle.

Потребление памяти

Ещё одним вопросом который меня долго занимал было куда сервер девает память. Дело в том, что под максимальной загрузкой он выедал все отведённые 4GB примерно за 3-4 секунды. Признаков того, что именно это ограничивало производительность не было, но любопытство не давало покоя и выкроив пару свободных часов я занялся поиском причин.

Для поиска я воспользовался уже послужившим добру VisualVM, точнее его профайлером памяти. Это такая замечательная штука, которая которая при каждой аллокации памяти записывает кто это сделал (ну точнее с некоторой вероятностью записывает, чтобы приложение хоть как-то могло ползать). При этом умеет аггрегировать результаты в своеобразное дерево пожирателей памяти. В общем лепота.

Результат был достаточно предсказуем - больше половины занимали вариации append из StringBuilder и скальный :: (конструктор списка то есть). Ещё подозрительно близко к вершине обретался некий "scala.text.DocCons", но на тот момент должное внимание ему уделено не было. Беглый осмотр самых толстых веток того самого дерева пожирателей памяти показал, что все они растут из метода JsonAST.render.

Для того чтобы понять причину пришлось проследить нелёгкий путь, который переживает объект перед тем как отправиться к клиенту в виде Json. Всё обилие вариантов представлено на картинке в докуметации. Наша ветка - от "case class" до "string". Хм... две промежуточные формы представления. Кажда требующая создание своей копии данных. (Ну не совесм своей, строки ре-используются и то не всегда - эскейпинг вынуждает посимвольно копировать строки на стрелке JSON AST -> Document). Практически двухкратный оверхед.

Отдельно стоит рассказать про scala.text.Document, которую я тогда попутно для себя открыл. Это мини-библиотека для форматирования текста отступами. Кажется нигде, кроме как в собственных исходниках она не описана. Очень простая и компактная (один файл, 120 строк). Как ей пользоваться очевидно из исходников, если не до конца очевидно можно посмотреть на всё тот же render. Единственная её проблема - это выделение памяти на каждый чих, что впрочем и не удивительно - красиво форматируют обычно для людей, как следствие не гигабайтами.

Не буду приводить все рассчёты, но у меня оказалось, что паразитные копии строк + AST из пакета scala.text были повинны примерно в 20% потребляемой памяти. Подумываю о замене всей этого хозяйства на прямое формирование строк, при случае естественно. Кроме того этот способ более не является рекомендуемым в Lift и ему предложены некоторые альтернативы.


Мораль

Для начала надо отметить, что это всё-таки не столько проблема, сколько особенность Lift'а. Он создан для поддержки AJAX-COMET сайтов, но не сервисов для массовой перекачки данных. Вывод прост: надо брать простые инструменты сквозь которые видно что происходит. Ещё полезно изучать не только тех. документацию, но и мысли авторов относительно области применения их детищ. Эту область хорошо сопоставлять со своей задачей и делать выводы.

воскресенье, 30 октября 2011 г.

SO и Habr - поверхностное сравнение

Довелось недавно поучаствовать в подготовке небольшого перевода на Habr и немного в его поддержке после публикации. В процессе как-то придумалось сравнение со SO. Вот смотрите:
  1. Рейтинг. SO: единая постоянная метрика основанная на качестве контента. Habr: двухкомпонентная система: рейтинг основанный на контенте, но убывающий по времени и карма основанная на "личных качествах", постоянна по времени.
  2. Право публикации. SO: постоянное и неотъемлемое. Habr: даётся по итогам попрошайничества зависит от кармы.
  3. Возможность голосовать за. SO: всегда имеется. Habr: зависит от кармы.
  4. Возможность голосовать против. SO: имеется всегда, но за счёт своего рейтинга. Habr: зависит от кармы.
  5. Продолжительность голосования по топику. SO: не ограничена. Habr: ограничена по врмени.
  6. Оценки комментариев. SO только +, незначительное влияние на рейтинг. Habr: + или -, заметное влияние на рейтинг.
Вот подумалось, вроде 6 мелочей. Но ведь очевидна "заточка" одного под аккумуляцию полезного контента, а другого под массивные срачи и популяризацию кадров вроде alizar'а.

PS А ещё SO хоть и не ограничивает по рейтингу возможность публикации, зато существенно ограничивает возможности по созданию энтропии. Вот я например уже давно мечтаю о 1,5к рейтинга, чтобы наконец уметь создавать новые тэги а то на говнище, в котором я копаюсь постоянно даже тэга не находится.

суббота, 15 октября 2011 г.

Опыт работы с Lift: часть 1

После доклада на ScalaSPB образовалась подборка материалов, которыми грех не воспользоваться. Заодно включу в них то, что никак не вписывалось в доклад: примеры с кодом и философские рассуждения. И начну я пожалуй с наиболее простого и короткого кусочка, а именно опыта общения с Lift.

(Тут нужна небольшая вводная: речь идёт об опыте разработки REST сервиса. Он во-первых заведует преобразованиями между domain model и схемой хранения данных. Во-вторых по ходу дела планирует и выполняет всякие дополнительные задачи вроде отправки оповещений на почту или шаринга контента в facebook.)

Итак, от Lift'a используется практически два модуля. Это поддержка REST-сервисов и сериализация case классов в JSON (что однако тянет с собой всю инфраструктуру работы с HTTP). В работе с ними было три эпизода, которые представляют интерес. Ниже привожу первый из них.

Нестандартная авторизация

Первая проблема, которую пришлось решить это не совсем стандартная схема авторизации, используемая в нашем приложении. А именно: все экспортируемые методы делятся на два класса: приватные и публичные. Приватные защищаются на уровне файрвола от внешнего доступа и к ним на уровне приложения доступ свободный. Публичные доступны всем и соответственно на них висит авторизация. Авторизация не простая а по токену, добавленному одновременно в куку и параметр запроса. Это не что иное как издревле известный метод борьбы с CSRF. Придумать легко, а вот обучить Lift такому поведению потребовало немного нестандартного приёма.

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

Для того, чтобы под неё подстроиться была выбрана следующая стратегия:

  1. Помечать часть путей внутри сервиса, как требующие авторизации требуя для них определённую "волшебную" роль;
  2. Выполнять аутентификацию при наличии токенов путём присваивания текущему пользователю двух ролей: "волшебной" и роли с его идентификатором в качестве имени;
  3. Предоставлять нашему коду абстракцию над предыдущими двумя шагами вида "получить авторизованного для данного метода пользователя". Этот метод изящно реализовывался паттерн-матчингом над списком ролей :)

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

Ну а рассказ про замеченые проблемы с производительностью будет в следующем псто...

четверг, 7 июля 2011 г.

Мосты и самолёты

Недавно наткнулся на занятную статью. Основная её идея состоит в том, что разработка программ не была и никогда не будет инженерным делом. В частности автор критикует тех, кто списывает имеющиеся проблемы на молодость индустрии и возлагает надежды значительны прирост качества и надёжности ПО в будущем. В частности он обращает внимание на то, что программ за последнее время понаписали поболе, чем мостов построили за историю.

Мне не очень понравился аргумент. Строительство крупных сооружений на разведанной местности была одной из первых инженерных задач и решать её научились тысячи лет назад. Конструкции со сложностью в тысячи, редко десятки тысяч элементов (возможно моя оценка для мостов и неправильна, но количество ноликов явно до 6 не доходит) это самая заря технологии, как для инженерного дела "в железе" так и для разработки программ. С задачами современного программирования соизмеримы скорее проблемы проектирования автомобилей и самолётов. Давайте будем честными и прибавим к тем 600 000 мостов все модели транспортных средств хотя-бы. Не получается того-же объёма опыта, что у машиностроения, никак не получается.

В конце статьи есть предложение, просто заставляющее ощутить гордость за профессию и осознать разницу между программой и мостом: "Software development is about operating on a living breathing thing and all the while keeping it alive.". Но ведь вокруг помимо мостов огромное количество искусственных конструкций поражающей сложности и живучести. Вы когда нибудь смотрели в иллюминатор при посадке самолёта? Автор кажется нет :) Иначе бы он видел изгибающееся под скачущими нагрузками крыло и десятки аэродинамических элементов судорожно пытающихся отодвинуть момент сваливания ещё на десяток км/ч... Эта конструкция считается мёртвой? Или она случайно не попала в область рассмотрения?

Странно, что автор идёт в точности по стопам тех, кого критикует. Проводит аналогию разработки софта со старой, очень частной проблемой и доказывает её несостоятельность. Может рассматривая аналогии софтостроения с конструированием пора немного поднять планку абстракции и включить в рассмотрение какие-нибудь механизмы?

среда, 1 июня 2011 г.

Одна переписка

Дейсвующие лица:
P. Молодой, но амбициозный и самоуверенный программист.
HR. Умудрённый опытом а также высшими психологическим и неоконченным менеджерским образованиями специалист по управлению персоналом.

HR: "P, вечер добрый. Вы рассматриваете предлоежния на 3000 по разработке лидом на документум в другие компании? Очень спасибо, HR"

P: "Основной класс задач будет синтез текстов, я так понимаю?"

HR: "сейчас уточню у директора..минуточку"

HR: "•Разработка приложений •Разработка технической документации •Коммуникация с Заказчиком по техническим вопросам вот таку штуку прислали"

P: "Ага, то есть для вашего бота уже и заказчик есть? Что-то мне кажется надо будет много дорабатывать. Думаю это на 4000."

HR: "P, мне вчера позвонил Упячкасофт=) и попросил узнать есть ли ребята , которые работаю в этом направлении. Я просто сейчас ище специалистов в одноклассники - ну заодно вот вам решила написать."

HR: "а что такое БОТА?"

P: "Специальная программа, ктороая симулирует мыслительную деятельность. Проблема ведь в этом, я верно понял?"

HR: "Да нет, просто проект расширили и нужны сильны парни готовые сталть лидами через некоторое время. По деньгам надо торговаться как и везде."

P: "Ясно... Ну в общем передайте директору, что я готов написать бота лучше за 5000."

P: "Да, и спросите 5000 чего заодно."

HR: "хорошо, я перешлю ваш профайл - не возражаете?"

HR: "я в данном вопросе не сильна, но 4000 и 5000 - сейча на рынке таких зп не всем дают=)"

P: "Да да, главное передайте мои слова точно."

P: "Я тоже поделсь вашим предложением со знакомыми, не возражаете?"

HR: "да, но вот 4000 и 5000 - я сомневаюсь что они осилят."

четверг, 5 мая 2011 г.

"Version control with Git"

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

Итак, сабж - книжка о популярной в последнее время системе контроля версий, которая как-то так получилось, что завелась у меня на работе. Сразу могу сказать, что книжка однозначно рекомендуется к прочтению начинающим. Бывалым видимо тоже будет небесполезна (точно не берусь судить), так как приведённой там информации в общем то в сети и не видно.

Автор книги, Jon Loelinger, контрибьютор проекта Git и старательно фокусируется на его внутреннем устройстве и логике, лежащей за тем или иным поведением. Книга весьма радикально отличается по глубине изложения от свободно доступной Pro Git и от полупиратски попавшей ко мне "Getting good with Git" (она правда на полноту и не претендует).

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

Этот подход используется повсеместно практически во всей книге и результат по-моему просто отличный. Те места, которые выглядели сложными и запутанными после других источников делаются абсолютно прозрачными и очевидными. Весь этот ад с удалёнными репозитариями, fetch vs pull, tracking branches и т.д. становится более менее понятен или по крайней мере поддаётся освоению с точечными вычитками документации.

Вообще после книжки осталось стойкое ощущение, что использование Git подразумевает изучение его самого, а не того как с ним работать. Причём подразумевает на уровне самых ранних дизайнерских решений. Кстати, интересно есть-ли инструменты (наборы скриптов?..) превращающие его во что-то вроде Subversion c локально доступной историей но при этом соответственно сниженным порогом вхождения?

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

P.S. С удивлением заметил, что в книжке слово developer заменяется местоимением she. Это что получается: разраб в английском по умолчанию - девочка?

воскресенье, 27 марта 2011 г.

"Java concurrency in practice"

Собственно прочитал, наконец сабж. Книга без сомнения отличная и информации о ней в Сети достаточно, добавить особо нечего. Запишу просто пару субъективных впечатлений.

Книга написана просто и понятно. Материал изложен абсолютно последовательно без ссылок вперёд и с минимальным расстоянием в обратных ссылках. Автор старательно избегает обращения ко всяким примитивам синхронизации, вместо этого старается ввести набор паттернов и идиом безопасной синхронизации. Только в последней главе можно увидеть подробное изложение JMM и несколько трюков напрямую с ней связанных.

Из нового для себя я открыл работу с interrupt и InterruptedException а также паттерны корректной остановки выполнения заданий. Вообще тема останова и отмены задач в Java до сих пор была для меня  не изведана. Познавательно, но местами сложновато.

Единственный замеченный недостаток - это обилие примеров, повторяющих стандартную библиотеку. Хотя автор нигде не забывает вставить примечание, что это пример, а не рекомендуемое решение.

P.S. А еще ко мне приехала новая пачка книжек, в том числе DSLs in Action и Version Control with Git, также на подлёте Programming in Scala. В первую очередь навалился на Git. Очень много новой информации относительно устройства репозитария и принципов работы разных комманд. По подробности изложения несравнимо с "Getting good with Git" и "Pro Git".

    пятница, 11 марта 2011 г.

    Об учебных примерах

    Подготовка к продолжению семинара по Java для OSLL/АУ заставила снова глубоко озадачиться построением учебных примеров на тему программирования (кстати, для кого рассказываю и какой уровень ожидается я так и не понял, с одной стороны там рассказывают какой то реальный rocket science мужики зашкаливающей суровости, с другой стороны позвали тупо меня рассказать про тупо фичи Java). Тут ещё наложился затяжной флейм в одной занимательной рассылке. В этой связи я решил выписать важные для меня критерии качества для учебного примера на тему программирования. Для себя точку отсчёта зафиксировать, да может ещё кому интересно будет.

    1. Доступность. Пример должен быть доступен для понимания с минимумом затрат. Требования к теоретическим познаниям должны быть собраны в явные prequisities. Когда в середине, а то и в примечаниях в конце говорят "Так, а вот тут вам нужно (было) прочитать [1], [2], [3] и вот тот учебник." - материал идёт лесом.

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

    3. Простота. В примере должно так мало сущностей, как это возможно. Очень плохо, когда появляются примеры вроде: "Мы реализуем подсистему логирования с помощью аспектно-ориентированного программирования, но ещё нам потребуется IoC контейнер для включения аспектов".

    4. Изолированность. Пример должен задействовать минимальное количество (в идеале одну) фичей языка и/или техник программирования. Если речь идёт о каком-то последовательном изложении (например введении в ЯП) совершенно недопустимы ссылки вперёд. Граница с пунктом 2 не очень чёткая. Я их разделяю так: простота связана с требованиями к окружению (например "мы внутри IoC контейнера" или "у нас есть парсер JSON"), изолированность связана с инструментарием, привлекаемым непосредственно к решению задачи (например "мы используем обобщённые типы" или "мы используем механизм макросов").

    5. Реалистичность. читатель не должен проводить бессонные ночи ищя ответ на вопрос "зачем?". Знание, не подкреплённое практикой или хорошей идеей как эту практику можно устроить, быстро теряется и затирается чем нибудь более актуальным. Чисто эмпирически мне кажется, что пример должен попадать в одну из следующих категорий:
    а) Часть большего и жизненного примера (например "мы пишем графический редактор и нам нужна унифицированная обработка фигур" - "нас спасёт паттерн композит").
    б) Какое-то решение общей проблемы программирования ("нам нужно абстрагировать вычисление расстояния между точками от представления их в памяти") с которой гарантированно сталкивался каждый.
    в) Общеупотребимый алгоритмом или структура данных (например "решение комбинаторной задачи" или "представление графа в памяти").

    6. Масштабируемость. В идеале пример должен быть доведён до конца в несколько итераций, с целью показать несколько доступных уровней глубины. От простого (задача едва-едва решена) и узкого решения к более сложному (максимальному проходящему по критериям 2 и 3) и полному. Соответственно и при практическом применении рассматриваемого инструмента остаётся возможность подвигать ползунок.

    7. Содержательность. Что-то вроде возможности ответить на вопрос "зачем?" на самом высоком уровне. Пробовал сформулировать по разному, но кажется как не крути получается критерий-солянка:
    а) Не велик. Плохо когда что-то делается, а в конце говорится "на самом деле взрослые дяди это уже решили в стандартной библиотеке и лучше пользоваться их решением". Оставляет смешанное чувство собственной убогости ("действительно годное решение мне не понять?") и низкого качества материала ("то есть то же самое можно сделать лучше?"). Но это всё-равно лучше, чем когда такого примечания нет :)
    б) Связь с каким-то юзкейсом верхнего уровня (очевидно что 6. а) избавляет от этой проблемы в принципе).
    в) ..?

    Надо заметить, что это касается технических материалов для практиков. Научные статьи и обзоры по понятным причинам в эту шкалу не вписываются. А те люди которые легко переживают отсутствие таких примеров без сомнения должны бросать неблагодарное инженерное дело и релоцироваться в класс учёных. С другой стороны я недавно видел пример того, что в умелых руках даже казалось бы безнадёжно сложные и теоретизированные вопросы находят прекрасную иллюстративную основу, для сравнения: куча введений в продолжения от скалохаскелистов [1], [2], [3] и одно от питонщика.

    среда, 16 февраля 2011 г.

    "Структура и интерпретация компьютерных программ"

    Вот я наконец и дочитал эту замечательную книгу. Ну точнее постановил считать её дочитанной, так как предпоследняя глава растянулась чуть ли не на полгода, а последняя обещала и того дольше.

    Процесс и правда изрядно затянулся. Я начал её читать ещё в сентябре позапрошлого (2009) года. И вот только пара недель как закончил. Причина очень проста: книга явно требует подходящего настроения и регулярно читать не получалось хронически. Процесс и сам по себе получился не обычным. Я начал её читать с экрана, в виде английской pdf'ки. Однако, когда я был где-то на середине вышел очередной тираж русского издания и я без раздумий добыл себе бумажный экземпляр на русском.

    И наверно единственный раз не пожалел, что связался с русским переводом. Надо отдать должное переводчикам - это один из лучших технических переводов, что я видел. И без сомнения наиболее качественный из выпущенного в последние годы. Очень тщательный, последовательный подход к терминологии. Авторы всегда дают оригинальное написание вводя новый термин, но далее в книге используют один и тот же русский вариант. Нет ни диких транслитераций, ни странных оборотов в три слова. Я практически прозрачно переключился с английской версии на русскую.

    Ещё важный момент, все примеры в книге выполнены на LISPе (а точнее Scheme). Годика три назад, я наверно по этой причине сделал бы из книжки костер и сплясал бы вокруг него. Однако изучение Smalltalk и Python (а точнее отзывов их авторов о влиянии LISP) в последствии немного сгладили мое отношение к скобочкам. Нельзя не признать, что для первых глав, где демонстрируется куча разных подходов к программированию, он является пожалуй оптимальным выбором. Однако длинные куски кода на LISPе всё-таки страшно выглядят, понимание многих примеров из последних глав отнимало много времени, в основном на вычленение стандартных конструкций из мета-кодировки в скобках.

    Итак, о чём же собственно книга.

    Первая глава, "Построение абстракций с помощью процедур", рассказывает о базовых техниках функционального программирования. В ней вводятся понятие процедуры, её вычисления, объясняется семантика лямбда-исчисления. Также тут рассматриваются "формы" вычислительных процессов, и то какие потребности в памяти и процессорном времени они порождают. В конце показываются функции высокого порядка и лямбда-выражения, с примерами применения.

    Вторая глава, "Построение абстракций с помощью данных", рассказывает о том, что такое данные, как их можно унифицировать (на уровне доступа) с процедурами и почему это бывает важно. Попутно вводится понятие структуры данных и рассматриваются примеры, вроде пар, списков и т.п. Описывается свойство замкнутости для структур данных (программ?), и демонстрируется его важность в дизайне программ. Заканчивается глава по сути кратким введением в ООП, а точнее подробным рассмотрением идеи программирования в "стиле передачи сообщений". Показываются ЛИСПовые техники реализации классов, объектов, методов. Последнее без сомнения лучшее введение в ОО-программирование из всех что я видел. В основном благодаря тому, что мир объектов и сообщений строится вместе с читателем, а не приводится как данность (тут я немного забежал вперёд - достраивается он уже в следующей главе).

    Третья глава, "Модульность, объекты и состояние", вводит в стройный мир асбтрактных вычислений, выстроенный в первых главах, проблемы реального мира: присваивание и время. Примерно половина главы посвящена тому как эти явления аккуратно моделировать в языке программирования, и примерно половина конкретным техникам программирования. В этой главе особо порадовал раздел про потоки, который был прочитан с учётом возвратов из следующей главы и перелистываний на досуге раза три. Совершенно замечательный пример простого решения массы сложных проблем, настоятельно рекомендую для прочтения, даже если за всю книгу лень браться.

    Четвёртая глава, с жутким названием "Метаязыковая абстракция", посвящена созданию интерпретаторов языков программирования. В ней сначала подробно расписывается архитектура и реализация интерпретатора самой Scheme, а затем вносится в него ряд изменений, для поддержки экзотических методов программирования. Меня, в контексте аспирантуры, конечно больше всего заинтересовал логический интерпретатор (пролог в скобочном синтаксисе), который наверно единственный я более-менее до конца понял.

    Ну и на пятую главу, "Вычисления на регистровых машинах", я забил :)

    Надо отметить, что книга доставила массу удовольствия в процессе прочтения. Нельзя сказать, что всё написанное там было для меня откровением, но этот материал копился у меня в голове годами, из обрывков статей, блогпостов и собственного печального опыта... В общем стоило ей попасть мне в руки курсе на четвёртом.

    Вообще книга заставила много раз подряд проклясть родной универ. Всё-таки способность отечественных преподавателей сделать из даже самых интересных и полезных знаний невнятную, серую, трудноперевариваемую массу просто поражает. Вспомнился и курс функционального программирования, двухмесячный, с быстрым рассмотрением синтаксиса и стандартных функций, какого-то древнего ДОСовского ЛИСПА, увенчанный курсовиком, в котором нужно было сделать базу данных то ли студентов на кафедре, то ли телевизоров на складе. Курс, на котором понятие неизменяемой структуры данных даже не звучало, не говоря уже про то, чтобы объяснить выученному на паскале потоку как с ними работать. И курс объектно-ориентированного программирования, без единого упоминания о передаче сообщений.

    Ну да ладно, напоследок, по традиции, критическое замечание к сабжу. В последних главах очень мало диаграмм к сложным примерам кода. Хорошее именование и форматирование это конечно хорошо. Но всё-таки картинки - лучший способ доносить архитектуру до умственно не далёких читателей вроде меня.

    воскресенье, 13 февраля 2011 г.

    Из рассылки одного Open source проекта

    Q: ... What is the purpose of this group, given the existence of stackoverflow.com? Is there some advantage I am perhaps missing? ...

    A: I believe here we ask serious philosophical questions and get serious philosophical answers. At least we are trying to.

    P.S. Нашел тут случайно в черновиках. Скопипастил недели две назад, да забыл опубликовать... Но кажется эта цитата из нестареющих.

    четверг, 27 января 2011 г.

    Немного поупражнялся в рефакторинге

    Пару дней назад увидел пост на крайне актуальную тему в одном занимательном блоге. Суть: автор увидел кусок говнокода на Scala, переписал в цивильном виде, сделал аналог переписанного на Java и сравнил. По итогам сомнения он пришёл к выводу что разница в 30 строк кода из 100 не существенна для выбора языка, а идейной разницы никакой нет.

    Надо сказать, что пост задел практически за живое, ибо совсем недавно я занимался валидацией запросов и авторизацией пользователей как раз на Scala :) Так что что мне сразу бросилась в глаза пара некорректных мест в сравнении, которые я и попробую дальше подробно описать.

    Во-первых, утверждается, что два текста программ, приводимых в конце, одинаковы. И один имеет не лучшую надёжность, чем другой. Это просто не верно. Дело в том, что несмотря на очень похожую структуру Scala-код имеет на несколько порядков большую "защиту от дурака". Для обеспечения сравнимого уровня пассивной защиты от ошибок на Java требовалось-бы написать как-то так:
    @rest.Method(httpMethod = Array(POST)) 
    public void login(final rest.Request request, 
          @rest.Param(name = "login") @Nullable final String login, 
          @rest.Param(name = "password") @Nullable final String password) 
    { 
       debug("accessing login... " + login); 
    
       if( login == null || password == null ) 
          respond(UNAUTHORIZED, "You should provide login and password"); 
       else 
          doLogin(request, login, password); 
    } 
    
    private void doLogin(@NotNull final rest.Request request, @NotNull final String login, @NotNull final String password) 
    { 
       final UserInfo user = AccountsStorage.find(login); 
       if( user == null ) 
          handleUnknownUserLogin(); 
       else 
          (new KnownUserLoginHandler(request, login, password, user)).handle(); 
    } 
    
    И прикрутить сверху проверку статическим анализатором кода. Говорить о читабельности кода после такого преобразования не приходится.

    Во-вторых приводится аргумент, что разработчик, с учётом возможностей современных IDE будет руками писать кода Java ничуть не больше(Справедливости ради стоит заметить, что скорее всего даже меньше). Аргумент не очень хороший, в контексте того что код ведь обычно не столько пишут, сколько читают. У меня на строку написанного кода приходится минимум сотня прочитанного, и с пять сотен бегло просмотренного. Во время работы каждый программист возвращается к ранее сделанному много раз и шум вроде повторения списка аттрибутов класс четыре раза в разных обрамлениях мягко говоря не помогает понять свою или чужую мысль.

    В-третьих, в примере используется внешняя библиотека (для работы с http судя по всему) совершенно не приспособленная к языку и не использующая его возможностей. Как на образец удачной, основанной на полноценном использовании языка библиотеки можно посмотреть например на Circumflex Web Framework.

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

    Итак, что мне не нравится в предложенном решении и что я собираюсь улучшить:
    1. Сквозная связанность всего кода авторизации с классами внешней библиотеки. Не то чтобы переезды целых приложений с одного фреймворка на другой являются обычным делом, но когда они всё-таки происходят...
    2. Весь алгоритм по сути выполняет простой выбор: авторизовать пользователя или выдать сообщение о том, почему в авторизации отказано. В предложенном решении для того чтобы это понять требуется отсмотреть всю сотню строк кода, а исключить другие (ошибочные) варианты реакции весьма затруднительно.
    3. Сам алгоритм авторизации имеет преимущественно линейную структуру:
      • проверить, что логин и пароль предоставлены;
      • найти соответствующие учётные данные в БД;
      • проверить, что пользователь имеет право на вход;
      • аутентифицировать по паролю.
      Несмотря на это код с самого начала начинает ветвиться, полностью скрывая основную идею.

    Приступим. Вся процедура авторизации имеет своим результатом или учётные данные авторизованного пользователя, или сообщение об ошибке. Так и запишем:
    type LoginResult = Either[UserInfo, String]
    def Success[T, E] = Left[T, E] _
    def Failure[T, E] = Right[T, E] _
    

    Сама процедура логина получает возможно предоставленные данные, и возвращает описанный выше результат:
    def login(login: Option[String], password: Option[String]): LoginResult = ...

    Теперь можно отвлечься от деталей процедуры логина и заняться обработкой результатов
    @rest.Method(httpMethod = Array(POST))
    def login(request: rest.Request,
        @rest.Param(name = "login")
        loginParam: String,
        @rest.Param(name = "password")
        passwordParam: String): Unit =
      login(Option(loginParam), Option(passwordParam)) match {
        case Left(user) =>
          request.session("user") = user
        case Right(message) =>
          respond(UNAUTHORIZED, message)
      }
    
    Здесь важно во-первых что количество вариантов внешней реакции системы явно ограничивается. Во вторых, что на этом уровне заканчиваются все связи с фреймворком, включая возможные null'ы в параметрах вызова (именно по этому я не стал делать процедуры, аналогичной setupSuccessfulAuthResult - она просто не создавала бы никакой полезной абстракции).

    Дальше хочется заняться собственно процедурой логина, но стоит снова оставить её в стороне и описать все шаги авторизации. Очевидно, что каждый из шагов будет иметь то-же результат, что и процедура логина в целом, однако будет требовать разных данных на вход. Можно записать каждый шаг в виде отдельной функции:
    def findUser(login: String): LoginResult =
      AccountsStorage.find(login).toLeft( "User not found" )
    
    def checkUser(user: UserInfo): LoginResult =
      if (user.inactive) Failure("Account is inactive")
      else Success(user)
    
    def doLogin(user: UserInfo, login: String, password: String): LoginResult =
      if (user.authScheme == "PETRIVKA")
        handlePetrivkaAuthSchemeLogin(user, password)
      else
        handleUsualAuthSchemeLogin(user, login, password)
    
    def handlePetrivkaAuthSchemeLogin(user: UserInfo, password: String): LoginResult =
      if( user.passwordMatches(password) ) Success(user)
      else Failure("Authentication failed")
    
    def handleUsualAuthSchemeLogin(user: UserInfo, login: String, password: String) =
      AccessStorage.access.auth_configs.find(_.key == user.authScheme) match {
        case Some(scheme) =>
          log.debug("authenticating with " + scheme.command)
          val exec = Runtime.getRuntime.exec(
              scheme.command replace("{login}", login) replace("{password}", password))
          if( exec.waitFor == 0 )
            Success(user)
          else
            Failure("Authentication within " + scheme + " failed")
        case None => Failure("Unknown authentication scheme: " + user.authScheme)
      }
    
    Если немного помедитировать на handleUsualAuthSchemeLogin, то наверняка можно её сократить и упростить, но это мало повлияет на основную идею решения.

    Теперь осталось самое простое - собрать все шаги вместе. Совершенно случайно в Scala завалялась подходящая конструкция :)
    def login(login: Option[String], password: Option[String]): LoginResult =
      for (login <- login.toLeft( "You should provide login" ).left;
           password <- password.toLeft( "You should provide password" ).left;
           user <- findUser(login).left;
           checkedUser <- checkUser(user).left;
           loggedUser <- doLogin(checkedUser, login, password).left
      ) yield loggedUser
    
    Не вдаваясь в детали, скажу что выражение for делает очередной шаг и проверяет результат: если он успешен то продолжает цепочку, если неуспешен то прерывает цепочку, возвращая неудачу. То какой вариант сейчас считается успешным мы сообщаем в конце каждого выражения, я соответственно всегда считаю успешным левый. Метод toLeft у класса Option преобразует его в Either, говоря куда помещать существующее значение и чем заменять несуществующее.

    Вот и всё. Данный пример исправляет отмеченные ранее недостатки, при этом имеет более простую (7 элементов против 9) и близкую к задаче структуру. Также стоит добавть, что он имеет и существенный недостаток: двойную терминологию. В одних местах используется пара Success-Failure, в других Left-Right. Однако это имеет и положительный эффект - интерпретация результатов всегда отличима от их создания.

    Вот полный исходник с кое-какими моками для компилябельности.
    class LoginDemo {
    
      type LoginResult = Either[UserInfo, String]
      def Success[T, E] = Left[T, E] _
      def Failure[T, E] = Right[T, E] _
    
      //@rest.Method(httpMethod = Array(POST))
      def login(request: rest.Request,
          //@rest.Param(name = "login")
          loginParam: String,
          //@rest.Param(name = "password")
          passwordParam: String): Unit =
        login(Option(loginParam), Option(passwordParam)) match {
          case Left(user) =>
            request.session("user") = user
          case Right(message) =>
            respond(UNAUTHORIZED, message)
        }
    
      def login(login: Option[String], password: Option[String]): LoginResult =
        for (login <- login.toLeft( "You should provide login" ).left;
             password <- password.toLeft( "You should provide password" ).left;
             user <- findUser(login).left;
             checkedUser <- checkUser(user).left;
             loggedUser <- doLogin(checkedUser, login, password).left
        ) yield loggedUser
    
      def findUser(login: String): LoginResult =
        AccountsStorage.find(login).toLeft( "User not found" )
    
      def checkUser(user: UserInfo): LoginResult =
        if (user.inactive) Failure("Account is inactive")
        else Success(user)
    
      def doLogin(user: UserInfo, login: String, password: String): LoginResult =
        if (user.authScheme == "PETRIVKA")
          handlePetrivkaAuthSchemeLogin(user, password)
        else
          handleUsualAuthSchemeLogin(user, login, password)
    
      def handlePetrivkaAuthSchemeLogin(user: UserInfo, password: String): LoginResult =
        if( user.passwordMatches(password) ) Success(user)
        else Failure("Authentication failed")
    
      def handleUsualAuthSchemeLogin(user: UserInfo, login: String, password: String) =
        AccessStorage.access.auth_configs.find(_.key == user.authScheme) match {
          case Some(scheme) =>
            //log.debug("authenticating with " + scheme.command)
            val exec = Runtime.getRuntime.exec(
                scheme.command replace("{login}", login) replace("{password}", password))
            if( exec.waitFor == 0 )
              Success(user)
            else
              Failure("Authentication within " + scheme + " failed")
          case None => Failure("Unknown authentication scheme: " + user.authScheme)
        }
    
      def respond(code: Int, message:String = "") = {}
    
      val UNAUTHORIZED = 401
    }
    
    class UserInfo {
      val inactive = false
      val authScheme = ""
    
      def passwordMatches(pwd: String) = true
    }
    
    object AccountsStorage {
      def find(login: String): Option[UserInfo] = None
    }
    
    object AccessStorage {
      object access {
        object auth_configs {
          def find(pred: {val key: String} => Boolean): Option[{val command: String}] = None
        }
      }
    }
    
    package rest {
      class Request {
        val session = scala.collection.mutable.Map[String, Any]()
      }
    }
    

    четверг, 13 января 2011 г.

    Провёл семинар по Java

    В начале декабря проводил семинар для пары студентов из OSLL про Java. Так как народ в основном ковыряет железки больше что-то не нашлось кому рассказать у них про Java. Вот я и вызвался. Ораторские неспособности потренировать, да для себя какой-нибудь забавный фактик отрыть.

    Идея провести семинар (точнее взяться наконец и провести, ибо тема семинара уже давно в их виш-листе висела) пришла ко мне давно. Соответственно кое какие мысленные подготовительные этапы я прошёл. Хотелось поменьше рассказать про язык (до него благо хоть от ++ хоть от # полшага сделать) и по больше про виртуальную машину, библиотеки, то что Sun при жизни именовал экосистемой.

    Наученный горьким опытом недавней подготовки лекции про RDF для семинаров лаборатории (там была масса "ощущений" и последующих выводов, надо бы тоже отписаться) я начал готовиться за 3 недели. Сел, бодро накидал план в 4 пункта:

    1. Устройство платформы. Ну понятно VM, байт-код, компилятор, библиотеки.
    2. Пример простой программы. Тут помимо куска кода хотелось ещё скомпилировать и потом рассмотреть декомпилятором что получилось. Основная цель - изгнать из сознания слушателей мысли о волшебстве в байт-коде и работе JVM.
    3. Обзор языка. Быстро, чтобы только понять о чём речь. Класы, интерфейсы, методы и аттрибуты, примитивные типы.
    4. Собственно обзор "мира". Библиотеки, фреймворки, ну и моё маленькое увлечение - другие языки.

    Первый и второй пункты написал быстро, вечера за три. Дальше стало резко хуже. Когда что-то используешь особо не задумываешься. Но когда надо заключить это в текстом и потом рассказать свежему человеку понимаешь насколько там на самом дохрена всего. Чуть больше года назад я писал небольшую вводную статья о Scala на хабр, там было и правда легко - язык я знал чуть, "особых случаев" там поменьше, да и в глаза они особо не бросались. А в Java ведь так и не знаешь с какого конца браться. "Пишем в класса public static void main" - какой класс? Откуда он взялся? Откуда не начни приходишь к класслоадерам и мониторам. Ладно, кое как разложил, упорядочил, докинул в начальный план дженериков, а то как-то совсем убого выглядела старушка.

    Ещё был неприятный вопрос с примерами. С одной стороны рассказывать о конструкциях языка махая в воздухе руками как-то странно. С другой стороны пространные синтаксические конструкции не располагают к выписыванию и запоминанию. Кроме того привод пример программы надо как-то объяснять что она делает. В общем покрутив и так и сяк план, понял, что стоит вернуться к классической схеме. А именно сначала описание языка и синтаксических конструкций с кусочками кода (как ни старался не получается их делать содержательными). Потом пример, декомпиляция. В итоге 2 и 3 пункт поменялись местами.

    К моему великому сожалению часть про язык всё-таки разрослась на большую часть семинара, зато получилось почти полноценно: рассказал про классы и интерфейсы, модель наследования и перегрузки, простые случаи дженериков, конструкторы и инициализаторы, статические члены классов и интерфесов.

    Последним кусочком был обзор мира, начальный план был обширен:

    1. Бибилиотеки/фреймворки
      1. JEE
      2. Spring
      3. Apache-commons
      4. OSGi
    2. Инструменты
      1. Ant
      2. Maven
      3. IDEs (Eclipse, IDEA, Neteans)
    3. Языки
      1. Groovy
      2. Scala
      3. Clojure

    Правда в процессе подготовки быстро стало понятно, что в регламент времни с такой программой вписаться без шансов. В результате пошли под но OSGi и все инструменты кроме IDEs.

    Ну затем и пришёл день события. Из человек 6 отписавшихся о желании слушать по факту пришло 2 :) Я про себя от души посмеялся, но всё-таки количество народу полностью развязало мне руки в плане скорости изложения и объёма. Надо заметить, что оба имели хорошую вводную по Java и кажется уже изрядно программировали на ++. Так что я как мог навалился на рассказ про архитектуру платформы. Язык прошёл легко, большая часть вещей была народу знкома, действуя по ситуации, я пару раз заворачивал вглубь: немного сказал про ограничения в дженериках(<T extends Iterable>) и анонимные классы. Явный промах был только в части обзоров. Уже в процессе я понял, что идея рассказывать про библиотеки без юзкейсов и примеров кода - порочна по определению. Однако даже этот кусочек не совсем пропал - идея Inversion of control кажется всё-таки нашла понимание в массах. Ну и естестенно я как мог с запалом рассказал про Groovy и Scala, перечислил длинный список плюшек. По Groovy набросал примеров на поиски и выборки из коллекций. Из Scala привёл пример quicksort а-ля Haskell (поставил я тут, кстати, эксперимент на сортировку 1 000 000 интов - всего в три раза медленнее нативного на массивах, кажется в Швейцарских университетах практикуют чёрную магию). Ну и вычисление ряда Фибоначи на скобочках естественно =)

    Ну и напоследок я спросил нужно ли продолжение и ответ был положительным. Так что постараюсь по весне сделать либо что-то поглубже про Java (это если Блох приедет и успею прочитать) либо (что привлекает больше и будет иметь больше практического фундамента к тому времени) краткое введение в Groovy и Scala. Ибо как говорит один английский профессор - "лучший способ разобраться в чём-нибудь - прочиать по этому курс".

    PS вот моя недопрезентация к семинару, в основном веслые картинки: http://dl.dropbox.com/u/1776995/pres.pdf