Сайт Романа ПарпалакаБлог202308

Чему же равно 6:2(1+2)?

15 августа 2023 года, 23:57

Когда я впервые увидел этот пример в интернете, подумал, что это проблема на ровном месте. Да, мнения людей об ответе расходятся: кто-то отвечает 1, а кто-то 9. Но в реальности никакой путаницы не бывает, хотя бы потому, что деление обозначается двоеточием разве что в школе. В книгах и статьях формулы с делением записываются в виде дроби, и сначала нужно выполнить действия в числителе (над чертой) и в знаменателе (под чертой), и только потом выполнять деление, обозначаемое чертой. С такой записью разночтений нет:

$${6\over 2(1+2)}={6\over 6}=1.$$

Борис Трушин смог снять по этой теме целых два видео по 18 минут:

После просмотра я сделал для себя такой вывод. Приоритет арифметических действий учат в начальной школе, а опускать знак умножения — только в средней школе. По характеру необходимых действий пример 6:2(1+2) — из начальной школы, поэтому он записан некорректно, умножение между двойкой и скобками опускать нельзя.

И совсем недавно мне попалось еще одно видео по теме. Оказывается, мнение о правильном ответе расходятся не только у спорящих в интернете, но и у производителей калькуляторов!

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

После просмотра этого видео я понял, что мой аргумент про использование двоеточия как знака деления в основном в начальной школе — это всего лишь отрицание проблемы. Действительно, использование горизонтальной черты для обозначения деления удобно в отдельных формулах, а не в сплошном тексте. Сейчас в математических текстах вместо двоеточия используется наклонная черта. Но никто не мешает спросить, чему равно 6/2(1+2).

Я решил посмотреть, как сам записывал в одну строку формулы с делением и неявным умножением, и какие приоритеты подразумевал. Прошелся по текстам в блоге о теоретической физике старше нескольких лет, чтобы исключить возможное влияние обсуждений этого вирусного примера. В выражении (5/4) v/R взял в скобки числовой множитель, чтобы показать, что получившаяся величина на четверть больше некоторой характерной угловой скорости v/R. При этом (5/4) v/R ≠ 5/4v/R = 5/4vR. По тем же соображениям использовал скобки в (4π/c) j, здесь так же (4π/c) j = 4πj/c ≠ 4π/cj. В выражении v/(pR) оставил в знаменателе скобки для понятности, их можно было бы убрать. И, наконец, c2/4G. Здесь и 4, и G в знаменателе, c2/4G ≠ c2G/4. Получается, я вполне последовательно использовал неявное умножение с более высоким приоритетом, чем деление, хотя и не могу вспомнить, что подобному правилу нас учили так же, как, например, формуле для решения квадратного уравнения.

Раз уж мы обсуждаем приоритеты арифметических действий, поделюсь воспоминанием из начальной школы, кажется, из второго класса. Учительница нам говорила, что если в выражении на одном уровне несколько умножений и делений, то выполняются сначала деления, а потом умножения. Такого правила я больше нигде не встречал. Обычно учат, что умножение и деление выполняется подряд, слева направо. Например, 8/4*10/2 = 2*10/2 = 20/2 = 10. Если воспользоваться «странным» правилом о приоритете деления над умножением, тоже получится 8/4*10/2 = 2*5 = 10. При этом нельзя, например, сначала выполнить все умножения, а потом все деления. В нашем примере получилось бы 8/(4*10)/2 = 8/40/2 = 0,1, что не совпадает с правильным ответом. Как вы думаете, всегда ли «странное» правило приоритета деления над умножением приводит к тем же результатам, что и обычное правило? Или сможете найти контрпример?

    Оставить комментарий

Мысли о движке сайтов S2

25 августа 2023 года, 16:30

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

История

Историю движка можно проследить по тегу «S2». Главная проблема движка в том, что он был полем для моих экспериментов в процессе изучения веб-разработки. Человек, изучающий некоторый предмет, проходит те же стадии, по которым этот предмет развивался. Поэтому всё, что я делал в движке, было вчерашним (если не позавчерашним) днем с точки зрения качества кода и современных практик его написания.

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

В какой-то момент я опубликовал код движка, так как хотел поделиться наработками и подумал, что кто-то захочет присоединиться к разработке. Первая часть намерений воплотилась в жизнь. У движка появились свои пользователи, которые делали на них свои сайты. Так, на форуме поддержки 15 человек оставили от 10 сообщений, а двое самых активных пользователей написали больше 100 сообщений каждый.

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

Со временем я приобрел достаточный опыт в разработке и стал понимать, насколько тяжело дописывать новый код движка в старой парадигме. Я несколько раз пытался переписывать код с нуля. Сначала без фреймворков с «нормальным» объектным подходом (версия 2.0dev). Потом на микрофеймворке Silex. Потом авторы Silex отказались от его развития, и я подключил Symfony. Все эти попытки сделать версию 3.0 останавливались на том, что надо переделать на новую схему админку и расширения, и для такой объемной работы у меня не было времени и желания.

Одновременно с этим активность на форуме угасла. Авторы некоторых сайтов перенесли их на другие движки. Некоторых сайтов больше нет. Некоторые заброшенные сайты до сих пор работают на S2.

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

Доработка

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

У меня получилось за 1 января (обычно бесполезный день) подключить к S2 версии 2.0dev свежую версию поискового движка Rose, и при этом сделать так, чтобы в общем кодовом пространстве движка сосуществовали устаревший код, на который больно смотреть, и новый код, с которым приятно работать. Такой быстрый прогресс открыл дорогу к тому, чтобы сделать уже упоминавшуюся систему рекомендаций.

Также я внес несколько менее масштабных, но не менее желанных изменений. Перенес php-код расширений из xml-файлов (он выполнялся через eval()) в php-файлы, что минимизировало объем кода, выполняемого в eval(), и дало возможность делать нормальный рефакторинг и отладку этого кода в PhpStorm. Добавил загрузку картинок на сервер прямо из буфера обмена, минуя сохранение в промежуточные файлы. Отрегулировал расчет релевантности в самом поисковом движке Rose на настоящем живом сайте.

Еще подключил codeception — библиотеку для написания автотестов, и стал добавлять эти автотесты. Среди нескольких видов тестов пришлось выбрать приемочные (acceptance). В них выполняются настоящие http-запросы к отдельно запущенной копии приложения (в моем случае ко встроенному в PHP веб-серверу). Запускать и тесты, и код движка в одном процессе нельзя, потому что в случае ошибок выполняются die/exit, расставленные в куче мест, и их отрефакторить затруднительно.

Продуктовый подход

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

Альтернатива — забросить S2 и перейти на другой движок, хотя бы ту же Эгею Ильи Бирмана. Но для этого надо создать свою тему оформления, написать и отладить конвертер заметок, разобраться со старыми адресами URL, пройтись по всем заметкам и убедиться, что ни в одной ничего не сломалось (а ломаться есть чему: у меня есть заметки с нетривиальной версткой вроде рецензии на книгу о фильме «Интерстеллар»). Это значительный объем работы, которую нельзя делать понемногу, мелкими шагами. Мне проще было постепенно доработать свой движок.

Светлое будущее

Ха-ха, просто будущее :) Оно хорошо описывается фразой из заголовка статьи Бенджамина Эберлея, одного из разработчиков Doctrine ORM, о поддержке в опенсорсе: бесплатный софт, а не бесплатная поддержка.

S2 переехал на гитхаб, откуда его можно скачать. Версию 1.0 я пока что использую, поэтому еще некоторое время буду исправлять баги и проблемы совместимости со свежими версиями PHP. Новых фич в ней не будет. С версии 1.0 можно обновиться до 2.0dev, переработав стили.

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

На вопросы возможных пользователей движка я отвечать не планирую. Я не вижу перспектив в том, чтобы у движка появлялись новые пользователи. Сейчас соцсети, облачные платформы и конструкторы сайтов не оставляют движкам типа S2 какой-либо ниши для развития.

    5 комментариев

Как покрыть тестами устаревший код?

28 августа 2023 года, 12:22

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

Идея приемочных тестов в том, что приложение тестируется целиком, как есть. Для легаси-кода с процедурным подходом, хранением состояния в глобальных переменных, множеством точек выхода die/exit при ошибках и редиректах, перемешанной логикой и представлением чаще всего альтернатив нет.

Я расскажу на примере S2, как добавлять приемочные тесты в легаси-приложение. Ниже я приведу существенные упрощенные фрагменты кода и оставлю ссылки на полные версии файлов.

Проще всего Codeception подключить к проекту через composer:

    "require-dev": {
        "codeception/codeception": "^4.2",
        "codeception/module-asserts": "^2.0.0",
        "codeception/module-phpbrowser": "^2.0"
    }

В проекте нужно создать файл codeception.yml:

suites:
    acceptance:
        actor: AcceptanceTester
        modules:
            enabled:
                - Asserts
                - PhpBrowser:
                      url: http://localhost:8881
                      curl:
                          CURLOPT_TIMEOUT_MS: 120000

После этого файлы тестов можно писать в таком cest-синтаксисе:

<?php

use Codeception\Example;

class InstallCest
{
    public function tryToTest(AcceptanceTester $I, Example $example): void
    {
        $I->install('admin', 'passwd', $example['db_type'], $example['db_user'], $example['db_password']);

        $I->amOnPage('/');
        $I->see('Site powered by S2');
        $I->click(['link' => 'Page 1']);
        $I->see('If you see this text, the install of S2 has been successfully completed.');
        $I->canWriteComment();
    }
}

Здесь методы amOnPage(), see(), click() — встроенные, а install() и canWriteComment() — мои сокращения, определенные в классе-тестере:

<?php

use Codeception\Actor;

class AcceptanceTester extends Actor
{
    public function install(string $userName, string $userPass, string $dbType, string $dbUser, string $dbPassword): void
    {
        $I = $this;
        $I->amOnPage('/');
        $I->seeLink('install S2', '/_admin/install.php');
        $I->amOnPage('/_admin/install.php');
        $I->seeResponseCodeIs(200);
        $I->see('S2 2.0dev', 'h1');

        $I->selectOption('req_db_type', $dbType);
        $I->fillField('req_db_host', '127.0.0.1'); // not localhost for Github Actions
        $I->fillField('req_db_name', 's2_test');
        $I->fillField('db_username', $dbUser);
        $I->fillField('db_password', $dbPassword);
        $I->fillField('req_username', $userName);
        $I->fillField('req_password', $userPass);
        $I->click('start');
        $I->canSeeResponseCodeIs(200);
        $I->see('S2 is completely installed!');
    }

    public function canWriteComment(): void
    {
        $I = $this;

        $name = 'Roman 🌞';
        $I->fillField('name', $name);
        $I->fillField('email', 'roman@example.com');
        $I->fillField('text', 'This is my first comment! 👪🐶');
        $text = $I->grabTextFrom('p#qsp');
        preg_match('#(\d\d)\+(\d)#', $text, $matches);
        $I->fillField('question', (int)$matches[1] + (int)$matches[2]);
        $I->click('submit');

        $I->seeResponseCodeIs(200);
        $I->see($name . ' wrote:');
        $I->see('This is my first comment!');
    }
}

Теперь посмотрим, как это всё запускается. Я написал отдельный скрипт:

# Очистка тестовой базы данных
mysql -uroot --execute="DROP DATABASE IF EXISTS s2_test; CREATE DATABASE s2_test;"

# Запуск веб-сервера
APP_ENV=test \
 PHP_CLI_SERVER_WORKERS=2 \
 nohup php \
  -d "max_execution_time=-1" \
  -d "opcache.revalidate_freq=0" \
  -S localhost:8881 >/dev/null 2>&1 &

serverPID=$!

# Запуск тестов
php _vendor/bin/codecept run acceptance

pkill -P $serverPID # Убиваем воркеры PHP, образовавшиеся из-за PHP_CLI_SERVER_WORKERS
kill $serverPID

Перед запуском тестов поднимается встроенный в php веб-сервер, обрабатывающий запросы по адресу http://localhost:8881/. Веб-сервер запускает несколько воркеров в параллель (PHP_CLI_SERVER_WORKERS=2), так как движок в процессе установки обращается сам к себе, чтобы понять, какая схема перезаписи URL доступна. В процессе установки создается файл config.php. Чтобы PHP сразу видел изменения в этом файле, пришлось переопределить параметр из php.ini: opcache.revalidate_freq=0. Альтернатива — добавить sleep(), но я не хотел играться с ненадежными способами. Переменная окружения APP_ENV=test говорит движку, чтобы он вместо файла config.php создавал и использовал файл config.test.php. Это упрощает запуск и тестов и обычной версии для разработки из одной папки.

Достоинства получившегося способа написания приемочных тестов следующие. Устаревший код приложения практически не нужно дорабатывать, чтобы писать тесты. Так как приложение тестируется через HTTP API, внутренние изменения в приложении, не меняющие API, не требуют доработки тестов. Тесты запускаются где угодно, я даже добавил запуск тестов в github actions при каждом пуше веток.

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

    Оставить комментарий

Неудачная попытка включить JIT в PHP

30 августа 2023 года, 23:16

Обновил на этом сервере версию PHP с 7.4 на 8.2. Решил включить JIT-компиляцию и посмотреть, будет ли от нее эффект.

Оказалось, на JIT влияет только одна настройка, но она не очень-то и простая. Включил рекомендуемое значение, но никакого эффекта не заметил. Статистика JIT, выдаваемая через var_dump(opcache_get_status()['jit']);, показывала, что на самом деле JIT не включен.

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

PHP Warning:  JIT is incompatible with third party extensions that override zend_execute_ex(). JIT disabled. in Unknown on line 0

Поиск проблемы в интернете быстро подсказал причину: расширение для нью-релика использует устаревший вызов API и тем самым блокирует включение JIT. Проблеме на гитхабе скоро будет три года, и она до сих пор не решена. Я не был в курсе этой проблемы, когда хвалил нью-релик в своем рассказе. Не очень серьезный подход с их стороны — запускать новые продукты и не уделять должного внимания поддержке существующих, дающих ключевые преимущества перед конкурентами.

Попробую JIT как-нибудь в другой раз, всё-таки от нью-релика сейчас больше пользы, чем вреда. К тому же не очень понятно, как без нью-релика измерять эффект от внедрения: вряд ли в моем случае этот эффект будет заметен без инструментов мониторинга.

Добавлено 29.03.2024: Включил JIT несколько недель назад. Видимо, нью-релик выпустил новую версию, в которой проблема исправлена. Однако особого влияния на производительность по данным того же нью-релика не заметил. Значит, вычисления не являются бутылочным горлышком в коде движка.

    1 комментарий

← сюда туда →

Поделиться
Записи