Adopt articles for astro

This commit is contained in:
2026-03-07 11:14:50 +03:00
parent 50d032ce62
commit 4b04afb912
13 changed files with 818 additions and 2 deletions

View File

@@ -0,0 +1,134 @@
---
title: Гадалка Шеннона
description: Демо-версия электронной гадалки Шеннона
keywords: [гадалка, угадыватель, шеннон, чет-нечет]
---
import PredictorDemo from '../../components/PredictorDemo.vue';
В студенческое время я наткнулся на интересную статью об [игре "Чет-нечет"][game]
на домашней страничке пользователя [ltwood][ltwood].
Правила очень простые. Игрок загадывает один вариант из двух: "чет" или "нечет",
а оппонент пытается угадать выбор игрока. Если угадать не удалось, то очко получает
загадавший, а если угадать получилось - то угадывающий. Кто первым наберет 20 очков,
тот и молодец!
Кажется, что в этой игре все случайно. Случайно загадывается число, потом случайно
второй игрок пытается угадать что же было загадано. Я очень сильно удивился, когда
попробовал поиграть в эту игру с программой и за десять попыток так ни разу и не выиграл.
Парадокс в том, что мы _думаем_ что загадываем числа случайно. На самом деле все не так,
и последовательность загаданных чисел не случайна.
Исходного кода оригинальной гадалки в открытом доступе нет, есть только [описание алгоритма][algo],
по которому я сделал свою реализацию на TypeScrypt.
## Демоверсия
Попробуйте набрать 50 очков и выиграть. Чтобы выбирать вариант с клавиатуры,
кликните внутри серой рамки, а потом пользуйтесь клавишами "1" - нечет или "2" - чет.
---
<PredictorDemo client:visible />
---
## Как Это работает
Математически алгоритм на [странице][algo] сайта ltwood.
Я рассмотрю простой пример, чтобы показать принцип.
В основе алгоритма находится популяция "демонов" - автоматов, которые на основании ходов
игрока и предсказанных значениях выдают новое предсказание. Демонами управляет
супервайзер. Задача супервайзера в том, чтобы опросить всех демонов, выбрать ответ
от одного их них, а после получения ответа игрока пометить тех, кто выдал правильный ответ.
Алгоритм состоит из двух шагов:
- предсказать следующих ход игрока;
- учесть реальный ход игрока, добавив веса тому демону, который предугадал ход.
Рассмотрим работу на примере одного демона.
Пусть у нас есть демон, который смотрит на последний хода игрока
и на свое последнее предсказание.
Строим два вектора:
- `[<1 ход демона>, <1 ход игрока>, 0]`
- `[<1 ход демона>, <1 ход игрока>, 1]`
В самом начале, когда у демона нет никакой информации о ходах игрока, эти векторы
будут выглядеть как `[0]` и `[1]`. Но с накоплением данных, они всегда будут каждый
по 5 элементов.
После чего смотрим, который из таких наборов в прошлом приносил победу чаще,
и соответственно выбираем или вариант с 0, или с 1.
После получения действительного хода игрока, мы увеличиваем вес того набора,
который оказался верным. И далее снова предсказываем ход.
Теперь с числами.
#### Ход 1
У демона нет информации, наборы `[0]` и `[1]` равнозначны, выбираем `[0]`,
а значит предсказываем ход игрока 0.
Игрок загадывал 1. Обновляем веса:
```
[0] = 0
[1] = 1
```
#### Ход 2
Строим векторы на основе последних ходов:
```
0: [0, 1, 0]
1: [0, 1, 1]
```
Для этих векторов тоже еще нет весов, так что снова выбираем первый, предсказываем 0.
Игрок снова выбрал 1. Обновляем веса (помним, что еще были прошлые вектора из одного элемента):
```
[0] = 0
[1] = 1
[0, 1, 0] = 0
[0, 1, 1] = 1
```
#### Ход 3
Картина такая же, как на втором ходу, но отличие в том, что у нас есть веса с прошлого хода:
```
0: [0, 1, 0] - 0
1: [0, 1, 1] - 1
```
Выбираем вариант 1, игрок снова выбирает 1. Предсказание удалось!
## Расширение алгоритма
Это был самый элементарный вариант. Понятно, что на таком далеко не уедешь,
и никого не обыграешь. Чтобы хорошо предугадывать ходы игроков, используется
несколько демонов с разной величиной просматриваемой истории. Следит за ними
"супервайзер", который ведет для каждого демона рейтинг. На основе этого рейтинга
выбираются ответы тех демонов, которые были наиболее успешны в своих предсказаниях.
## Ссылки
- [Код гадалки][repo]
- [Описание алгоритма][algo]
- [Описание игры у ltwood][game]
[ltwood]: https://sites.google.com/site/ltwood/
[game]: https://sites.google.com/site/ltwood/projects/heshby
[algo]: https://sites.google.com/site/ltwood/projects/heshby/algorithm
[repo]: https://github.com/anwinged/predictor

View File

@@ -0,0 +1,39 @@
---
title: Сериализация в PHP
description: Проблема долговременного хранения сериализованных данных
keywords: [php, serialization, сериализация, пхп]
---
В PHP есть две функции для сериализации и десериализации данных: `serialize()` и
`unserialize()`. Функции встроены в язык, не требуют дополнительных модулей.
В один момент кто-то решает использовать их для долговременного хранения
объектов. В базе данных, на диске, еще где-то.
```
namespace Test\Serialize;
class A {}
$a = new A();
serialize($a);
```
И тут начинаются проблемы.
Дело в том, что при сериалзации объектов классов кроме самих данных объекта
сохраняется еще и информация о классе. Его имя, пространство имен.
Результатом сериализации в примере выше будет:
```
O:16:"Test\Serialize\A":0:{}O:16:"Test\Serialize\A":0:{}
```
Если теперь произвести рефакторинг, переместить класс, изменить пространство
имен, то десериализация уже не сработает. И будет больно.
Не делайте так.
Контролируйте процесс сериализации. Например, используйте JSON и специальные
функции для превращения объекта в массив и обратно.

View File

@@ -0,0 +1,85 @@
---
title: Сторителлинг
description: Конспект видеолекций Алексея Каптерева о сторителлинге и презентации
keywords: [story, storytelling, lectures, история, сторителлинг, каптерев]
---
Это конспект трех лекций Алексея Каптерева, где он рассказывает о презентациях.
Как придумать историю, как подготовить слайды и что рассказывать.
- [https://youtu.be/PaoKhNLyxdk](https://youtu.be/PaoKhNLyxdk)
- [https://youtu.be/18M9ZJRU2wI](https://youtu.be/18M9ZJRU2wI)
- [https://youtu.be/KyyAgiw8B6I](https://youtu.be/KyyAgiw8B6I)
## История
Самый главный вопрос: "Чего я хочу?", а второй - "Что им нужно?" Далее нужно
найти пересечение ответов на эти два вопроса. "Что я хочу, чтобы у вас
получилось?"
Эмоции = Мотивация
Я хочу, чтобы они
- поняли, что...
- изменили мнение о...
- сделали...
Кто? Кому? Что? Зачем? Как?
История = интересная тема + близость темы + сценарий
История = факты + смысл + эмоции
Основные составляющие истории:
- **Герой** Кто хочет? Я сам, клиент, клиент клиента.
- **Цель** Что хочет?
- **Проблема / слабость** Что мешает, какая проблема? Почему он не может без
этого жить?
- **Злодей** Кто мешает?
- **Решение** Пути обхода? В чем инсайт?
- **Цена** Плата за решение?
- **Мораль** В чем призыв к действию? Если вы сделаете Х, то будет Y, иначе Z.
Факт -> Проблема -> Решение -> Но не все так просто -> Теперь уж точно
План, зум (уточнение) частей плана - деревья представления.
Концовка - самое тяжелое, начните с концовки.
Говорите то, что вас зажигает.
Чтобы заинтересовать человека в письменном виде, нужно сделать письмо
максимально похожим на устную речь. С героем, целью и так далее.
## Презентации
Таблицы для анализа, презентации для результата.
1. Монотонность - враг истории.
2. Дело не в "умении выступать" (владейте сутью, слова найдутся).
3. История - это не сложно. Экспозиция, проблематизация, решение, выводы.
4. Не начинайте с PowerPoint.
5. Вы не можете сказать всего (Скажите 5% от того, что вы знаете). Одна хорошая
мысль и _очень_ много дисциплины.
6. Лучше один раз увидеть (чем 100 раз услышать и 10 раз потрогать).
7. Слайды - они как дети. Уродливые, зато свои. Не увлекаться форматированием.
Чтобы выделить главное, нужно ответить на вопрос "Какова цель этого слайда?"
8. Дизайн - это вычитание. Пропорции, цвета, типографика.
9. Контакт глаз. При выступлении смотрите людям в глаза. Это значит, что вы не
врете.
10. Правда делает нас свободными. Верьте в то, что говорите. Аудитория хочет
правды, настоящности. Правда, сострадание, самоирония.
## Слайды
Цель слайда: напомнить, впечатлить, объяснить, доказать. Определить цель, делать
простым, смотреть на слайд со стороны аудитории.
- Фотографии. Размер (большой), смысл, честность, логика. Не использовать
фотографии для схем.
- Схемы. Простота, пошаговость, направление. Пошаговое разжевывание схем.
Схемы "как это устроено" и "как это работает". Схема-исория лучше
схемы-модели.
- Статистика. Результат (а не анализ), ничего лишнего, честность.

View File

@@ -0,0 +1,69 @@
---
title: Яндекс.Диск для хостинга картинок
keywords: [яндекс.диск, хостинг картинок, yandex disk, image hosting, hosting]
---
У [Яндекс.Диска][ya-disk] есть замечательная функция. Он может создавать превью
загруженных фотографий. Эта функциональность не афишируется, но описана
в [документации][ya-api-preview].
У меня есть фотография на Диске `/img/kemsky.jpg`. Чтобы получить ее превью,
нужно выполнить запрос:
```
GET /img/kemsky.jpg?preview&size=XS
User-Agent: my_application/0.0.1
Host: webdav.yandex.ru
Authorization: OAuth 0c4182a7c2cf4521964a72ff57a34a07
```
Но есть проблема. Для запросов нужен токен. Без токена не получится использовать
это API для публичного хостинга.
Решение - сервер [Caddy][caddy] в качестве прокси. Caddy очень
удобно использовать в качестве фронтенда для внутренних сервисов.
Он просто настраивается, а самое главное - поддерживает автоматический
выпуск и обновление SSL-сертификатов буквально одной строчкой конфига.
Скроем токен в конфигурации сервера, и будем передавать его при обращении
к Яндекс.Диску:
```
preview.vakhrushev.me {
proxy /img https://webdav.yandex.ru {
transparent
header_upstream User-Agent "yandex-disk-previewer/1.0"
header_upstream Authorization "OAuth 0c4182a7c2cf4521964a72ff57a34a07"
}
tls anwinged@ya.ru
}
```
Директива `proxy /img` будет направлять все запросы с `preview.vakhrushev.me/img`
на `https://webdav.yandex.ru/img`. Таким образом во внешний
мир будет смотреть только директория `img`, а остальные останутся скрытыми.
Кроме OAuth авторизации можно использовать Basic, передавая логин и
[пароль приложения][app-password]. Мне этот способ удобнее,
чтобы не заморачиваться с OAuth. Логин и пароль я храню
зашифрованными с помощью [Ansible Vault][vault].
И строчка с заголовком тогда будет выглядеть так:
```
header_upstream Authorization "Basic {{ '{{' }} (yandex_disk.login ~ ':' ~ yandex_disk.password) | b64encode {{ '}}' }}"
```
А так будет выглядеть ссылка на картинку:
```
https://preview.vakhrushev.me/img/kemsky.jpg?preview&size=XXL
```
![Кемский поселок](https://preview.vakhrushev.me/img/kemsky.jpg?preview&size=XXL)
[ya-disk]: https://disk.yandex.ru
[ya-api]: https://yandex.ru/dev/disk/doc/dg/concepts/quickstart-docpage/
[ya-api-preview]: https://yandex.ru/dev/disk/doc/dg/reference/preview-docpage/
[caddy]: https://caddyserver.com/
[app-password]: https://yandex.ru/support/passport/authorization/app-passwords.html
[vault]: https://docs.ansible.com/ansible/latest/user_guide/vault.html

View File

@@ -0,0 +1,25 @@
---
title: Как проектировать хайлоад, видео
keywords: [highload, высоконагруженные системы, онтико, олег бунин]
---
Нашел три замечательных видеоролика о высоконагруженных системах.
Архитектура, подходы, планирование, проблемы.
## Часть 1
<div class="youtube-embed-container">
<iframe src="https://www.youtube.com/embed/KmIE5K6adus" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
## Часть 2
<div class="youtube-embed-container">
<iframe src="https://www.youtube.com/embed/sCm4qUw28y4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
## Часть 3
<div class="youtube-embed-container">
<iframe src="https://www.youtube.com/embed/MG8-HmgOXlk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

View File

@@ -0,0 +1,16 @@
---
title: Интересные блоги о программировании
description: Сборник интересных блогов и статей о программирования для себя
keywords: [блоги, программирование, сборник, чистый код, ооп, haskell]
---
## Дизайн приложений
- Сайт Мартина Фаулера - [martinfowler.com](https://martinfowler.com/).
Архитектура, рефакторинг, чистый код.
- Блог Роберта Мартина, или дядюшки Боба, о чистом коде - [The Clean Code Blog](https://blog.cleancoder.com/).
## Функциональное программирование
- Хорошие статьи о функциональном программировании с примерами на F# - [F# for fun and profit](https://fsharpforfunandprofit.com/).
- Блог [Alexis King](https://lexi-lambda.github.io/) о Haskell и Racket.

View File

@@ -0,0 +1,122 @@
---
title: О полях-дискриминаторах
description: Заметка о том, как записывать конфигурацию для сложных объектов
keywords: [чистый код, дискриминатор, php, конфигурация]
---
Поля-дискриминаторы - это удобный прием для обработки нескольких типов
данных со схожей структурой.
Лучше начать с примера.
Допустим, у нас есть объект-фильтр для целых чисел. Можно применить фильтр
к последовательности чисел и получить новую последовательность.
Его параметры выглядят следующим образом:
```json
{
"from": 0,
"to": 10
}
```
Отлично.
Фильтр пропустит только числа от 0 до 10.
По такой конфигурации без проблем можно создать объект.
Теперь добавим второй фильтр - он будет отсекать нечетные числа.
```json
{
"odd": false
}
```
## Создаем фильтр на основе структуры
Теперь есть два фильтра, и нужно понимать какой их них создать.
Простое и наивное решение - смотреть на структуру полей.
Если есть поле `odd`, то фильтр нечетных чисел, иначе - фильтр по диапазону.
```php
if (isset($config['odd'])) {
// фильтр нечетных чисел
} else {
// фильтр диапазона
}
```
У этого решения масса недостатков.
Например, если поле `odd` понадобится двум фильтрам сразу, то условие усложнится.
Или появятся поля с разными названиями, но одинаковым смыслом.
## Добавляем поле-дискриминатор
Решение проблемы в добавление специального поля, в котором содержится имя фильтра.
Назовем это поле `type`. Тогда конфигурации фильтров будут выглядеть следующим образом:
```json
{
"type": "range",
"from": 0,
"to": 10
}
```
```json
{
"type": "odd_control",
"odd": false
}
```
Поле `type` - это и есть поле-дискриминатор.
По нему можно точно определить какой фильтр перед нами.
```php
switch ($config['type']) {
case 'range':
// фильтр диапазона
break;
case 'odd_control':
// фильтр нечетных чисел
break;
default:
throw new \LogicException(sprintf(
'Unknown filter type "%s"', $config['type']));
}
```
## Выделить зависимые поля
Решение еще можно улучшить.
Сейчас на одном уровне в структуре конфига есть и обязательные поля, и необязательные поля.
Мы можем перенести все необязательные поля на дополнительный уровень,
например, в поле `params`.
Эти поля необязательны для всей конфигурации, но обязательны для конкретного фильтра.
```json
{
"type": "range",
"params": {
"from": 0,
"to": 10
}
}
```
Что дает такой маневр?
На верхнем уровне конфигурации у нас будет постоянная структура.
Поле `params` можно до определенного времени рассматривать как черный ящик,
просто зная, что это некий словарь параметров.
Когда будет понятно какой фильтр создавать, тогда этот набор параметров
послужит базой для создания объекта-фильтра.
## Заключение
- Если структура конфигурации предполагает создание нескольких разных объектов,
то лучше использовать специальное поле-дискриминатор для точного указания
типа создаваемого объекта.
- Все поля, которые зависят от типа, лучше перенести на дополнительный уровень,
тем самым сохранив структуру верхнего уровня постоянной (на верхнем уровне
будет всегда один и тот же набор полей).

View File

@@ -0,0 +1,177 @@
---
title: Организация доступа к nullable полям класса
description: Заметка о том, лучше организовать доступ к полям класса, которые могут содержать значение null
keywords: [чистый код, php, "null", поля класса]
---
Нередкая ситуация, когда в классе есть поле, которое может содержать `null`.
```php
class User
{
private Email $email;
private ?string $name;
}
```
Пользователь может указать имя, а может и не указывать,
ограничившись только почтовым адресом.
А далее мы пишем код, которые работает с полем имени.
```php
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
И использование этого кода:
```php
/** @var User $user */
if ($user->hasName()) {
do_something_with_name($user->getName());
}
function do_something_with_name(string $name) {}
```
Выглядит хорошо.
Сначала убедились, что имя установлено, а потом использовали его.
Но статический анализатор нам обязательно припомнит, что мы пытаемся передать
в функцию `do_something_with_name` значение типа `string|null`, хотя функция
ожидает значение типа `string`.
И получается дурацкая ситуация, что формально мы должны дописать
еще одну проверку.
```php
/** @var User $user */
if ($user->hasName()) {
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
}
function do_something_with_name(string $name) {}
```
Статический анализатор наш друг, он помогает находить ошибки и несоответствия
в коде.
И здесь он нашел такое формальное несоответствие типов.
Статический анализатор прав, а мы, как проектировщики интерфейса, не правы.
На самом деле мы смешали два подхода, когда описывали методы в нашем классе:
1. Получить и проверить
2. Проверить и получить
## Получить и проверить
И сразу начнем с примера использования.
```php
$name = $user->getName();
if ($name !== null) {
do_something_with_name($name);
}
```
Сначала мы получаем значение поля, а потом проверяем, соответствует ли это
значение нашим требованиям. Класс при этом будет построен вот так:
```php
class User
{
private ?string $name;
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
Заметьте, здесь уже нет метода `hasName()`, потому что этот метод перестал быть
нужным. Его роль исполняет метод `getName()`.
## Проверить и получить
Второй подход: сначала проверяем значение, а потом работаем с ним:
```php
if ($user->hasName()) {
do_something_with_name($user->getName());
}
```
Структура класса:
```php
class User
{
private ?string $name;
public function hasName(): bool
{
return $this->name !== null;
}
public function getName(): string
{
if ($this->name === null) {
throw new \LogicException('Name is not set');
}
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
}
```
Смотрите отличия.
Метод `hasName()` остается.
А вот метод `getName()` теперь возвращает значение типа `string`.
Он выбросит исключение, если мы попытаемся получить значение,
которое не установлено.
## Использование
Теперь встает вопрос, когда и какой подход следует использовать.
- Если ситуация, когда поле не установлено, скорее исключительная, нежели
обычная, то можно использовать второй подход, а проверку опустить.
Исключение в методе `getName()` позволит обнаружить странное поведение.
- Если в пустом поле нет ничего не обычного, то подход "получить и проверить"
будет удобнее, все равно нужно делать проверку.
В любом случае, нужно смотреть на уместность того или иного подхода в каждом
случае, и не использовать их одновременно.