Блог Анатолия Гладкого

Разрабатываю приложения для бизнеса на .NET, Node.js и Go. Налаживаю процессы в команде разработчиков. Тащусь от оптимизации и рефакторинга. Люблю собирать конструкторы и играть в автосимуляторы.

Для связи: me@agladky.ru и телеграм.

Новые посты советую получать через РСС. Также сообщаю о них в твитере, фейсбуке и гугл плюсе.

Мой гитхаб. Последний проект — todoist-overdue-alfred. Расширение для альфреда. Перемещает на сегодня все просроченные задачи в тудуисте.

13 декабря 2016   я  

Это краткая выжимка из рабочего доклада по работе с async/await в C#. Для наглядности, параллельно рассматривается подход с использованием блока ContinueWith.

Основные паттерны асинхронного программирования

async/await — синтаксическая обертка над задачами

  • await ставит на паузу текущий метод, ожидая выполнения задачи.
  • Выглядит как блокирующая (синхронная) операция.
  • Не блокирует текущий поток.
  • Выполнение продолжается в том же контексте, из которого была вызвана задача, если явно не указано иное.
  • Ключевое слово async указывается, чтобы среда исполнения воспринимала await как ключевое слово.
  • await метод начинает выполняться синхронно. Если он уже закончил свое выполнение то новый поток не создается. Все продолжается в том же потоке. Подробнее в ответе на stackoverflow.com.
  • await работает с любым типом, для которого реализован метод GetAwaiter(). Подробнее в статье - await anything.

Демонстрационное приложение

Примеры показываются на тестовом Windows Form приложении. GitHub репозиторий с приложением.

async/await app

Асинхронные действия лежат в PeopleRepositoryAsync:

public class PeopleRepositoryAsync
{
    public async Task<List<string>> GetPeopleList()
    {        
        await Task.Delay(2000);                       
        return new List<string>
        {
            "John Smith",  
            "Ivan Ivanov",
            "Joao Fetucini"
        };
    }
}

Метод GetPeopleList() асинхронно ожидает 2 секунды и возвращает список пользователей.

Первое сравнение TAP и async/await подхода

Реализация с Task и ContinueWith

Добавим код для получения списка пользователей в обработчик нажатия кнопки “Fetch Data (with Task)” - buttonTask_Click:

Task<List<string>> peopleTask = Repository.GetPeopleList();
List<string> people = peopleTask.Result;

Этот код не будет выполняться асинхронно. Он будет ожидать завершение задачи peopleTask в основном потоке, поэтому UI заморозится. Добавим конструкцию ContinueWith(t => { }):

peopleTask.ContinueWith(t =>
{
  List<string> people = peopleTask.Result;
});

Теперь задача по получению пользователей выполнится асинхронно. После её завершения выполнится код в блоке ContinueWith. Добавим в ContinueWith отображение полученных имен в textBoxMain:

textBoxMain.AppendText($"{Environment.NewLine}Person list:{Environment.NewLine}");
foreach (var person in people)
{
    textBoxMain.AppendText($"- {person}{Environment.NewLine}");
}

Если запустить приложение и нажать на “Fetch Data (with Task)”, то возникнет ошибка. Все потому, что код в блоке ContinueWith выполняется в потоке, отличном от того где находится SynchronizationContext UI потока. Для выполнения в нужном потоке добавим в вызов метода ContinueWith аргумент TaskScheduler.FromCurrentSynchronizationContext():

peopleTask.ContinueWith(t => { ... }, TaskScheduler.FromCurrentSynchronizationContext());

Теперь приложение работает корректно. Перейдем к реализации этого кода с помощью ключевых слов async и await.

Реализация с async/await

Основное отличие от предыдущей реализации — код будет похож на синхронный. Перейдем в обработчик нажатия кнопки “Fetch Data (with await)” - buttonAwait_Click. Добавим код для получения списка пользователей:

List<string> people = await Repository.GetPeopleList();

В объявление метода добавим слово async, чтобы среда исполнения поняла что await это ключевое слово, а не просто переменная. Вставим без изменений код из ContinueWith:

textBoxMain.AppendText($"{Environment.NewLine}Person list:{Environment.NewLine}");
foreach (var person in people)
{
    textBoxMain.AppendText($"- {person}{Environment.NewLine}");
}

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

Обработка ошибок

Для демонстрации добавим в метод GetPeopleList код вызова ошибки (после Task.Delay(2000)):

throw new NotImplementedException("Метод не реализован!");

Обработка ошибок для await метода

Сначала рассмотрим самый простой случай. Отлов и обработка ошибок для метода с await происходит как в синхронном коде:

try
{
    // Получение и вывод значений из репозитория
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message, "ОШИБКА");
}
finally
{
    // Критичный к выполнению код
}

Всё. Дополнительно писать ничего не надо, ошибка будет поймана.

Обработка ошибок для Task метода

Для метода с блоком ContinueWith обработать ошибки можно несколькими способами.

Первый, использовать еще один вызов ContinueWith на задаче. Вызовем ContinueWith с 2 дополнительными параметрами:

peopleTask.ContinueWith(t =>
    {
        // Получение и вывод значений из репозитория
    },
    CancellationToken.None,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.FromCurrentSynchronizationContext());

Главное — аргумент TaskContinuationOptions.OnlyOnRanToCompletion. Он указывает, что блок кода выполнится только если в задаче не было ошибок.

Теперь, код для обработки ошибки:

peopleTask.ContinueWith(t =>
    {
        foreach (var exception in t.Exception.Flatten().InnerExceptions)
        {
            MessageBox.Show(exception.Message);
        }
    },
    CancellationToken.None,
    TaskContinuationOptions.OnlyOnFaulted,
    TaskScheduler.FromCurrentSynchronizationContext());

Опция OnlyOnFaulted указывает, что код в блоке выполниться только при ошибке в задаче. Оператор foreach разворачивает ошибки в «плоское» состояние, т. к. все ошибки представляются в виде иерархии и оборачиваются в AggregateException.

Для имитации блока finally, напишем:

peopleTask.ContinueWith(t =>
    {
        // Критичный к выполнению код
    },
    CancellationToken.None);

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

peopleTask.ContinueWith(t =>
    {
        if (t.Status == TaskStatus.RanToCompletion)
        {
            // Получение и вывод значений из репозитория
        }
        if (t.Status == TaskStatus.Faulted)
        {
            // Обработка ошибок
        }
        // (finally) Критичный к выполнению код
    },
    TaskScheduler.FromCurrentSynchronizationContext());

Отмена действий

Обновим метод GetPeopleList класса Repository. Добавим параметр CancellationToken и точку отмены после вызова Task.Delay(2000):

public async Task<List<string>> GetPeopleList(CancellationToken cancellationToken = new CancellationToken())
{
    await Task.Delay(1500, cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    return new List<string> { ... };
}

Обратит внимание, что отмена произойдет только после ожидания в 2 секунды. Само действие Task.Delay не прерывается.

Для операции отмены используем кнопку “Cancel request” с обработчиком buttonCancel_Click. Объекту CancellationToken можно задать значение только при инициализации. Поэтому создадим переменную CancellationTokenSource. Она позволяет генерировать токены и изменять их состояние во время выполнения.

В класс MainForm добавим поле:

private CancellationTokenSource _tokenSource;

А в обработчик buttonCancel_Click код для подачи токену сигнала отмены:

_tokenSource.Cancel();

Обработка отмены для async метода

В обработчике buttonAwait_Click изменим вызов метода GetPeopleList(), добавив инициализацию _tokenSource и передав сгенерированный токен в качестве аргумента:

_tokenSource = new CancellationTokenSource();
List<string> people = await Repository.GetPeopleList(_tokenSource.Token);

Добавим обработку операции отмены:

catch (OperationCanceledException ex)
{
    MessageBox.Show(ex.Message, "Canceled");
}

Обработка отмены для Task метода

Для buttonTask_Click добавим похожий код для передачи токена:

_tokenSource = new CancellationTokenSource();
Task<List<string>> peopleTask = Repository.GetPeopleList(_tokenSource.Token);

Для обработки операции отмены в блок ContinueWith добавим:

if (t.Status == TaskStatus.Canceled)
{
  MessageBox.Show("Operation Canceled", "Canceled");
}

Deadlocks

Добавим в класс Repository метод DeadlockTestAsync():

public async Task DeadlockTestAsync()
{
    await Task.Delay(1500);
    Console.WriteLine("Done!");
}

Вызовем этот метод в обработчике кнопки “Deadlock”:

async void buttonDeadlock_Click
{
    Repository.DeadlockTestAsync().Wait();
}

Все. При нажатии на кнопку возникнет Deadlock. Почему? Рассмотрим по пунктам:

  1. DeadlockTestAsync() вызывается на потоке с UI.
  2. Task.Delay() запускается в новом потоке.
  3. await захватывает SynchronizationContext и подключает continuation для выполнения действий после завершения.
  4. Вернемся к вызову DeadlockTestAsync().
  5. Wait() ждет завершение задачи в UI потоке.
  6. Task.Delay() ожидает выполнить продолжение на UI потоке.
  7. Но поток в ожидание - Дедлок!
  8. Все потому, что задача не вернется из DeadlockTestAsync(), пока не выполнится “продолжение”.

Для избежания подобной ситуации, в библиотеках, лучше писать .ConfigureAwait(false):

await Task.Delay(1500).ConfigureAwait(false);

Это позволит выполнить “продолжение” в том же потоке, в котором работала задача. В моем примере это будет поток, отличный от UI потока.

Полезные ссылки

13 декабря 2016   .net  

Для разработчика, вести блог на гитхабе — отличная идея.

Я прошёл путь от простой хотелки перейти на статический блог до разработки шаблона и применения инфостиля. Расскажу как это было.

Начало

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

Решил попробовать для блога. Для генерации сайта из кода использую hugo. Быстрый, изменения моментально отображаются в браузере при запуске на локальном сервере. Понимает маркдаун для статей. Написан на го, а не руби, как рекомендуемый гитхабом jekyll.

Дизайн

Не приглянулась ни одна тема из каталога. Написал свою. Это отняло много времени. Очень. Забросил написание статей на полгода. Из полученных плюсов — прокачал навыки верстки, получил опыт работы с CSS препроцессором less. Узнал о БЭМ методологии, но не успел применить. Не пожалел что потратил на тему столько времени.

Как перфекционист, полировал бы тему и дальше. Но, в какой-то момент понял — надо выпускать. Последовал методу прогрессивного джипега — доделал критическое, остальное по мере сил и времени. Из крупного — не стал доделывать страницу о себе. Она пока повисит в закрепленном посте у которого, при выходе нового, обновляется время, чтобы он оставался вверху. Без решения остановки, делал бы тему еще полгода и не выпускал новые посты.

Редактура

Во время перехода, стал интересоваться редактурой Максима Ильяхова. Прошел базовый курс, подписался на продвинутый. Кайфую. Исчезла тяга к графоманству, начал четче выражать мысли в письме. Из минусов — вокруг оказалось много плохого текста. Тянет переписывать. Начал с себя. Переписал посты в блоге. Это отодвинуло время перехода на полгода. Дольше всего редактировал пост о первом скрипте на питоне. Но, результатом доволен. Можете прогнать на glvrd.ru — рейтинг больше 9.

Ощущения

Мне понравилось писать свой блог для статического хостинга. Никаких админок, только редактор и код. Он также подойдет для небольшого сайта о компании или продукте. Буду и дальше использовать github pages.

10 сентября 2016   блог  

Не каждый день встретишь человека с такой уверенностью в своей правоте и идущего против системы.

Послушал 105 серию подкаста «Разбор полетов». Веселый выпуск. Гость, Егор Бугаенко, убеждён что большинство использует ООП неправильно. Не понимают парадигмы. Ведущие с ним не соглашаются. Море троллинга и споров. Послушайте, узнаете как далеко можно зайти в своих убеждениях.

После, советую пролистать блог Егора. Прав он или нет, но интересные мысли почерпнете.

5 сентября 2016   подкасты  

Маленькая хитрость, которую я открыл совсем недавно. Для добавления переменной в окно Watch, необязательно вводить туда значение или копировать и вставлять из редактора. Просто перетащите её:

Второе окно Watch 2 обрело для меня смысл и, главное, легкость в использовании:

Сайт с полезными советами для .Net разработчиков — dailydotnettips.com.

25 февраля 2016   .net   ide  

Одно из нововведений в С# 6 — оператор ?.. Давайте рассмотрим, где и как его использовать.

Преобразование цепочки вызовов

С новым оператором уменьшается количество проверок на null в цепочке вызовов:

var documentName = taskManager.CurrentTask == null ? null :
  (taskManager.CurrentTask.GetDocument() == null ? null :
    taskManager.CurrentTask.GetDocument().Name);

Перепишем в одну строчку используя ?.:

var documentName = taskManager.CurrentTask?.GetDocument()?.Name;

documentName примет значение null, если CurrentTask, GetDocument() или Name вернет null.

Второй вариант компактнее. Также, в первом блоке кода метод GetDocument() вызывается два раза, а во втором — один. Для реализации подобного поведения без использования ?. нужна дополнительная переменная для сохранения значения из GetDocument().

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

Рассмотрим получение значения из коллекции по индексу, с проверкой набора значений на null:

var tasks = (taskManager.Tasks != null) ? taskManager.Tasks[index] : null;

С новым оператором проверка на null условным оператором не расписывается. Бонус — выражение в 2 раза короче:

var tasks = taskManager.Tasks?[index];

Оборачивание в нулевые типы

Результат выполнения String.Equals()bool. Скомпилируется ли следующий код?

public void Main()
{
  String str = "x";
  bool result = str?.Equals("x");
}

Нет. Возникнет ошибка: Cannot implicitly convert type ’bool?’ to ’bool’. Такое поведение ожидаемо, так как str?.Equals("x") вернет null если переменная str не инициализирована и bool в остальных случаях.

В подобных ситуациях, при использовании оператора ?., возвращаемый тип функции T будет оборачиваться в Nullable<T>.

Использование в условии

Есть задача — проверить коллекцию на наличие в ней элементов. Типичный код:

if (taskManager.Tasks != null && taskManager.Tasks.Any())
  Console.Write("Tasks has items!");

Попробуем переписать:

if (taskManager.Tasks?.Any())
  Console.Write("Tasks has items!");

Код не скомпилируется. Как написано в предыдущем пункте, taskManager.Tasks?.Any() вернет Nullable<bool> тип, который нельзя однозначно трактовать в условии if. Дополним код:

if (taskManager.Tasks?.Any() == true)
  Console.Write("Tasks has items!");

Работает. И на одно условие стало меньше.

Комбинируя с оператором ??, перепишем в эквивалентный код:

if (taskManager.Tasks?.Any() ?? false)
  Console.Write("Tasks has items!");

Комбинация с оператором ??

Оператор ?? называется оператором объединения со значением null. Если операция возвращает null, оператор ?? подставит значение из правой части выражения.

var documentName = taskManager.CurrentTask?.GetDocument()?.Name ?? "No Name";

Если CurrentTask, GetDocument() или Name вернет null, то переменная примет значение "No Name".

Использование с делегатами

Стандартный код для вызова делегата:

var handler = this.PropertyChanged;
if (handler != null)
  handler()

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

PropertyChanged?.Invoke(e)

Компилятор создает код для вычисления PropertyChanged только один раз, запоминая результат во временной переменной.

Обратите внимание, следующий код вызовет ошибку:

PropertyChanged?(e)

Вызов метода Invoke обязателен.

Полезные ссылки

17 февраля 2016   .net  

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

Операторы == и === проверяют значения на совпадение, используя различные определения совпадения. Оператор идентичности (identity) === проверяет операнды на «идентичность», руководствуясь строгим определением совпадения. Оператор равенства (equality) == проверяет по менее строгим правилам, допускающим преобразования типов.

Оператор идентичности === вычисляет значения своих операндов, а затем сравнивает, без преобразования типов. Он руководствуется правилами:

  • Если у значений разные типы — они не идентичны.
  • Если оба значения или null или undefined — они идентичны.
  • Если оба значения или true или false — они идентичны.
  • Если одно или оба значения — NaN — они не идентичны. (Значение NaN никогда не идентично никакому значению, даже самому себе. Чтобы проверить значение x на NaN, используйте выражение x !== x. Только для NaN такая проверка вернет true).
  • Если оба операнда это числа с одним и тем же значением — они идентичны. Если одно число равно 0, а другое -0, они также идентичны.
  • Если оба значения это строки и содержат одни и те же 16-битные значения в одинаковых позициях — они идентичны. Две строки могут иметь один и тот же смысл и одинаково выглядеть на экране, но содержать отличающиеся последовательности 16-битных значений. Интерпретатор JavaScript не выполняет нормализацию символов юникода, поэтому подобные пары строк не считаются операторами === и == ни равными, ни идентичными.
  • Если оба значения ссылаются на один и тот же объект, массив или функцию — они идентичны. Если они ссылаются на различные объекты — они не идентичны, даже при идентичных свойствах.

Оператор равенства == похож на оператор идентичности, но он использует менее строгие правила. Если у значений разные типы — они преобразуются и сравниваются:

  • Если у значений одинаковый тип, они проверяются на идентичность, как описано выше.
  • Если значения не относятся к одному типу, оператор == считает их равными, при следующих правилах:
    • Если одно значение null, а другое undefined — они равны.
    • Если одно значение число, а другое строка, то строка преобразуется в число и выполняется сравнение.
    • Если одно значение — true, оно перед сравнением преобразуется в 1. Если — false, оно преобразуется в 0 и сравнение выполняется снова.
    • Если одно значение число или строка, а другое — объект, то перед сравнением объект преобразуется в простой тип. Встроенные классы преобразуются методом valueOf(), если не получилось, то toString(). Класс Date всегда выполняет преобразование toString(). Не базовые объекты джаваскрипта сами определяют способ преобразования в простые типы.
    • Любые другие комбинации значений не равны.

Правила преобразования типов и сравнения значений для оператора равенства == сложные и труднозапоминаемые. Интересные случаи:

'' == '0'           // false
0 == ''             // true
0 == '0'            // true

false == 'false'    // false
false == '0'        // true

false == undefined  // false
false == null       // false
null == undefined   // true

' \t\r\n ' == 0     // true

Особый случай — сравнение литерал с объектом:

"abc" == new String("abc")    // true
"abc" === new String("abc")   // false

Здесь, оператор == проверяет значение объектов и возвращает true. Оператор === возвращает false, т.к. у объектов различные типы. Какое поведение корректно? Зависит от того, что сравнивать. Но лучше обойти вопрос, и не использовать конструктор для создания строковых объектов.

В заключении — таблицы сравнения значений для операторов равенства и идентичности с сайта dorey.github.io.

Для оператора == (или !=):

Для оператора === (или !==):

Из таблиц следует вывод, что поведение == не очевидно и может только запутать. Используя === можно быть уверенным в возвращаемом значении.

Интересная статья: When is it OK to use == in JavaScript?

2 февраля 2016   javascript  

Конспект — вольный перевод одного из лучших циклов статей о монадах. Эрик Липперт, на протяжении 13 глав, отвечает на вопрос:

Я C# разработчик без опыта в «функциональном программировании». Что такое «монада» и как можно её использовать для себя?

Оригинальный цикл статей доступен по тегу monads.

Часть первая

Монада в функциональном программировании — абстракция линейной цепочки связанных вычислений. Её основное назначение — инкапсуляция функций с побочным эффектом от чистых функций, а точнее, их выполнений от вычислений. (Определение из википедии).

«Шаблон монады» это еще один шаблон для типов. Как, например, одиночка (синглтон).

Часть вторая

  • Nullable<T> — представляет объект типа T, который может быть null (в дальнейшем, подразумевается что Nullable<T> может работать с любым типом данных).
  • Func<T> — представляет объект типа T, который будет вычислен отложено (в дальнейшем, для большей ясности, будет использоваться делегат delegate T OnDemand<T>();).
  • Lazy<T> — представляет объект типа T, который будет вычислен отложено в первый раз, а после, закеширован.
  • Task<T> — представляет объект типа T, который будет вычислен асинхронно и будет доступен в будущем, если уже не вычислен.
  • IEnumerable<T> — представляет упорядоченную, доступную только для чтения последовательность от нуля и более элементов типа T.

Часть третья

Первое требование для монад: «если M<T> это тип-монада, тогда должен быть простой путь по превращению любого значение типа T в значение типа M<T>». Например:

static Nullable<T> CreateSimpleNullable<T>(T item) { return new Nullable<T>(item); }
static OnDemand<T> CreateSimpleOnDemand<T>(T item) { return () => item; }
static IEnumerable<T> CreateSimpleSequence<T>(T item) { yield return item; }

Кажется, что второе требование просто сформулировать: «из монады M<T>можно получить значение типа T». Но не все так однозначно. Начнем с очень специфичного вопроса. Можно легко прибавить единицу к целочисленному типу, но как «прибавить единицу» к типу-монаде обернутого вокруг целочисленного типа?

Для Nullable<T>:

static Nullable<int> AddOne(Nullable<int> nullable)
{
  if (nullable.HasValue)
  {
    int unwrapped = nullable.Value;
    int result = unwrapped + 1;
    return CreateSimpleNullable(result);
  }
  else  
    return new Nullable<int>();
}

То есть можно развернуть, произвести операцию и завернуть? Не совсем, если проделать ту же операцию для OnDemand<T>(), который обернут вокруг DateTime.Now.Seconds, то получится статическое значение. Поэтому проделанную операцию вместе с разворачиванием необходимо завернуть в функцию, как показано здесь:

static OnDemand<int> AddOne(OnDemand<int> onDemand)
{
  return () =>
  {
    int unwrapped = onDemand();
    int result = unwrapped + 1;
    return result;
  };
}

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

Для Lazy<T>:

static Lazy<int> AddOne(Lazy<int> lazy)
{
  return new Lazy<int>(() =>
  {
    int unwrapped = lazy.Value;
    int result = unwrapped + 1;
    return result;
  });
}

Для Task<T>:

async static Task<int> AddOne(Task<int> task)
{
  int unwrapped = await task;
  int result = unwrapped + 1;
  return result;
}

И, наконец, для IEnumerable<T>:

static IEnumerable<int> AddOne(IEnumerable<int> sequence)
{
  foreach(int unwrapped in sequence)
  {
    int result = unwrapped + 1;
    yield return result;
  }
}

Таким образом, одно из полезных правил для шаблона монад — добавление единицы к завернутому целочисленному типу производит другой завернутый целочисленный тип, с сохранением всех особенностей.

Часть четвертая

Напишем метод, который позволит делать оболочку над любыми Nullable<T> функциями, а не только операцией по добавлению единицы:

static Nullable<T> ApplyFunction<T>(
  Nullable<T> nullable,
  Func<T, T> function)
{
  if (nullable.HasValue)
  {
    T unwrapped = nullable.Value;
    T result = function(unwrapped);
    return new Nullable<T>(result);
  }
  else
    return new Nullable<T>();
}

Теперь метод AddOne(...) будет выглядеть так:

static Nullable<int> AddOne(Nullable<int> nullable)
{
  return ApplyFunction(nullable, (int x) => x + 1);
}

Но, допустим, мы хотим функцию которая принимает тип int и возвращает double. Например, поделить 2 целых числа и получить результат типа double. Для этого, перепишем метод ApplyFunction в следующий вид:

static Nullable<R> ApplyFunction<A, R>(
  Nullable<A> nullable,
  Func<A, R> function)
{
  if (nullable.HasValue)
  {
    A unwrapped = nullable.Value;
    R result = function(unwrapped);
    return new Nullable<R>(result);
  }
  else
    return new Nullable<R>();
}

Для остальных типов, можно сделать по аналогии. По сути, получился способ превращения типов из A в R в монадические типы из М<А> в М<R> такие, что сохраняется действие функции и значения, предоставляемые в монадическом («расширенном») типе.

Часть пятая

Ранее было указано, что можно взять любую функцию с одним параметром и любым не пустым возвращаемым типом и применить эту функцию к монаде с возвращаемым типом M<R>. Любой возвращаемый тип, так? Предположим, что есть функция с одним параметром:

static Nullable<double> SafeLog(int x)
{
  if (x > 0)
    return new Nullable<double>(Math.Log(x));
  else
    return new Nullable<double>();
}

Обычная функция с одним параметром. Значит, ее можно применить к Nullable<int> и получить обратно… Nullable<Nullable<double>>! Это неправильно.

Создадим новую версию ApplyFunction, которая избегает описанной проблемы:

static Nullable<R> ApplySpecialFunction<A, R>(
  Nullable<A> nullable,
  Func<A, Nullable<R>> function)
{
  if (nullable.HasValue)
  {
    A unwrapped = nullable.Value;
    Nullable<R> result = function(unwrapped);
    return result;
  }
  else
    return new Nullable<R>();
}

Просто, не так ли? Создадим функции для остальных операторов:

static OnDemand<R> ApplySpecialFunction<A, R>(
  OnDemand<A> onDemand,
  Func<A, OnDemand<R>> function)
{
  return () =>
  {
    A unwrapped = onDemand();
    OnDemand<R> result = function(unwrapped);
    return result();
  };
}

static Lazy<R> ApplySpecialFunction<A, R>(
  Lazy<A> lazy,
  Func<A, Lazy<R>> function)
{
  return new Lazy(() =>
  {
    A unwrapped = lazy.Value;
    Lazy<R> result = function(unwrapped);
    return result.Value;
  };
}

static async Task<R> ApplySpecialFunction<A, R>(
  Task<A> task,
  Func<A, Task<R>> function)
{
  A unwrapped = await task;
  Task<R> result = function(unwrapped);
  return await result;
}

static IEnumerable<R> ApplySpecialFunction<A, R>(
  IEnumerable<A> sequence,
  Func<A, IEnumerable<R>> function)
{
  foreach(A unwrapped in sequence)
  {
    IEnumerable<R> result = function(unwrapped);
    foreach(R r in result)
      yield return r;
  }
}

В итоге для «шаблона монады» имеются 3 правила:

  1. Всегда существует возможность преобразовать тип T в тип M<T>.

    static M<T> CreateSimpleM<T>(T value)
    
  2. Если существует функция, преобразующая A в R, тогда можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>.

    static M<R> ApplyFunction<A, R>(
    M<A> wrapped,
    Func<A, R> function)
    
  3. Если существует функция, преобразующая A в M<R>, тогда можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>.

    static M<R> ApplySpecialFunction<A, R>(
    M<A> wrapped,
    Func<A, M<R>> function)
    

Но, правило 2 является частным случаем правила 3. Его можно представить в как комбинацию 1 и 3 правила:

static M<R> ApplyFunction<A, R>(
  M<A> wrapped,
  Func<A, R> function)
{
  return ApplySpecialFunction<A, R>(
    wrapped,
    (A unwrapped) => CreateSimpleM<R>(function(unwrapped)));
}

Остается всего два правила. Они являются полными правилами «шаблона монады»? В принципе, да.

Часть шестая

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

Пусть имеются 2 метода:

static M<T> CreateSimpleM<T>(T t) { ... }
static M<R> ApplySpecialFunction<A, R>(
  M<A> monad, Func<A, M<R>> function) {...}

Тогда, результат следующего выражения:

ApplySpecialFunction(someMonadValue, CreateSimpleM)

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

ApplySpecialFunction(CreateSimpleM(someValue), someFunction)

по значению идентичен:

someFunction(someValue)

Часть седьмая

Допустим, имеются 2 функции:

Func<int, Nullable<double>> log = x => x > 0
    ? new Nullable<double>(Math.Log(x))
    : new Nullable<double>();
Func<double, Nullable<decimal>> toDecimal = y => Math.Abs(y) < decimal.MaxValue
    ? new Nullable<decimal>((decimal)y)
    : new Nullable<decimal>();

Тогда, с помощью определенного ранее метода ApplySpecialFunction можно написать следующий метод-помощник:

static Func<X, Nullable<Z>> ComposeSpecial<X, Y, Z>(
  Func<X, Nullable<Y>> f,
  Func<Y, Nullable<Z>> g)
{
  return x => ApplySpecialFunction(f(x), g);
}

который позволяет объединить определенные выше функции в одну:

Func<int, Nullable<decimal>> both = ComposeSpecial(log, toDecimal);

Отсюда следует последнее правило — метод ApplySpecialFunction должен гарантировать работу композиции. Пример:

Func<X, M<Y>> f = whatever;
Func<Y, M<Z>> g = whatever;
M<X> mx = whatever;
M<Y> my = ApplySpecialFunction(mx, f);
M<Z> mz1 = ApplySpecialFunction(my, g);
Func<X, M<Z>> h = ComposeSpecial(f, g);
M<Z> mz2 = ApplySpecialFunction(mx, h);

Значения mz1 и mz2 должны быть одинаковыми.

Наконец, можно полностью описать «шаблон монады» в C#:

Монада — это обобщенный тип M<T>, такой что:

  • Для нее существует конструирующий механизм, который принимает на вход переменную типа T и возвращает M<T>:

    static M<T> CreateSimpleM<T>(T t)
    
  • Если существует способ преобразования значения типа A в M<R>, то можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>:

    static M<R> ApplySpecialFunction<A, R>(
    M<A> monad, Func<A, M<R>> function)
    

Оба этих метода должны подчиняться следующим законам:

  • Применение функции создающую простую монаду (правило-метод 1) к конкретному экземпляру монады должно приводить к логически идентичному экземпляру монады.
  • Применение функции к результату функции создающей простую монаду из определенного значения и применение этой функции к определенному значению напрямую должно приводить к логически идентичным экземплярам монад.
  • Результат применения к значению первой функции второй функции и результат применения первоначального значения к функции-композиции первых двух функций должен приводить к двум логически идентичным экземплярам монад.

Часть восьмая

Традиционное имя для функции CreateSimpleunit. В Haskell — return.

Традиционное имя для функции ApplySpecialFunctionbind. В Haskell она является встроенной функцией, для того чтобы применить функцию f на экземпляр монады m необходимо написать m >>= f.

Фактически функция привязки берет неизменный рабочий процесс и операцию над ним и возвращает новый рабочий процесс.

Мой конспект на этом оканчивается. В последующих частях серии рассматривается практическое применение монад в коде.

  • Часть 9. О простых монадах «присоединяющих дополнительные данные к значению».
  • Часть 10. О запросах и LINQ на примере SelectMany.
  • Часть 11. Дополнения к предыдущей главе. Аддитивная монада.
  • Часть 12. Продолжение про запросы и SelectMany.
  • Часть 13. О Task монадах.
27 января 2016   .net   functional  

Желание изучить питон было давно. Я много где слышал что он хорошо подходит для прототипов, скриптов и небольших приложений. Он отличается высокой скоростью разработки и низким порогом вхождения. Динамичность — непривычно, но решаемо.

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

Установка Python 3

Не нашел причин, чтобы начать знакомство не с 3 версией питона. И, так как в Mac OS X El Capitan, установлена версия 2.7, то рассмотрим простой способ установки Python 3.

Понадобится менеджер пакетов Homebrew. У кого нет — советую. Для его установки введем в терминале:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Теперь установим третий питон:

brew install python3

Проверить работоспособность можно набрав в консоли python3.

Получение пакета для работы с тудуистом

Следующий этап — установка библиотеки для работы с АПИ тудуиста. Воспользуемся менеджером пакетов pip3, который поставляется вместе с Python 3:

pip3 install todoist-python

Напоминаю о командной оболочке Zsh, о которой я писал ранее. Подключив плагины brew и pip можно добавить автодополнение для команд и пакетов.

Среда разработки

Автодополнение, рефакторинг, отладка — все это помогает на любых стадиях изучения языка. Поэтому я воспользовался IDE от JetBrains — PyCharm CE. Это бесплатная версия, в которой есть все необходимое.

Стиль кода и именования

Планирутся отдельная статья. А пока — о стиле именования.

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

def foo_bar(new_value):

Стиль mixedCase допускается в тех местах, где уже преобладает такой стиль, для сохранения обратной совместимости:

def fooBar(newValue):

Для атрибутов и непубличных методов используется один символ подчёркивания перед именем:

_private_value = 3

Итерация и получение значений из коллекций

Библиотека todoist возвращает на запрос о просроченных задачах словарь или список. Возник вопрос: как работать с коллекциями?

Итерация оказалась похожа на foreach из C#:

for overdue_item in overdue_items:
    item = api.items.get_by_id(overdue_item['id'])
    print item

Для получения значения словаря есть 2 способа . Первый — обращение по ключу:

id = overdue_item['id']

Но, если значения нет, возникнет ошибка — KeyError: 'id'. Поэтому при неуверенности, используйте get:

id = overdue_item.get('id')

Если значение отсутствует, метод вернет значение по умолчанию — None.

Преобразование и работа с датой

Рассмотрим как парсить дату, находить дельту, добавлять значения и приводить к определенному строковому формату.

Разберем построчно код из моего скрипта:

item_due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S %z')
delta = datetime.now(timezone.utc).date() - item_due_date.date()
item_today_date = item_due_date + timedelta(days=delta.days)
item.update(due_date_utc=item_today_date.strftime('%Y-%m-%dT%H:%M:%S'))
  1. По ключу due_date_utc получаем дату в формате "Fri 26 Sep 2014 08:25:05 +0000". Выражением %a %d %b %Y %H:%M:%S %z переводим в понятный питону формат. Документация по значениям переменных из выражения.
  2. Находим разницу между текущей датой и датой полученного объекта.
  3. Актуализируем дату объекта, добавляя к нему полученную разницу дней.
  4. Переведем дату в строку используя %Y-%m-%dT%H:%M:%S и отправим изменения в тудуист.

Типы выполнения модуля с кодом

Часто, читая код на гитхабе, встречал конструкцию:

if __name__ == "__main__":
    ...

Выясним ее предназначение.

Когда исполняется файл с кодом, выполняются все команды на нулевом уровне: задаются специальные переменные, импортируются модули, определяются функции и классы. Одна из специальных переменных — __name__. Она хранит имя модуля, который вызвал скрипт.

Например, модуль вызвали из файла foo.py:

import TodoistOverdue

Переменная __name__ примет значение foo. Если вызвать скрипт напрямую из терминала:

python TodoistOverdue.py

__name__ инициализируется значением __main__. И тогда выполнится весть код из условия if __name__ == "__main__":. Получается, модуль может работать и библиотекой и независимым приложением.

Добавление атрибутов командной строки

Мой скрипт может принимать значение токена АПИ тудуиста. Для этого скрипт вызывается с параметром -t:

python TodoistOverdue.py -t 0123456789abcdef0123456789abcdef01234567

Рассмотрим построчно, как задавать описание скрипта и определять аргументы:

parser = argparse.ArgumentParser(description="Moving overdue tasks for today in todoist")
parser.add_argument("-t", "--token", help="Todoist API token")
args = parser.parse_args()
  1. Инициализируем класс ArgumentParser, помогающий работать с командной строкой, и зададим описание скрипта.
  2. Добавим аргумент, задаваемый ключом ’-t’ или ’--token’. В параметре help указываем описание, показываемое при вызове скрипта с ключом -h.
  3. Переводим строки аргументов в объекты и присваиваем их как атрибуты к переменной.

Теперь, при запуске скрипта с ключом -t, в переменной args.token будет храниться значение введенного токена. Про остальные параметры и методы можно узнать в документации python.

Чтение и запись в файл конфигурации

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

Чтение и запись в файл конфигурации

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

config = configparser.ConfigParser()
config.read(expanduser('~') + "/.todoist")
token = args.token
if token is None:
    token = config['Global']['TokenAPI']
else:
    config['Global'] = {'TokenAPI': token}
    with open(expanduser('~') + "/.todoist", 'w') as configfile:
        config.write(configfile)
  1. Инициализируем ConfigParser и читаем файл из домашней директории пользователя. expanduser('~') позволяет получить путь к домашней директории в любой операционной системе.
  2. Читаем конфиг как словарь: config['Global']['TokenAPI']. Сначала обращаемся к секции со значением, следом — к ключу.
  3. В блоке else записываем значение токена, если оно было указано при запуске. После конфигурационный файл записывается на диск.

Мне понравилось, как просто записать изменения в файл. В C# для этого надо открывать потоки, указывать дополнительную информацию, здесь это просто 2 строчки. Возьми это и запиши сюда, все.

Документация для ConfigParser. Информацию о вводе и выводе в Python 3.

Заключение

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

Полный скрипт TodoistOverdue.py из статьи лежит в gist.

Полезные ссылки

— Как запускать скрипты в Mac Os X без указания полного пути. Stackoverflow. — Изучить питон за несколько минут (learnxinyminutes.com). Русская и английская версия. — Документация для Python 3.5.х.

20 января 2016   python  

Конфигурация

git config --global user.name "[name]" — установить имя, которое будет прикрепляться к коммиту.

git config --global user.email "[email address]" — установить email, который будет прикрепляться к коммиту.

git config --global color.ui auto — включить полезную подсветку командной строки.

git config --global push.default current — обновлять удаленную ветку с таким же именем, что и локальная, при пуше изменений (если не указано иного).

git config --global core.editor [editor] — установить редактор для редактирования сообщений коммита.

git config --global diff.tool [tool] — установить программу для разрешения конфликтов при слиянии.

Создание репозиториев

git init [project-name] — создать новый локальный репозиторий с заданным именем.

git clone [url] — загрузить проект и его полную историю изменений.

Работа с изменениями

git status — полный список изменений файлов, ожидающих коммита.

git status -s — краткий вид изменений.

git diff — показать изменения в файлах, которые еще не были добавлены в индекс коммита (staged).

git add [file] — сделать указанный файл готовым для коммита.

git add . — сделать все измененные файлы готовыми для коммита.

git add '*.txt' — добавить только файлы, соответствующие указанному выражению.

git add --patch filename — позволяет выбрать какие изменения из файла добавятся в коммит.

git diff --staged — показать что было добавленно в индекс с помощью git add, но еще не было закоммиченно.

git diff HEAD — показать что изменилось с последнего коммита.

git diff HEAD^ — показать что изменилось с предпоследнего коммита.

git diff [branch] — сравнить текущую ветку с заданной.

git difftool -d — то же самое, что и diff, но показывает изменения в заданной difftool.

git difftool -d master.. — показать изменения, сделанные в текущей ветке.

git diff --stat — показать статистику какие файлы были изменены и как.

git reset [file] — убрать файлы из индекса коммита (изменения не теряются).

git commit — записать изменения в репозиторий. для написания сообщения откроется назначенный редактор.

git commit -m "[descriptive message]" — записать изменения с заданным сообщением.

git commit --amend — добавить изменения к последнему коммиту.

Работа с ветками

git branch — список всех локальных веток в текущей директории.

git branch [branch-name] — создать новую ветку.

git checkout [branch-name] — переключиться на указанную ветку и обновить рабочую директорию.

git checkout -b <name> <remote>/<branch> — переключиться на удаленную ветку.

git checkout [filename] — вернуть файл в первоначальное состояние если он еще не был добавлен в индекс коммита.

git merge [branch] — соединить изменения в текущей ветке с изменениями из заданной.

git merge --no-ff [branch] — соединить ветки без режима “fast forwarding”.

git branch -a — посмотреть полный список локальных и удаленных веток.

git branch -d [branch] — удалить заданную ветку.

git branch -D [branch] — принудительно удалить заданную ветку, игнорируя ошибки.

git branch -m <oldname> <newname> — переименовать ветку.

Работа с файлами

git rm [file] — удалить файл из рабочей директории и добавить в индекс информацию об удалении.

git rm --cached [file] — удалить файл из репозитория, но сохранить его локально.

git mv [file-original] [file-renamed] — изменить имя файла и добавить в индекс коммита.

Отслеживание файлов

.gitignore — текстовый файл, в котором задаются правила для исключения файлов из репозитория. Например:

  • *.log
  • build/
  • temp-*

git ls-files --other --ignored --exclude-standard — список всех игнорируемых файлов.

Сохранение фрагментов

git stash — положить во временное хранилище все отслеживаемые файлы.

git stash pop — восстановить последние файлы, положенные во временное хранилище.

git stash list — список всех сохраненных изменений во временном хранилище.

git stash drop — удалить последние файлы, положенные во временное хранилище.

Просмотр истории

git log — список изменения текущей ветки.

git log --follow [file] — список изменения текущего файла, включая переименования.

git log --pretty=format:"%h %s" --graph — изменение вида отображения истории изменений.

git log --author='Name' --after={1.week.ago} --pretty=oneline --abbrev-commit — посмотреть над чем работал заданный пользователь последнюю неделю.

git log --no-merges master.. — посмотреть историю изменений только для текущей ветки.

git diff [file-branch]..[second-branch] — посмотреть различия между двумя заданными ветками.

git show [commit] — показать метадату и изменения в заданном коммите.

git show [branch]:[file] — посмотреть на файл в другой ветке, не переключаясь на неё.

Отмена коммитов

git reset — убрать изменения из индекса коммита, сами изменения останутся.

git reset [commit/tag] — отменить все коммиты после указанного коммита, изменения будут сохранены локально.

git reset --hard [commit] — принудительно вернутся к указанному коммиту, не сохраняя историю и изменения.

Синхронизация изменений

git fetch [bookmark] — загрузить всю историю с заданного удаленного репозитория.

git merge [bookmark]/[branch] — слить изменения локальной ветки и заданной удаленной.

git push — запушить текущую ветку в удаленную ветку.

git push [remote] [branch] — запушить ветку в указанный репозиторий и удаленную ветку.

git push [bookmark] :[branch] — в удаленном репозитории удалить заданную ветку.

git push -u origin master — если удаленная ветка не установлена как отслеживаемая, то сделать ее такой.

git pull — загрузить историю и изменения удаленной ветки и произвести слияние с текущей веткой.

git pull [remote][branch] — указать конкретную удаленную ветку для слияния.

git remote — посмотреть список доступных удаленных репозиториев.

git remote -v — посмотреть детальный список доступных удаленных репозиториев.

git remote add [remote][url] — добавить новый удаленный репозиторий.

Полезные ссылки

6 января 2016   git