Статьи

Подражая Kotlin Builders в Java и Python

вступление

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

Котлин

Для начала мне нужно объяснить способность Kotlin делать типобезопасные сборщики. Чтобы быстро объяснить, что это за строители, вы должны проверить их страницу о них . В этой статье мы будем реализовывать небольшое подмножество их HTML-компоновщика.

Способность Kotlin создавать типобезопасные компоновщики обусловлена ​​множеством мелких особенностей. Первый — это лямбда-синтаксис; {param, list -> block.of.code()} . Если лямбда имеет нулевые параметры, вы можете игнорировать список параметров и стрелку. То же самое верно, когда он имеет только один параметр, так как этот параметр неявно называется it . Например, {doSomethingWith(it)} является допустимой лямбда- doSomethingWith() , если doSomethingWith() , что doSomethingWith() принимает объект того же типа, что и передаваемый в лямбда-выражение.

Следующая функция — как передавать лямбды в функции. Если последний аргумент является лямбда-выражением, его можно передать после скобок вызова функции. Например, myFunc(arg1){lambdaArg()} . Если лямбда является единственным аргументом, круглые скобки могут быть полностью проигнорированы: aFunc{lambdaArg()} . Это позволяет вам определять функции, которые могут выглядеть как языковые функции. Технически вы можете определить свои собственные блоки if-else или любой из циклов, если бы не тот факт, что эти ключевые слова зарезервированы.

Далее идут методы расширения и тот факт, что вы можете определять лямбды, которые работают как они. Методы расширения — это новые методы, которые определены для класса или интерфейса вне класса интерфейса. Например, вы можете создать новые методы для класса String . В действительности это просто статические методы, которые принимают неявный первый параметр того типа, для которого они предназначены. В коде Kotlin этот первый параметр присваивается идентификатору this , который используется неявно, как и в реальном методе.

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

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

01
02
03
04
05
06
07
08
09
10
html {
   head {
      title("A Title")
   }
   body {
      p = "paragraph"
      p = "'nother one"
      p = "last paragraph"
   }
}

Чтобы вернуть Html объект, который содержит Title и Body , Title содержит Title с текстом «заголовок». Body содержит 3 Paragraphs .

Вы можете заметить, что title и [p] различаются по своему определению. Вероятно, было бы разумнее иметь title чтобы использовать синтаксис = вместо p , но p демонстрирует, насколько креативно эти конструкторы могут быть лучше, чем title . Я сделал то же самое с Python, так как он также поддерживает свойства.

Давайте посмотрим на код Kotlin, который позволяет создавать эти объекты

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
fun html(htmlBuilder: Html.() -> Unit): Html {
   val html = Html()
   html.htmlBuilder()
   return html
}
 
class Html {
   private var head: Head? = null
   private var body: Body? = null
 
   fun head(headBuilder: Head.() -> Unit) {
      head = Head()
      head?.headBuilder()
   }
 
   fun body(bodyBuilder: Body.() -> Unit) {
      body = Body()
      body?.bodyBuilder()
   }
}

Мы начнем с класса Html и функции html() используемой для запуска компоновщика. Функция html не является необходимой, поскольку код можно использовать в качестве конструктора Html , но она позволяет нам сохранять конструктор простым и все функции строчными, не нарушая соглашений об именах.

Вы заметите, что на самом деле все чертовски мало. Только функция html состоит из 3 строк, и это только потому, что она должна возвращать результат в конце. Если бы вместо этого мы использовали конструктор в Html , он имел бы только строку htmlBuilder() .

Вот Title и Title .

01
02
03
04
05
06
07
08
09
10
class Head {
   private var title: Title? = null
 
   fun title(text: String) {
      title = Title(text)
   }
}
 
 
class Title (private val text: String) { }

Все еще идет довольно хорошо. Title не требует строителя, так как он просто содержит текст. Если бы не тот факт, что понадобилась бы более сложная механика сборки, я бы на самом деле заставил бы Head просто удерживать саму String вместо создания класса и объекта Title .

01
02
03
04
05
06
07
08
09
10
11
12
class Body {
   private val paragraphs: ArrayList<Paragraph> = ArrayList()
 
   var p: String
      private get() = null!!
      set(value) {
         paragraphs.add(Paragraph(value))
      }
}
  
 
class Paragraph (private val text: String) { }

Вот действительно интересная вещь. Вместо использования метода p() , как мы это сделали для Title , мы использовали установщик p , чтобы продолжать добавлять объекты Paragraph в список. В этом случае это не самый интуитивный; это просто чтобы показать вам, насколько креативным можно стать с этими строителями.

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

Ява

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

01
02
03
04
05
06
07
08
09
10
html(html -> {
   html.head(head ->
      head.title("A Title")
   );
   ht.body(body -> {
      body.p("paragraph");
      body.p("'nother one");
      body.p("last paragraph");
   });
});

И это настолько близко к синтаксису компоновщика, который вы можете получить в Java. Обратите внимание, что нет никакой разницы в способе вызова title() и p() , поскольку Java не предоставляет никакой подобной свойствам конструкции. Также обратите внимание, что вам нужно иметь имя для всего. При неявном this вы должны написать что-то вроде hd.title(...) а не просто title(...) , и это даже не говоря о том факте, что мы должны определить список параметров для лямбда-выражения.

Есть несколько других вещей, которые вы могли бы сделать, но это еще хуже, первое из них — просто использование нормального кода:

1
2
3
4
5
6
7
Html html = new Html();
   Head head = html.head();
      head.title("A Title");
   Body body = html.body();
      body.p("paragraph");
      body.p("'nother one");
      body.p("last paragraph");

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

Итак, давайте посмотрим на код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class Html {
   public static Html html(Consumer<Html> htmlBuilder)
   {
      Html html = new Html();
      htmlBuilder.accept(html);
      return html;
   }
 
   private Head head = null;
   private Body body = null;
 
   public void head(Consumer<Head> headBuilder) {
      head = new Head();
      headBuilder.accept(head);
   }
 
   public void body(Consumer<Body> bodyBuilder) {
      body = new Body();
      bodyBuilder.accept(body);
   }
}

Это так же напрямую, как порт для Java, как он получает. Функция html() была перемещена в класс Html как статический метод, поскольку она должна быть где-то в Java. Мы использовали Consumer<Html> , поскольку Java ближе всего подходит к тому типу лямбд, который мы хотим.

Вот Title и Title :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class Head {
   private Title title = null;
 
   public void title(String text) {
      title = new Title(text);
   }
}
 
 
public class Title {
   private final String text;
 
   public Title(String text) {
      this.text = text;
   }
}

Здесь не так много внимания. Вероятно, это то, что вы ожидали. Теперь, чтобы закончить с Paragraph Body .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class Body {
   private final List paragraphs = new ArrayList<>();
 
   public void p(String text) {
      paragraphs.add(new Paragraph(text));
   }
}
  
 
public class Paragraph {
   private final String text;
 
   public Paragraph(String text) {
      this.text = text;
   }
}

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

Вот что нужно для создания версии Java. Помимо многого синтаксического многословия, его легче создать в Java, чем в Kotlin, потому что нет никаких дополнительных функций, о которых нужно подумать и применить: P

питон

Попытка выяснить, как сделать что-то подобное в Python, потребовала, чтобы мне посчастливилось увидеть видео, демонстрирующее новый (но не интуитивно понятный) способ использования контекстных менеджеров ( with утверждениями). Проблема в Python заключается в том, что лямбды могут иметь только одно выражение или оператор. Менеджеры контекста позволяют (очень ограниченный) способ обхода однострочных лямбд, позволяя эффективно возвращать объект (или ничего) при входе, который можно использовать, находясь в менеджере контекста, как если бы он был внутри лямбда-выражения.

Так, например, конструктор будет выглядеть в Python следующим образом:

1
2
3
4
5
6
7
8
myhtml = Html()
with myhtml as html:
    with html.head() as head:
        head.title("A Title")
    with html.body() as body:
        body.p = "paragraph"
        body.p = "'nother one"
        body.p = "last paragraph"

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

1
2
3
4
5
6
7
html = Html()
head = html.head()
head.title("A Title")
body = html.body()
body.p = "paragraph"
body.p = "'nother one"
body.p = "last paragraph"

Самым большим преимуществом блоков with является отступ, поскольку Python имеет ограничения на отступ, потому что он использует отступ над фигурными скобками. Менеджеры контекста, возможно, того стоят. Но есть еще одно преимущество, о котором я расскажу ближе к концу, после того, как покажу вам основной код, необходимый для их создания в Python:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class Html:
    def __enter__(self):
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        return False
 
    def head(self):
        self._head = Head()
        return self._head
 
    def body(self):
        self._body = Body()
        return self._body

Здесь вы можете видеть, что класс Html имеет необходимые __enter__() и __exit__() для управления контекстом. Они практически ничего не делают; __enter__() возвращает только self , а __exit__() просто означает, что он не имел дело с какими-либо исключениями, которые могли быть переданы. Методы head() и body() делают в значительной степени то, что вы ожидаете сейчас, с предположение, что Head и Body также являются типами менеджера контекста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Head:
    def __enter__(self):
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        return False
 
    def title(self, text):
        self._title = Title(text)
  
 
class Title:
    def __init__(self, text):
        self.text = text
  
 
class Body:
    p = property()
 
    def __enter__(self):
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        return False
 
    @p.setter
    def p(self, text):
        if not hasattr(self, 'paragraphs'):
            self.paragraphs = []
        self.paragraphs.append(Paragraph(text))
 
 
class Paragraph:
    def __init__(self, text):
        self.text = text

Единственная новая вещь, на которую здесь стоит обратить внимание — это использование property Body для его тега p . К счастью, нам не нужны геттеры для property , для которых нам нужно возвращать None , как в Kotlin.

Хорошо, теперь мы рассмотрим интересную, менее очевидную причину, по которой полезно использовать контекстные менеджеры для этой ситуации. В Java и Kotlin нам потребовался бы дополнительный вызов в конце к методу build() (или иначе функция html() сделала бы это для нас) и заставила бы его сделать рекурсивный обход сразу в конце, чтобы принять заботиться об этом. С помощью диспетчера контекста __enter__() и __exit__() могут передать версию объекта при создании, а затем построить ее при выходе. Это означает, что каждый промежуточный этап сборщиков уже содержит полностью собранные версии к моменту выхода.

На самом деле это может быть немного трудно обернуть голову вокруг. Вот пример, который выполняет частичную реализацию, используя Html , HtmlBuilder и Head :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Html:
    def __enter__(self):
        self._builder = HtmlBuilder()
        return self._builder
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.head = self._builder._head
        self.body = self._builder._body
        del self._builder
        return False
 
 
class HtmlBuilder:
    def head(self):
        self._head = Head()
        return self._head
 
    def body(self):
        ...
 
 
class Head:
    def __enter__(self):
        self._builder = HeadBuilder()
        return self._builder
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.title = self._builder._title
        del self._builder
        return False

Здесь метод __enter__() объекта Html создает и сохраняет конструктор, а затем возвращает его. После __exit__() он строит себя из значений, хранящихся в компоновщике, и удаляет компоновщик из себя. Сначала подумав, по крайней мере для меня, можно подумать, что объекты, хранящиеся в конструкторе, не являются законченными объектами, но они есть. Методы объекта построителя возвращают правильный класс со своими собственными __enter__() и __exit__() которые также гарантируют его правильную HtmlBuilder , как это видно с помощью HtmlBuilder head() HtmlBuilder и реализации Head . При такой настройке код вызова на самом деле остается таким же, как и в первый раз.

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

01
02
03
04
05
06
07
08
09
10
11
Html html = Html();
try(html) {
   try(Head head = html.head()) {
      head.title("A Title");
   }
   try(Body body = html.body()) {
      body.p("paragraph");
      body.p("'nother one");
      body.p("last paragraph");
   }
}

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

Outro

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

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

Спасибо за чтение!

Примечание. Со вчерашнего дня все редактирование завершено. Отсюда мне «просто» нужно оформить обложку, для которой у меня есть идея; получить все форматирование, рассчитанное как для печатных, так и для электронных книг; написать приложение (в основном, просто набор фрагментов кода из книги, более подробно); и закончите писать репозиторий GitHub, в котором будут все супер полезные классы и функции для быстрого, простого и простого создания собственных дескрипторов с меньшим количеством проблем. Я ожидаю, что все это будет сделано к концу лета, но, надеюсь, раньше. Моя жизнь немного оживилась, поэтому я не знаю, сколько времени я смогу посвятить всему этому.

Ссылка: Подражание Kotlin Builders на Java и Python от нашего партнера JCG Джейкоба Циммермана в блоге « Идеи программирования с Джейком» .