воскресенье, 8 сентября 2013 г.

Пара слов о реальных проблемах со Scala

Как то давно я накатал изрядную стену текста на тему что хорошо в Scala и почему  люди рассказывающие что на ней невозможно писать не вызывают у меня доверия: http://it-talk.org/post82738.html#p82738

Пришло время востановить баланс и немного поговорить о конкретных проблемах, которые я в ней вижу.

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

Существенно лучше показывает себя sbt и волшебная ~. Однако тул это не простой, когда я следил за его развитием совместимость ломалась чуть не каждый релиз. А если надо развивать проект с историей, то смену системы сборки в серьёз никто не рассматривает. В итоге сейчас я вижу скорость работы компилятора главной проблемой scala.

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

Во-первых это понимания почему собственно этот язык. Если люди уверены что new ArrayList/for/add лучше map, он и правда будет лучше!

Во-вторых базового навыка программирование в функциональном стиле. Ничего космического, но надо ясно видеть в коде простые паттерны роде map/filter. Не заводить переменные без повода. Понимать что функции можно параметризовать не только данными но и операциями. Уметь создавать простые АТД по мере необходимости (а не мега бины со сложной символикой null'ов в разных полях). Вообще этерпрайзие мозга сводит на нет потуги эффективно применить языки моложе 20 лет :)

По большому счёту это все серьёзные проблемы, но есть еще пара неприятностей.

Не совсем уравновешенное сообщество. В том смысле что нельзя смотреть лежащий на гитхабе код и стабильно учиться. Не устоялся стиль, не сформировались практики. Нет общепринятой границы хорошо-плохо (что на самом деле хорошо, но не в самом начале). Кто-то пишет на самом деле на хаскеле, у кого-то джава с замыканиями. Нужно очень критическое восприятие для того чтобы отбирать решения и техники для своего проекта. Пруф: http://ru-scala.livejournal.com/39341.html.

Переусложнена на мой взгляд библиотека коллекций. Множество методов преобразования с неочевидными результатами. Куча классов и методов время работы которых непросто удержать в голове.

Противоречивый, сложный фреймворк Akka. Который позиционируется как аналог OTP выполненный на JVM, но на самом деле принципиально от него отличается.

Вот вроде и всё. Ваши мысли по сабжу?

Линкопост

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

Basics of the Unix Philosophy - в статье рассказывается история "философии Юникс", в форме примеров её формулировок разными людьми в разное время. Очень интересно показывается развитие идей в области дизайна программ.

The TTY demystified - подробно рассматривается архитектура TTY, то как особенности железа разных эпох влияли и продолжают влиять на устройство современных (уже исключительно виртуальных) терминалов. Очень интересная история, красивый и пугающий пример врмирования legacy.

Code's Worst Enemy - древний пост в ныне уже мёртвом блоге. Однако в связи со спецификой нынешней работы я прочувствовал его в полную силу только сейчас. В статье рассматриваются динамика и последствия раздувания кодовой базы. Очень помогает критически посмотреть на цену добавления "ещё одной фичи".

Как эффективно обучать инженеров? - изыскания небезызвестного Валкина на тему. Две очень полезные ссылки внутри.

VS Naipaul’s Seven Rules for Beginners - В продолжение темы, 7 простых правил написания текстов. Подкупает именно простотой. Вот, практикуюсь...

Jepsen - большая серия статей в которой рассматриваются проблемы поведения распределённых систем при сегментации. Интересна тем что автор предлагает простой тест, выражающих в конкретных цифрах утерянных данных и применяет его к ряду очень модных "scalable, fault-tolerant, NoSQL" баз данных. Серия ценна подробным анализом и объяснениями результатов тестов. Must read для всех практикующих модерновые БД в полях.

Java's Atomic and volatile, under the hood on x86 - подробный разбор особенностей реализации атомиков в Java. Содержит целый ряд заслуживающих внимания ссылок на смежные темы.

What is RCU, Fundamentally? - рассматривает семейство неблокирующих алгоритмов типа Read-Copy-Update и то как они поддерживаются Linux Kernel. Содержит массу примеров того как легко получить некорректный алгоритм такого типа, упустив из виду нюансы моделей памяти. Попутно даётся и обзор моделей памяти современных процессоров. Весь код на С, но не очень суровом, типичному Java-программисту было понятно.

среда, 26 сентября 2012 г.

Validation и исключения: совместная жизнь

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

Это значит,что будет происходить много чего о чём автор кода мог даже не догадываться. А о таких вещах JVM извещает код бросая исключения. Да да, возьмёт и бросит, и плевать ей на чистоту.

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

def readFields(rec: DBObject, ...): GzVal[DataRecord] = {
  val deletedBy =
    for {
      userId <- get[ObjectId](rec, "deleted_by");
      user <- getUserData(userId).toSuccess(MissedEntity(userId.toString, "user"))    } yield user
  for {
    id <- get[String](rec, "_id")
    content <- get[String](rec, "content")
    updated <- asValidOption(get[DateTime](rec, "upd"))
    //twelve more
    user <- getUserById(userId, currentUserId map (new ObjectId(_)))
        .toSuccess(MissedEntity(userId.toString, "user"): ValidationError)
  } yield DataRecord(/*you not gonna like it*/)
}

В общем-то ничего сложного - берём запись из БД, разбираем проверяем значения, создаём объект. При этом что-то может упасть, но что и как упало на этом уровне никого не волнует. Разбор записи надо прекращать сразу независимот от того нарушилась ли консистентность или умерло соединение с БД.

Мои основные мысли на тему Exceptions vs. Validations можно выразить таким вот набором тезисов:
  • Валидации позволяют очень выразительно и кратко обрабатывать ошибки в чистом коде.
  • При этом используя и смешивая разные стили обработки ошибок.
  • Они требуют небольшой синтаксической избыточности на каждую операцию.
  • В то же время исключения часто являются вполне достойной абстракцией для обработки ошибок.
  • Они создают значительную синтаксическую избыточность, зато сразу на группу операций.
Суммируя эти наблюдения с тем фактом, что неожиданные исключения всё-равно будут пролетать, я принял на вооружение следующие принципы:
  • Высокоуровневые абстракции в приложении стоит строить используя валидации, однако страховать их на случай совсем внезапной ошибки.
  • Тот код который легче написать с использованием исключений нужно писать с использованием исключений.
  • Исключения из такого кода надо ловить и преобразовывать в валидации.
Далее я попробую описать основные детали того, как я пытался преобразовать эти принципы в работающую архитектуру. Для начала общая картина, срисованная с моего доклада в Минске.



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

Те, кто находятся правее написаны в более чистом стиле, игнорируют исключения (в смысле прозрачного пробрасывания наверх, а не в смысле подавления). Об их существовании вновь вспоминает лишь тонкая прослойка, между нашим кодом и внешним миром (в моём случае это был Lift, в других может быть сетевая библиотека или GUI).
Давайте рассмотрим несколько фрагментов кода, для более точного представления этих идей. Скажем для нашего HTTP сервиса из предыдущих примеров мы отказались от строки как от типа ошибки, определив такой класс:
abstract class Error(
    val httpCode: Int,
    val code: String)

И каких-то его потомков, например так:

case class MissedEntity(
  id: String, type: String)
extends Error(404, "MISSED_ENTITY")

case class InternalError(
  @transient Throwable cause)
extends Error(cause, "INTERNAL")

Тогда можно легко задать функцию преобразования:
val dispatchException: Function[Throwable, Error] = {
  case UserNotFound(name) =>
    MissedEntity(name, "user")
  ...
  // Don't remove it!!!
  case t => InternalError(t)
}

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

def safeVal[T]: Catch[ReqVal[T]] =
  handling(classOf[Throwable]) by { e =>
    dispatchException(e).fail
  }

def safe[T](block: => T): ReqVal[T] =
  safeVal( block.success )

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

Пользоваться этими служебными функциями можно так:

def editData(e: Edit):
  ReqVal[Data] = safe {
    //all dangerous stuff here
  }

Экранируя подобным образом методы компонентов связаных с вводом-выводом (или ещё чем-то трудно обрабатываемым при помощи валидаций) мы можем создать на определённом уровне глобальный переход от одного способа работы с ошибками к другому. И стоящая выше логика может пользоваться всеми теми приятностями про которые рассказывал ранее. На картинке это уровни 'Services' и 'Controllers'.

Есть ещё один важнейший момент, который сподвиг меня на все эти эксперименты, но о котором я не говорил ранее. Дело в том, что конечной целью было построение REST сервиса, использующего протокол HTTP. Замечательной особенностью HTTP, дичайше игнорируемой всеми, является прямая поддержка сообщений об ошибках. В нём есть понятие кода ответа, что позволяет стандартным образом с детально описанной семантикой отвечать любому клиенту и быть понятым. Ещё раз подумайте, до любого клиента, хоть браузера, хоть curl можно донести что пошло не так и как с этим можно побороться... Вместо этого повсеместно практикуется '200 OK... {error: "Failed to..."}'. Зачем???!!!

Ну да, хватит о грустном. Поговорим о весёлом. А состоит оно в том что если мы представить себе, что у нас есть класс HttpResponse, позволяющий описать все ответы. То преобразование из результата R выполнения какой-то операции в HttpResponse концептуально абсолютно корректно задать как функцию Validation[R] => HttpResponse. Которая естественным образом распадается на незатейливый шаблон:

r match {
  case Sucess(r) => buildSuccessResponse(r)
  case Failure(f) => buildFailureResponse(f)
}

Где buildFailureResponse, как не сложно догадаться задаётся один раз на систему, а весь шаблон легко инкапсулируется в простую конструкцию. Предполагая, что у нас определены фунция-сериализатор decompose: Any=>JSONObject и преобразование из объекта ошибки в HTTP ответ errorResponse, можно написать такую функцию.

def valsToResponse[A](v: ListVal[A]): HttpResponse = v match {
  case Success(res) =>
    JsonResponse(decompose(rel), successCode)
  case Failure(err) => errorResponse(err)
}

Имея операцию op: A=>ListVal[B], где A и B уже специфичные для приложения классы представления запросов и ответов, можно легко строить (нетривиальные!) обработчики запросов в одну строчку.

def getUsers(req: Req, namePattern: String): LiftResponse = valsToResponse {
  getUsersInternal(namePattern) map { users =>
    val (count, list) = users
    Map("count" -> count, "items" -> list)
  }
}

Здесь для примера я преобразую пришедший список результатов в пару значений длина и собственно список (ну представим что кому-то из клиентов так удобнее ;) ). В таком примере getUsersInternal имел бы тип String => ListVal[List[User]].

Такого вида функции тривиально цепляются к REST модулю Lift - не буду тратить место на пример.

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

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

понедельник, 24 сентября 2012 г.

"DSLs in action" и CPJ

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

Итак, первая это "DSLs in action", пролежавшая, кстати, в очереди на прочтение без малого год. Начинается она довольно вяло, где-то на 100 странице было уже неслабое желание бросить. Введение полно рассуждениями на тему места DSL, их экономической целесообразности и классификаций. Не чтобы это было совсем не нужно, но ИМХО такие вещи должны быть в конце. Падать на читателя уже, так сказать, замотивированного дойти до конца. Ну да хватит о грустном.

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

Автор без сомнения проделал огромную работу готовя примеры и выбирая подход к решению каждой задачи. Книгу стоит прочитать даже людям совершенно не интересующимся DSL просто полюбоваться многообразием современных языков и тем диапазоном техник программирования который они предлагают.

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

Ктоме того замечу, что по контенту она практически не пересекается с прочитанной ранее "Java Concurrency in Practice". Если в гниге Lea рассматриваются в первую очередь примитивы многопоточности, в том числе и пути их реализации, то Goetz концентрируется на корректном их применениии в коде приложений. И естественно последний оперирует util.concurent как данностью. В общем можно заключить, что книги стоит читать обе, наверно даже в любой последовательности.write

воскресенье, 11 марта 2012 г.

"Programming Erlang" и "Я - математик"

С нового года осилил ещё пару книг. Отчитываюсь одним постом, чтобы не увеличивать энтропию.

Первая, "Programming Erlang" - классическое введение в язык программирования от автора. Очень краткая, очень содержательная книга. Где-то в сотне страниц рассматривается сам язык, ещё где-то в сотне страниц описываются основные техники применения и важные библиотеки. В заключении рассматриваются подходы к решению практических задач: работа с вводом-выводом, распределённое и параллельное программирование, защита от сбоев.

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

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

Несмотря на все старания не смог найти в книге заметных недостатков. Автор задаёт очень, очень высокий стандарт для вводных книг по языкам/технологиям. Трудно что-либо с ним сравнивать, разве что JCP (естественно со скидкой на сложность топика).

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

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

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

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

среда, 18 января 2012 г.

"Programming in Scala"


Незадолго до нового года я закончил читать "Programming in Scala", написанную частично самим автором языка. Первое что стоит отметить это внушительный размер труда - более 800 страниц. Что в принципе понятно, язык совсем не маленький. Во-вторых книжка даёт более чем достаточное для самостоятельной жизни представление о языке. После неё можно брать и начинать писать рабочий проект.

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

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

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

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

четверг, 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 контейнеров.