PDA

Просмотр полной версии : [Статья] Monads [1] = Do


HideFebruary
06.03.2016, 13:27
Прошлая статья: Monads [0] ([Ссылки могут видеть только зарегистрированные и активированные пользователи])

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

Сама по себе, эта тема мало интереса представляет для читерства как такового, но для повышения уровня кода и знаний это будет полезно. Как можно повысить уровень кода? Один из принципов ООП гласит, что нужно исключать дублирование кода, я выбрал именно этот принцип, потому что это самая популярная проблема языка на мой взгляд. Вторая проблема которую я выделю - лаконичность кода, он должен быть легко читаемым и понятным по смыслу. Сложно представить что C# не лаконичен по своей сути, его оказуалили так, что на нем легко сможет писать и ребенок, но все же есть некоторые места которые меня очень раздражают.

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

void Foo( object argument )
{
//todo
}

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

If ( argument == null ) return;

Обычно, если аргумент неинициализирован, то вызывается ошибка, но мы помним что код находится в важной системе и максимум можем написать в лог о том, что аргумент невалидный, либо как-нибудь по другому оповестить об этом. Только в 2015 году, в версии 6 языка C# предложили оператор ".?" который сокращает дублирование данных проверок. При чем в нашем случаее этот оператор никак не поможет, а таких проверок куча. И только в следующей версии языка разработчики планируют ввести обязательные аргументы которые никогда не равны null, но это уже другая история.

Теперь про лаконичность. Начну с конструкции try.

void Foo( object argument )
{
try
{
Console.WriteLine( argument.GetHashCode () );
}
catch
{

}
}

Для меня этого кода слишком много, особенно раздражает пустой, но обязательный блок catch, я бы хотел что-то вроде этого:

void Foo( object argument )
{
Try( Console.WriteLine( argument.GetHashCode() ));
}

На самом деле, все это меня не натолкнуло на поиск какого-либо решения. Эти вещи незначительны и с ними можно прижиться. Как-то раз я увидел в интернетах про монаду Maybe на языке C#, конкретная реализация меня не впечатлила, но вот возможность языка комбинировать методы расширения с дженериками рождая при этом что-то вроде монад, очень даже. Я начал с этим экспериментировать, максимально обобщая монады, заменяя некоторые конструкции языка и получилось кое-что, о чем я и буду писать в этом цикле статей.

Итак, что же такое монады, описание из вики:


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


Мы возьмем за основу это понятие и будем развивать в C#. Наши монады будут возвращать предсказуемый результат, по крайней мере мы снабдим их этой возможностью. Монады будут завязаны на методах расширения, принимать в себя один аргумент в виде дженерика и еще один аргумент в виде действия над ним. Действием будет являться один из трех стандартных делегатов: Action<T> ([Ссылки могут видеть только зарегистрированные и активированные пользователи]), Func<T, TResult> ([Ссылки могут видеть только зарегистрированные и активированные пользователи]), Predicate<T> ([Ссылки могут видеть только зарегистрированные и активированные пользователи]).

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

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

public static T Do<T>( this T argument, Action<T> doAction )
{
If ( argument != null ) doAction?.Invoke ( argument );
return argument;
}

Пример её использования:

"Hello Do".Do ( WriteLine );

Продление цепочки:

"Hello Do"
.Do ( WriteLine )
.Do ( WriteLine )
.Do ( str => WriteLine ( $"Приветственное сообщение: {str}" ) );

Так как действие вызывается через Action, мы не можем попросить монаду посчитать нам что-то или изменить исходный аргумент. Что бы это можно было сделать, мы добавим в качестве действия функтор.

public static TResult Do<T, TResult>( this T argument, Func<T,TResult> doFunc,
TResult defaultResult = default(TResult) )
{
return argument == null && doFunc == null ? defaultResult : doFunc ( argument );
}

Обратите внимание на реализацию с использованием default значения, это нужно для структур, если с классами этого можно избежать и просто вернуть null, а во внешнем коде использовав оператор "??" присвоить какое-то значение, то со структурами так просто не выйдет и мы даем возможность определить значение выходного результата заранее, если выполнить выражение не получается. Теперь мы можем легко изменять нашу переменную по монадической цепочке:

"Hello".Do ( WriteLine )
.Do ( s => s += " Do" ).Do ( WriteLine )
.Do ( s => s.Replace ( "Do", "World" ) ).Do ( WriteLine );

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

Исходники монады под спойлером:



using System;

namespace Monads
{
public static class DoMonad
{
public static T Do<T>( this T argument, Action<T> doAction )
{
if ( argument != null ) doAction?.Invoke ( argument );
return argument;
}

public static TResult Do<T, TResult>( this T argument, Func<T,TResult> doFunc,
TResult defaultResult = default(TResult) )
{
return argument == null && doFunc == null ? defaultResult : doFunc ( argument );
}
}
}