Самый маленький кусочек великого кода Pharo

Как познакомить других разработчиков с неизвестным альтернативным языком программирования? Один из способов сделать это - показать множество небольших примеров. Именно это мы и сделали в Elegant Pharo Code - Beautiful & Powerful One-liners, Expressions and Snippets.

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

Другой подход - показать и объяснить реальные, последовательные примеры. Это основная тема большинства статей в издании Относительно Pharo.

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

Что, если мы перевернем вопрос вверх дном?

Какой была бы самая маленькая часть великого кода Pharo? Что-то крошечное, но очень красивое?

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

Биты и байты

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

| mask |
mask := 0.
mask := mask bitAt: 3 put: 1.
mask := mask bitAt: 7 put: 1.
mask

Мы начинаем маску с нуля, а затем устанавливаем 3-й и 7-й бит в 1 (Pharo отсчитывает от 1, целые числа неизменны). Результат - 68 или 1000100 в двоичном формате. Получить бит еще проще.

mask bitAt: 3

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

| mask |
mask := 0.
mask := mask bitAt: 3 
             put: (self flag1 ifTrue: [1] ifFalse: [0]).
mask := mask bitAt: 7 
             put: (self flag2 ifTrue: [1] ifFalse: [0]).
mask

Мы предполагаем, что у нашего объекта модели есть методы # flag1 и # flag2, возвращающие логическое значение. Преобразование при тестировании немного тривиально.

(mask bitAt: 3) = 1

Хотя это здорово, что мы можем написать встроенный тест для преобразования, это не очень элегантно, даже некрасиво. Что, если бы мы ввели вспомогательные методы, чтобы немного навести порядок?

flag1AsBit
  ^ self flag1 ifTrue: [1] ifFalse: [0]

Теперь мы можем написать

| mask |
mask := 0.
mask := mask bitAt: 3 put: self flag1AsBit.
mask := mask bitAt: 7 put: self flag2AsBit.
mask

Это лучше, но это еще большая работа, добавление дополнительного метода для каждого флага. И тест остается таким глупым, правда?

Булевы

Вот как логические значения реализованы в Pharo. Они такие же объекты, как и все остальное, конечно. Существует абстрактный класс Boolean с двумя конкретными подклассами: True и False.

Object
   +--> Boolean
           +--> True
           +--> False

У этих классов нет состояния, это чистое поведение: они по-разному реагируют на разные сообщения. Система проверяет наличие только одного экземпляра каждого из этих двух классов, ever, с подходящими именами true и false. Они похожи на предопределенные константы (и 2 из 5 зарезервированных слов в языке). Это экземпляры классов True и False соответственно, которые имеют общий
суперкласс Boolean.

Вся логика определяется как сообщения, отправляемые этим классам. Например, рассмотрим реализацию #and:

Boolean>>and: alternativeBlock 
  "Non evaluating conjunction -- If the receiver is true, answer the value of the argument, alternativeBlock; otherwise answer false without evaluating the argument"
  self subclassResponsibility

False>>and: alternativeBlock 
  "Non evaluating conjunction -- answer with false since the receiver is false"
  ^ self

True>>and: alternativeBlock 
  "Non evaluating conjunction -- answer the value of alternativeBlock since the receiver is true"
  ^ alternativeBlock value

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

Жемчужина

Теперь мы можем, наконец, представить крошечный кусок кода, который так красив: сообщение #asBit.

Boolean>>asBit
  "Convert me to an Integer using 1 for true and 0 for false"
  self subclassResponsibility

False>>asBit
  "Return Integer 0 since I am false"
  ^ 0

True>>asBit
  "Return Integer 1 since I am true"
  ^ 1

«Это все?», - возможно, спросите вы. Да, это так (я сказал вам, что он был крошечным).

Почему это так здорово?

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

Это крошечный

Он настолько мал и даже тривиален, что его легко не заметить. Тем не менее, это действительно здорово.

Название сообщения также короткое, по сути, легкое для понимания и соответствует общепринятым соглашениям.

Это решает нашу проблему

Используя этот метод, мы можем переписать наш проблемный код и жить по правилу DRY (не повторяйся).

| mask |
mask := 0.
mask := mask bitAt: 3 put: self flag1 asBit.
mask := mask bitAt: 7 put: self flag2 asBit.
mask

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

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

Это невозможно на большинстве языков программирования.

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

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

Использует метод отправки

Обратите внимание, как тест полностью пропал! Где это делалось? Как это вообще возможно ?

Он использует диспетчерский метод Pharo, основной метод, при котором идентичное сообщение вызывает разные реализации в зависимости от класса получателя. Отправка #asBit в true приводит к тому, что система выбирает одну реализацию, а отправка ложной - в другой. Это также называется полиморфизмом.

Эффективное использование диспетчеризации методов - ключ к созданию отличных объектов. Чрезмерное использование тестов, условных операторов и особенно операторов case часто указывает на то, что ваш дизайн можно улучшить.

Он затрагивает суть объектного программирования.

Объектное программирование - это отправка сообщений между объектами в большей степени, чем абстракции данных. Поведение важнее внутренней структуры.

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

Манипулирование объектами с использованием их ранее существовавшего API часто приводит к повторяющемуся коду. Расширение их поведения может элегантно решить эту проблему.

Это показывает, что даже объекты без состояния являются мощными

Как упоминалось ранее, обратите внимание, что задействованные 3 класса не имеют ни состояния, ни переменных экземпляра. Тем не менее, их поведение действительно имеет значение, имеет функциональное значение и решает реальные проблемы.

Это показывает, что даже постоянные методы являются мощными

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

Крутые комментарии

Необходима документация. В данном конкретном случае говорить о «я» и «я» или о своей «собственной ценности» - это довольно круто с философской точки зрения, не так ли?

Есть модульные тесты

В Pharo есть большой набор модульных тестов, которые запускаются каждый раз, когда что-то меняется, чтобы гарантировать отсутствие сбоев в работе. Даже такому простому методу, как #asBit, нужны свои модульные тесты, см. 2 реализации #testAsBit, которые в основном утверждают следующие 2 утверждения:

self assert: true asBit = 1
self assert: false asBit = 0

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

Он был добавлен недавно

Этот код был добавлен в самом конце 2012 года и с тех пор стал частью основного образа Pharo. Это важно, потому что это означает, что Pharo постоянно развивается.

Активно используется только в Pharo.

Способ работы Boolean в Pharo наследуется всеми реализациями Smalltalk. Метод #asBit активно используется только в Pharo, хотя его можно добавить к любому из них. Как быстро развивающаяся, активно развивающаяся система 21 века, Pharo принимает и использует такие хорошие идеи, как эта, почти ежедневно.

Добавили прикольные люди

Pharo разрабатывается разнообразным международным сообществом. Один из наших девизов - «Pharo принадлежит вам», что означает, что вы можете видеть его таким и вносить даже небольшие улучшения.

В данном конкретном случае Игорь Стасенко (из Украины) и Камилло Бруни (из Швейцарии) добавили это, работая вместе в лаборатории RMOD INRIA во Франции. Вероятно, они не изобрели его сами, но они прекрасно знали, насколько это круто, что им следует использовать его и что он заслуживает свое место в образе. Спасибо ребята !

По всем вышеперечисленным причинам этот крошечный фрагмент кода является отличным примером действительно отличного кода Pharo.

Конец

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

У вас есть любимый фрагмент кода?