С нового года осилил ещё пару книг. Отчитываюсь одним постом, чтобы не увеличивать энтропию.
Первая, "Programming Erlang" - классическое введение в язык программирования от автора. Очень краткая, очень содержательная книга. Где-то в сотне страниц рассматривается сам язык, ещё где-то в сотне страниц описываются основные техники применения и важные библиотеки. В заключении рассматриваются подходы к решению практических задач: работа с вводом-выводом, распределённое и параллельное программирование, защита от сбоев.
Книга подкупает скорость и глубиной изложения. По большому счёту со всеми упражнениями её можно прочитать за неделю. Прогресс который читатель делает по ходу прочтения потрясает - где-то на 200 странице он уже, вместе с автором, пишет обобщённый сервер с горячей заменой кода и экспортом функционала по сети (наколенный gen_server). Помнится Одерски на 200 странице ещё разбирался с объектами...
Простота и доходчивость также на высоте. Автор мастерски маневрирует между сложными концепциями передачи сообщений, процессов, очередей, сетей, синхронизации, надёжности и всего прочего. Все проблемы ставятся и решаются по одной оставляя у читателя лёгкое ощущение собственной божественности. Маленький совет для читающих: после главы про IRC чат не поленитесь прочитать сразу и приложение D. Очень очень хорошо дополняет.
Несмотря на все старания не смог найти в книге заметных недостатков. Автор задаёт очень, очень высокий стандарт для вводных книг по языкам/технологиям. Трудно что-либо с ним сравнивать, разве что JCP (естественно со скидкой на сложность топика).
Вторая книга "Я - математик" - это автобиография Ноберта Винера, посвящённая его научной карьере. Книга очень интересна анализом сути научной работы и её связи с прикладными задачами а также передачей настроений и мыслей учёных "того самого" поколения. В смысле сделавшего большинство открытий, которыми принято объяснять поразительный прогресс человечества за прошлый век. Автор тщательно передаёт характеры и стиль общения учёных того времени, показывает их отношение к политике и экономике, место в обществе.
Честно говоря я немного побаиваюсь пересказывать мысли изложеные в этой книге в силу возможного неверного истолкования мной и призываю прочитать её самостоятельно. Однако для привлечения внимания поделюсь парой кое-как осознанных идей. Во-первых меня, как инженера, подкупила позиция Винера относительно прикладных исследований и практических задач. Он уверен в том, что связь с реальным миром может принести любой научной работе не только ценность и признание, но и красоту, и завершённость. Он демонстрирует это множество раз, подчёркивая во-первых тот физический смысл который он находил в математических концепциях с во-вторых называя конкретное приложение своих результатов в технике и, иногда, других работах.
Во-вторых очень интресен его взгляд на науку как на коллективный труд, двигаемый вперёд не достижениями одиночек, но прогрессом всего сообщества. Он придаёт большее значение общению и кооперации, связям и кругозору чем единоличному таланту и затяжному "копанию" в одну сторону. Это кстати совпадает с недавно сформировавшимся у меня мнением, что научных успех имеет в основном социальную природу (и что в науку мне идти не стоит).
В общем книга вполне достойна прочтения и её можно рекомендовать как последний мотиватор, если мучает вопрос идти в науку или нет. Также стоит отметить, что перевод делался в 50е годы, то есть до тотального вымирания научных переводчиков. Так что язык и стиль в книге на высоте, о которой тем-же "Пионерам программирования" можно только мечтать.
воскресенье, 11 марта 2012 г.
среда, 18 января 2012 г.
"Programming in Scala"
Незадолго до нового года я закончил читать "Programming in Scala", написанную частично самим автором языка. Первое что стоит отметить это внушительный размер труда - более 800 страниц. Что в принципе понятно, язык совсем не маленький. Во-вторых книжка даёт более чем достаточное для самостоятельной жизни представление о языке. После неё можно брать и начинать писать рабочий проект.
Основным достоинством книги является наличие нескольких полноценных законченных примеров. Начиная с классической программы симуляции дискретных событий (угадайте откуда взятой) и заканчивая примером табличной системы. При этом фокус этих примеров плавно смещается с демонстрации отдельных фич языка на исследование взаимодействия нескольких особенностей и приёмов в одном проекте. Это оставляет весьма приятное впечатление, и, я думаю, должно позитивно сказываться на уверенности в новом инструменте у начинающего скала-программиста.
Если говорить о структуре книги, то она проста и очевидна, от базовых элементам к сложным, от стандартных функций первой необходимости к обзору больших кусков библиотеки. Несмотря на обилие ссылок вперёд спланированы они более чем грамотно и при последовательном прочтении книги необходимости перейти по ним не возникает.
Авторы не пытаются залезть очень глубоко, здесь не обсуждаются эзотерические возможности системы типов и проблемы ленивого ввода-вывода. Однако ряд сложных топиков освещён более чем добротно. В частности великолепны главы про парсер комбинаторы и устройство стандартной библиотеки коллекций (она без сомнения является лучшим гайдом по сабжу, который я видел).
По началу мне было несколько скучно читать (даже несмотря на осознанные пролистывания очевидных топиков), но по мере появления больших примеров и погружения в более тонкие моменты языка я втянулся. Вторая половина книги поглотила раза в 4 меньше времени чем первая. Уже знакомому с языком человеку видимо стоит иметь эту особенность в виду. В общем книга трудно назвать блестящей, однако ни малейших технических претензий к ней нет, это твёрдое 4.
четверг, 15 декабря 2011 г.
Введение в Validation: часть 2
В прошлой статье я постарался рассмотреть наиболее простой подход к использованию класса Validation (искушенный читатель заметит, что не только его). Помимо этого, весьма прямолинейного, подхода к комбинированию значений внутри Validation у нас есть второй, несколько более хитрый. Именно из за него scalaz когда-то и проник в мой проект.
Основная идея второго подхода состоит в том, что если мы можем как-то агрегировать ошибки, то выполняя операции над завёрнутыми в Validation значениями можно автоматически это делать в случае обнаружения нескольких ошибок во входных данных. В отличии от ранее рассмотренного стиля, где ошибки вытаскиваются по одной, мы получаем стиль обработки (и соответственно извлечения из внешнего мира) ошибок "всё и сразу".
Именно в таком сценарии проявляется преимущество всех этих сложных штук, перед "проверенными временем" исключениями. У вас бывали в проекте классы "CompositeException" со списком других исключений в одном из полей? А милые кусочки кода, которые пробовали что-то делать, складывали ошибки в список и потом выбрасывали его в этой незатейливой оболочке? Тогда этот пост для Вас.
scalaz даёт нам операцию "собрать значения и если они успешны применить к ним функцию, а если нет - сагрегировать все возникшие ошибки". Реализуется она созданием временного объекта, который представляет собой выборку успешных данных. Эта выборка может быть преобразована применением к ней обычной функции (в смысле принимающей и возвращающей простые значения). Применение это похоже на обычный map, в том смысле, что при успешных входах на выходе появляется результат применения к ним функции. Но отличается тем, что оперирует сразу над набором значений и знает что делать с несколькими неудачами.
В scalaz для доступа к этому функционалу применяется без сомнения согревающий сердце истинного ценителя математики оператор |@|. Результатом его применения к паре завёрнутых в Validation значений является тот самый промежуточный объект в который мы можем или продолжить добавлять значения с помощью того же оператора или применить к нему функцию соответствующей арности.
Не смертельно сложно, давайте решим простую задачку: пусть теперь мы ходим, чтобы наш код из предыдущего поста теперь не вытаскивал и проверял параметры по одному а брал и проверял всё сразу. Таким образом, чтобы напортачившему клиенту приходил сразу весь пакет его ошибок и он мог над всеми сразу поработать.
Это решается почти тривиально, однако для начала стоит немного абстрагироваться от деталей устройства запроса, для того чтобы переход был проще. Для этого воспользуемся старой доброй инкапсуляцией: соберём весь набор параметров в один класс и сделаем отдельную функцию для преобразования запроса в экземпляр этого класса:
Неплохо, наша программа несколько прибавила в кохесии, а основные умственные усилия были потрачены при удалении отступов после копипаста. Теперь пора притворить в жизнь задуманное. К сожалению сразу сделать это немного затруднительно. Дело в том, что для старого класса ошибок у нас нет разумного способа их объединять. Но это легко, исправить. Пусть в случае неудачи вместо одной ошибки у нас будет список ошибок. Да собственно библиотека нам даёт готовый такой тип, надо просто им воспользоваться.
А всего остального кода сделанное не касается. Основная проблема - это места, где создаются экземпляры ошибок. Они имеют теперь новый тип и должны создаваться иначе, с помощью метода failNel добавляемого ко всем объектам при импорте scalaz автоматически. К счастью с успешными результатами вывод типов вполне может управится сам.
Обратите внимание, мы не можем (и определённо не хотим) втянуть в мир "всего и сразу" операцию обращающуюся к БД. Но миры "шаг за шагом" и "всё сразу" совершенно интероперабельны. Как только что было показано, мы всегда можем выбрать удобный и даже потом поменять решение, чтоб так реализации SOAP жили.
Ну и для полноты картины можно привести пример с композицией. Давайте снова уроним на свои головы новый функционал. Пусть наши пользователи захотели смотреть не первые N результатов поиска, а все сколько их есть. С издавна на руси такую задачу решали пагинацией. И вот на смену отслужившему свое параметру limit приходит пара новых: slice_start и slice_end. И для обеспечения приемлемой производительности мы решили ограничить размер страницы 50 результатами.Основная идея второго подхода состоит в том, что если мы можем как-то агрегировать ошибки, то выполняя операции над завёрнутыми в 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 автоматически. К счастью с успешными результатами вывод типов вполне может управится сам.
Обратите внимание, мы не можем (и определённо не хотим) втянуть в мир "всего и сразу" операцию обращающуюся к БД. Но миры "шаг за шагом" и "всё сразу" совершенно интероперабельны. Как только что было показано, мы всегда можем выбрать удобный и даже потом поменять решение
Для начала как всегда подправим наши типы:
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 контейнеров.
Ярлыки:
achtung code,
programming,
scala,
scalaSPB,
scalaz,
validation
вторник, 8 ноября 2011 г.
Введение в Validation: часть 1
В этом посте я хочу подготовить почву для будущего рассказа о подходе к обработке ошибок, который как ни крути получился весьма скомканный без должной политической подготовки аудитории. Его темой будет класс Validation из библиотеки scalaz, некоторые (далеко не все) техники работы с ним и отдельные языковые конструкции, которые я нашёл наиболее полезными.
Этот класс по сути объявлен следующим образом:
Во-первых у него объявлены методы map и flatMap. В scala это означает, что мы можем использовать экземпляры этого класса (на самом деле trait, но как это блин по-русски сказать то?) в выражении for. Например так (это переписанный код метода login из моего давнего поста на эту же тему):
Теперь очередь магии внутри выражения for. <- в данном контексте работает как "Если у нас справа успешное вычисление связать имя слева с его результатом, если неудача - вернуть его как результат всего выражения." А yield в свою очередь работает как "Если все связывания были успешны вычислить выражение и вернуть его как успех". Обратите внимание, выбор всё ещё тут, но он спрятан в способе которым выражение for комбинирует свои элементы.
У Either методов map и flatMap нет, есть они только в его "проекциях" (правой и левой). На практике это значит, что использование Validation практически постоянно экономит нам 5 символов. Сравните код выше с кодом из моего старого поста.
Важно понимать, что этот подход позволяет унифицировано обрабатывать целый ряд проблемных ситуаций: (а) ошибку во входных данных (отсутствие пароля), (б) собственно отказ в обработке основанный на логике (пароль не верен) и (в) ошибку во внешней службе (БД легла в момент сверки пароля). При этом код верхнего уровня даже не упоминает никакие Failure, только использует <- вместо =.
Помимо приятного синтаксического сахара для сокрытия if'ов от нашего взгляда, такой подход к передаче ошибок обладает одним важным свойством - отдельные методы следующие паттерну "значения на вход - валидации на выход" могут легко объединяться вместе рядом других способов помимо for. В каком-то смысле они обладают свойством замыкания. Для примера я сейчас построю простой модуль для парсинга параметров http запросов из пары десятков строк (полнофункциональная, боевая её версия около 50, но её я естественно никому не покажу :) ).
Пусть у нас есть класс Req, несущий в себе параметры HTTP запроса в форме словаря <имя параметра> - <список значений>. Нам нужен набор функций, позволяющий типобезопасно извлекать из него параметры, при этом возможно применяя некие алгоритмы парсинга (скажем, стандартный toInt). Также желательно иметь возможность извлекать необязательный параметры и наборы параметров. Интерфейс такого модуля можно описать следующим образом:
Во-первых я ввожу новый тип для результата получения параметра. Этот тип параметризован типом успешного результата. Он является псевдонимом для Validation со строкой в качестве описания неудачи и его параметром в качестве типа успешного значения.
Давайте последовательно реализуем каждую из этих функций, начнём с get:
Она извлекает значение параметра по имени, преобразует его в стандартный Option. И уже его преобразует в Validation с соответствующим сообщением об ошибке.
Следующим шагом будет реализация метода getOpt. Его можно сделать уже совсем прямолинейным:
Реализацию второй пары методов, подразумевающих парсинг стоит начать с того, что собственно определить парсер:
Здесь используется стандартный метод parseInt, возвращающий Either. При помощи метода fail мы преобразуем его в FailProjection и преобразуем исключение из стандартной библиотеки Java в читаемое сообщение об ошибке. Кагда это сделано результат преобразуется назад в обычный Validation. FailProjection - это специальная форма Validation с инвертированной в каком-то смысле логикой: он сделан для явного манипулирования ошибкой и неявного игнорирования успешного результата.
А также вспомогательный метод для применения parseInt и ему подобных к необязательному значению:
Метод lift берёт успешное значение и дополнительно заворачивает его в указанную "оболочку", в данном случае Option.
Теперь можно написать реализацию getInt:
Она предельно проста. Не особо задумываясь мы можем получить и getOptInt:
Этот метод очень похож структурно на getInt, практически он им и является с той поправкой, что мы трансформируем функцию парсинга для работы с необязательными значениями (продолжая следовать шаблону значение на вход - валидация на выход).
Итак, следуя простому паттерну значения на вход - Validation на выход мы получили простой в реализации и использовании модуля для типобезопасного извлечения данных из параметров HTTP запроса. Он может быть легко расширен как добавлением новых поддерживаемых типов (ну например пишем парсер дат какого-то формата и получаем тривиальное в реализации расширение модуля для работы с датами).
Он не идеален, в нём есть очевидные места дублирования (например необходимость набивать новое семейство функций getX, getOptX для каждого нового типа, да и от обязательности/необязательности значения кажется можно абстрагироваться), однако для затраченного количества умственных усилий мне кажется неплохо.
Для того, чтобы получить более правдоподобную картину давайте придумаем тестовый пример для этого модуля... Скажем запрос на поиск поста в блоге по названию (параметр запроса "text"), с опцией сортировки по дате публикации (параметр "sort") и указанием ограничения на количество результатов в ответе (параметр "limit").
И пусть у нас уже есть метод которые делает что надо в БД и метод сериализующий список результатов для передачи по HTTP, как-то так:
Давайте подумаем что нам нужно чтобы соединить вместе вышележащую библиотеку для работы с сетевым протоколом и нижележащую функцию работы с БД. Кажется почти всё есть, вот только надо научиться вытаскивать параметр сортировки... А чем порядок хуже числа?
А теперь можно и написать требуемый код склейки, извлекаюий параметры из запроса и передающий их в функцию работы с БД:
Вот и всё с первой частью моего введения. Далее я покажу как с помощь всё того-же класса Validation можно организовывать не только цепочки проверок до первой ошибки но и аккумуляцию нескольких возможных ошибок. Очень надеюсь, что он потребует меньшего времени.
Этот класс по сути объявлен следующим образом:
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 можно организовывать не только цепочки проверок до первой ошибки но и аккумуляцию нескольких возможных ошибок. Очень надеюсь, что он потребует меньшего времени.
Ярлыки:
achtung code,
programming,
scala,
scalaSPB,
scalaz,
validation
среда, 2 ноября 2011 г.
Опыт работы с Lift: часть 2
Продолжая начатое в прошлом посте выкладывание материалов ScalaSPB расскажу ещё два занятных эпизода из своего почти годового общения с Lift. Дальше будут интехнические подробности и даже куски кода. Не пугайтесь совсем, дальше в серии будет больше абстракции и философии.
Проблема с локализацией
В один светлый день функционал проекта показался близок к полному а архитектура к устоявшейся. В этот день было решено провести нагрузочное тестирование, чтобы понять как скоро станет плохо, если проектом кто-то начнёт пользоваться. Идея была с энтузиазмом подхвачена и воплощена в жизнь. К сожалению результат оказался не очень радужным. Сервер, использующий отличную кэширующую БД, написанный на замечательном, масштабируемом языке и использующий топовую EC2 конфигурацию оказался способен переварить не более 20 запросов в секунду (наиболее тяжёлых из возможных надо заметить).
Обдумывая возвращение и прекрасного мира стартапов в кровавый энтерпрайз я начал применять все подручные средства для поиска причин такой небодрой работы сервера. Первым попавшимся тулом был munin, отчёты о загрузке CPU если конкретно. Он был весьма обнадёживающим: загрузка была около 15% на сервере и 5% на БД. После недолгой медитации на эти отчёты мне пришла гениальная идея: погрепать код на слово "synchronized". Это правда ничего не дало, но немного подняло мораль.
Следующим шагом стало настроить на сервере доступа к JMX и воспользоваться им для подвода информации в VisualVM. И это был частичный win. Кусочек кода внутри Lift (ладно, не точно этот, в том момент использовался Lift 2.2, но его исходники кажется остались только в виде архивов) пользовался дикой популярностью среди заблокированных потоков. Собственно выбора особо небыло, все обращения к S.? были заменены на свежедобавленный ResourceBundle.
Для поиска я воспользовался уже послужившим добру 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 сайтов, но не сервисов для массовой перекачки данных. Вывод прост: надо брать простые инструменты сквозь которые видно что происходит. Ещё полезно изучать не только тех. документацию, но и мысли авторов относительно области применения их детищ. Эту область хорошо сопоставлять со своей задачей и делать выводы.
Обдумывая возвращение и прекрасного мира стартапов в кровавый энтерпрайз я начал применять все подручные средства для поиска причин такой небодрой работы сервера. Первым попавшимся тулом был 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. Вот смотрите:
- Рейтинг. SO: единая постоянная метрика основанная на качестве контента. Habr: двухкомпонентная система: рейтинг основанный на контенте, но убывающий по времени и карма основанная на "личных качествах", постоянна по времени.
- Право публикации. SO: постоянное и неотъемлемое. Habr:
даётся по итогам попрошайничествазависит от кармы. - Возможность голосовать за. SO: всегда имеется. Habr: зависит от кармы.
- Возможность голосовать против. SO: имеется всегда, но за счёт своего рейтинга. Habr: зависит от кармы.
- Продолжительность голосования по топику. SO: не ограничена. Habr: ограничена по врмени.
- Оценки комментариев. SO только +, незначительное влияние на рейтинг. Habr: + или -, заметное влияние на рейтинг.
Вот подумалось, вроде 6 мелочей. Но ведь очевидна "заточка" одного под аккумуляцию полезного контента, а другого под массивные срачи и популяризацию кадров вроде alizar'а.
PS А ещё SO хоть и не ограничивает по рейтингу возможность публикации, зато существенно ограничивает возможности по созданию энтропии. Вот я например уже давно мечтаю о 1,5к рейтинга, чтобы наконец уметь создавать новые тэги а то на говнище, в котором я копаюсь постоянно даже тэга не находится.
суббота, 15 октября 2011 г.
Опыт работы с Lift: часть 1
После доклада на ScalaSPB образовалась подборка материалов, которыми грех не воспользоваться. Заодно включу в них то, что никак не вписывалось в доклад: примеры с кодом и философские рассуждения. И начну я пожалуй с наиболее простого и короткого кусочка, а именно опыта общения с Lift.
(Тут нужна небольшая вводная: речь идёт об опыте разработки REST сервиса. Он во-первых заведует преобразованиями между domain model и схемой хранения данных. Во-вторых по ходу дела планирует и выполняет всякие дополнительные задачи вроде отправки оповещений на почту или шаринга контента в facebook.)
Итак, от Lift'a используется практически два модуля. Это поддержка REST-сервисов и сериализация case классов в JSON (что однако тянет с собой всю инфраструктуру работы с HTTP). В работе с ними было три эпизода, которые представляют интерес. Ниже привожу первый из них.
Дело в том, что лифтовая система авторизации состоит из двух компонентов: функции-делегата, которая должна для каждого запроса указывать роли пользователей, которые имеют права на его исполнения и обработчика авторизации - который для новой сессии формирует список ролей в которых находится открывший её пользователь. Как видно она достаточно ортогональна тому поведению которое нам требовалось.
Для того, чтобы под неё подстроиться была выбрана следующая стратегия:
После этого встала другая проблема. При отсутствии подходящей аутентификации Lift по умолчанию отправляет браузеру просьбу предоставить учётные данные. Это естественно совершенно недопустимо, особенно учитывая, что часть клиентов - браузерные плагины и предложение ввести учётные данные получали бы живые пользователи. Для того, чтобы этого избежать был сооружён маленький хак, который даже уже помог ещё одному человеку.
Ну а рассказ про замеченые проблемы с производительностью будет в следующем псто...
(Тут нужна небольшая вводная: речь идёт об опыте разработки REST сервиса. Он во-первых заведует преобразованиями между domain model и схемой хранения данных. Во-вторых по ходу дела планирует и выполняет всякие дополнительные задачи вроде отправки оповещений на почту или шаринга контента в facebook.)
Итак, от Lift'a используется практически два модуля. Это поддержка REST-сервисов и сериализация case классов в JSON (что однако тянет с собой всю инфраструктуру работы с HTTP). В работе с ними было три эпизода, которые представляют интерес. Ниже привожу первый из них.
Нестандартная авторизация
Первая проблема, которую пришлось решить это не совсем стандартная схема авторизации, используемая в нашем приложении. А именно: все экспортируемые методы делятся на два класса: приватные и публичные. Приватные защищаются на уровне файрвола от внешнего доступа и к ним на уровне приложения доступ свободный. Публичные доступны всем и соответственно на них висит авторизация. Авторизация не простая а по токену, добавленному одновременно в куку и параметр запроса. Это не что иное как издревле известный метод борьбы с CSRF. Придумать легко, а вот обучить Lift такому поведению потребовало немного нестандартного приёма.Дело в том, что лифтовая система авторизации состоит из двух компонентов: функции-делегата, которая должна для каждого запроса указывать роли пользователей, которые имеют права на его исполнения и обработчика авторизации - который для новой сессии формирует список ролей в которых находится открывший её пользователь. Как видно она достаточно ортогональна тому поведению которое нам требовалось.
Для того, чтобы под неё подстроиться была выбрана следующая стратегия:
- Помечать часть путей внутри сервиса, как требующие авторизации требуя для них определённую "волшебную" роль;
- Выполнять аутентификацию при наличии токенов путём присваивания текущему пользователю двух ролей: "волшебной" и роли с его идентификатором в качестве имени;
- Предоставлять нашему коду абстракцию над предыдущими двумя шагами вида "получить авторизованного для данного метода пользователя". Этот метод изящно реализовывался паттерн-матчингом над списком ролей :)
После этого встала другая проблема. При отсутствии подходящей аутентификации Lift по умолчанию отправляет браузеру просьбу предоставить учётные данные. Это естественно совершенно недопустимо, особенно учитывая, что часть клиентов - браузерные плагины и предложение ввести учётные данные получали бы живые пользователи. Для того, чтобы этого избежать был сооружён маленький хак, который даже уже помог ещё одному человеку.
Ну а рассказ про замеченые проблемы с производительностью будет в следующем псто...
Ярлыки:
authorization,
csrf,
lift,
programming,
scala,
scalaSPB
Подписаться на:
Сообщения (Atom)