Статьи

Scala: работа с предикатами

Я люблю меня немного Скала. На самом деле, так как теперь это моя дневная работа, я люблю ее все время. Он сочетает в себе короткую выразительность, которую я ценил в Python, с богатой библиотечной базой (благодаря Java) и проверкой компилятора, от которой я стал зависеть в статически типизированном языке. Мне все равно, что говорят некоторые люди . Я признаю, что язык не лишен недостатков. Можно сказать, что есть некоторые недостающие языковые расширения, особенно с предикатами.

Что я имею в виду под этим? Не существует ли неявной поддержки, встроенной в язык, чтобы они обобщали A => Boolean ? Конечно. Однако у меня возникает проблема, когда я вижу такие методы, как List’s :: filter и :: filterNot . Первое имеет смысл, последнее подчеркивает отсутствие фундаментальных строительных блоков, которые можно увидеть непосредственно в названии. То есть нам не хватает вспомогательной предикатной функции « Не »:

1
2
3
case class Not[A](func: A => Boolean) extends (A => Boolean){
  def apply(arg0: A) = !func(arg0)
}

Если бы это было простое исправление, и если бы это было все, чего не хватало, то было бы легко предложить и включить в следующую версию Scala. Конечно, нам также нужно было бы иметь 22 версии «Not» для каждой из 22 версий Function, но это спор для другого дня.

Достаточно сказать, что Scala нужна явная поддержка предикатов. Ему нужно больше, чем просто «Нет», ему нужно легко читать и поддерживать логические комбинаторы, и ему нужна поддержка основных строительных блоков, которые можно использовать для формирования логики предикатов более высокого порядка . Использование других принятых библиотек Predicates не даст нам необходимой мощности и гибкости.

Добавление выражений предикатов
Это именно то, что я сделал с моей библиотекой предикатов . Одна из целей этой небольшой библиотеки состояла в том, чтобы добавить некоторую простую синтаксическую поддержку для составления функций предикатов в описательной и краткой форме. В частности, я хотел сказать «больше 4, но меньше 10? или «больше нуля или даже, но не оба» в почти простом английском. Я пишу выражения, эквивалентные этому, все время с помощью операторов :: filter и :: exist :

1
myList.filter(x => x > 4 && x < 10)

Для небольших фраз это не так сложно. Единственный добавленный дополнительный шаблон — это обозначение «x =>», которое указывает, что мы формируем анонимную функцию . К сожалению, если я хочу повторно использовать, расширять или поддерживать эту логику, мне придется использовать еще больше шаблонов. Иногда, если логика достаточно серьезна, мне нужно разделить ее на несколько методов, которые могут или не могут быть присоединены к иерархии признаков / классов. В то время как хороший стиль кодирования, это добавленное многословие оставляет неприятный вкус во рту.

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

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

Существует 22 варианта предиката, очень похожих на то, как Scala выбрала 22 варианта функции, каждый из которых оснащен следующими методами:

и => pred1 (…) && pred2 (…)}
andNot => pred1 (…) &&! pred2 (…)
nand =>! pred1 (…) || ! Pred2 (…)
или => pred1 (…) || pred2 (…)
orNot => pred1 (…) || ! Pred2 (…)
nor =>! (pred1 (…) || pred2 (…))
xor => if (pred1 (…)! pred2 (…) еще pred2 (…)
xnor =>

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

1
2
3
4
5
6
case class LessThen(x: Int) extends Function[Int,Boolean]{ def apply(arg: Int) = arg < x }
case class Modulo(x: Int, group: Int) extends Function[Int,Boolean]{ def apply(arg: Int) = (arg % x) == group }
case class GreaterThanEqual(x: Int) extends Function[Int,Boolean]{ def apply(arg: Int) = arg >= x }
val myList = List(1,2,3,4,5,6,7,8,9)
myList.filter( LessThan(7) and GreaterThanEqual(4))
myList.filter( Modulo(4,2) or Modulo(3,0) or Modulo(5,1) )

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

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

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

Я хотел избежать как можно более общих проблем, с которыми я сталкивался ранее при использовании строго OO-кода. Неявное преобразование скрыло преобразованный класс за ограниченным интерфейсом, как шаблон адаптера , так же, как это делает Scala с анонимными функциями. Я рассуждал, что все типы crud, которые могут быть добавлены в класс, будут скрыты этим интерфейсом и, следовательно, не будут загрязнять предикат. Добавьте к этому возможность составлять функции для создания различных типов предикатов из исходного предиката, и мы получили довольно большую долю в создании плохого кода. Функциональная композиция должна быть одной из лучших вещей, которые Scala украла из функционального программирования.

Что еще?
Была только одна вещь, которую нужно добавить к части «предикатов» этой библиотеки, функция «есть». Идея для этой функции была украдена из Data.Function.Predicate из Haskell. Сначала я создал все 22 версии с одной и той же точной сигнатурой «есть» на Haskell, но потом я понял, что энергичная оценка Scala вызвала несоответствие типов, которое не могло быть легко преодолено без добавления шаблона. Поскольку «is» был разработан с уменьшением шаблонного кода и в то же время повышением читабельности, простым решением было создание неявного преобразования в анонимный класс с помощью одного метода «is», принимающего предикат. Написано таким образом, это может быть использовано следующим образом:

myStringList.filter (_. длина меньше LessThan (0))

который очень читабелен и отображает анонимную функцию типа A => B в A => Boolean. Недостатком является то, что он создает новый объект при каждом вызове.

Будущая работа
Условные функции сложно спроектировать хорошо, но в то же время они являются основой вычислительных логических элементов . Частичные функции могут использоваться для создания предикатной логики, но непрозрачным образом для внешнего наблюдателя. Есть причина :: orElse функция по причине (тоже хорошая), которая используется больше для охвата случая, а не для завершения дела. Фактически, существование функции-члена :: lift демонстрирует, что логический путь «catch all» не требуется в отличие от стандартного оператора «if-else». Следовательно, PartialFunction не является хорошим выбором для предикатных приложений.

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

1
2
3
case class ApplyEither[A,B](pred: Predicate[A], thatTrue: A => B, thatFalse: A => B) extends (A => B){
  def apply(arg0: A) = if(pred(arg0)) thatTrue(arg0) else thatFalse(arg0)
}

было легко следовать очень простой императивной модели . Расширяя это до состава:

1
2
3
4
5
6
case class ComposeEither[A,B,C](pred: Predicate[B], that: A => B, thatTrue: B => C, thatFalse: B => C) extends (A => C){
  def apply(arg0: A) ={
    val out = that(arg0)
    if(pred(out)) thatTrue(out) else thatFalse(out)
  }
}

также оказалось легко. Это было так просто, я написал больше скриптов для генерации кода для 22 версий «ApplyIf», «ApplyEither», «ComposeIf», «ComposeEither», «AndThenIf» и «AndThenEither». Затем я расширил написанный мною код, чтобы все они расширили одну и ту же черту, что позволило использовать один из них внутри другого.

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

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

Это что-то в будущем. Лично я хотел бы дождаться правильной реализации HList , который не страдает от стирания типов или не требует экспериментальных флагов компилятора, но в то же время. Майлз Сабин уже доказал, что это можно сделать с его невероятной библиотекой Shapeless . Теперь все, что мне нужно сделать, — это дождаться изменений компилятора, которые требуются, чтобы перейти в основной поток.

Ссылка: Scala: Работа с предикатами от нашего партнера JCG