Pull to refresh

Comments 59

В мире С++ люди еще помимо fluent interface’ов делают так: дают некоторым результатам цепочных вызовов классы-обертки, т.е. например вызов Maybe() возвращает не T или nullptr, а обертку Maybe<T>. А для некоторых таких оберток генерят перегрузки операторов вроде || чтобы композиция не выглядела так адово, как огромная цепочка вызовов.
В C# тоже можно так делать. А для Maybe еще и реализовать метод SelectMany и писать совсем странные конструкции вроде:
   var maybe=
                from x in 1.Maybe()
                from y in SomeOtherMaybe
                select x + y

Правда, практического применения этой конструкции мне найти не удалось :)
В C++ вообще можно изменить поведение так, что вообще непонятно что творит код. В своей жизни пришлось клепать аналог Expression Tree, ORM, с лютым синтаксисом и т.д… А уж если посмотреть на что-нить типа boost::spirit, boost::phoenix, то становится дурно.

Увы, C# в большинстве случаев не даст такое сделать, ибо generic != template, да и даже отсутствие typdef уже делает невозможным подобные вещи.

С другой стороны, скорость компиляции и требования к памяти оставляют желать лучшего в случае таких лютых извращений на C++.
Да, шаблоны в плюсах — штука эпичная. Особенно ошибки компилятора в них:)
Увы, C# в большинстве случаев не даст такое сделать, ибо generic != template, да и даже отсутствие typdef уже делает невозможным подобные вещи.

Думаю, что не «увы», а «хорошо что» :). У C# же достаточно неплохо с интероперабельностью с плюсами. Захотелось чего-нибудь эдакого — знай подключай плюсовые dll. А для типовых задач, решаемых на C# такая мощь — избыточна.
Особенно ошибки компилятора в них:)

Со временем можно научиться в этом разбираться. И если получится разбираться в каше логов GCC, то с остальными компиляторами проблем не возникает.

Думаю, что не «увы», а «хорошо что» :).

Ну да, примерно это и имелось ввиду :) Пока народ не проникся Roslyn, C# будет держаться.

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

public IActionResult Get(int id) =>  query
    .Where(x => x.Id == id)
    .SingleOrDefault()
    .PipeTo(x => x != null ? Ok(x) : new NotFoundResult(“Not Found”));


Выглядит уже не так хорошо. Исправим это с помощью метода Either:
public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, bool> condition,
    Func<TInput, TOutput> ifTrue, Func<TInput, TOutput> ifFalse)
    => condition(o) ? ifTrue(o) : ifFalse(o);

public IActionResult Get(int id) =>  query
    .Where(x => x.Id == id)
    .SingleOrDefault()
    .Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult("Not Found"));


Стало ничем не лучше, даже наоборот, переусложнён код, который ещё может быть кому-то предстоит отлаживать…
Начали за здравие, закончили за упокой.
PipeTo, Do ещё приемлемы, а все остальное выглядит как функциональщина ради функциональщины:
Either — заменили? и: на, и,
ById — теперь надо реализовать ещё один интерфейс.
ById с проекцией — если вставить перед ним Select будет понятнее.
ToPagedEnumerable — не ленивая
IQueryableSpecification — В каком порядке отсортирует? сначала по Id потом по Name или наоборот?
В случае с лямбдой все понятно, а компилятор эту «спецификацию» создаст за нас.
MaybeWhere — зачем проверять на Expression, если мы передаем всегда IPaging
Проще реализовать два метода расширения и переложить работу на компилятор.

ПС: Не написали про методы MinBy, MaxBy.
Также полезным был бы интерфейс:
interface ICountedEnumerable<out T> : IEnumerable<T>
{
    public int Count {get;}
}

Как промежуточный между ICollection и IEnumerable, расширения которого избавляют от лишней работы в методах ToArray, ToList.
ППС: И напоследок пару расширений:
IEnumerable<Tuple<T, T>> Pack<T>(this IEnumerable<T> source);
IEnumerable<Tuple<T, T>> Window<T>(this IEnumerable<T> source);

ICountedEnumerable уже есть, только называется IReadOnlyCollection

Either — заменили? и: на, и,
Я использую только вторую перегрузку (та, что проверяет на null). Получается так:
str.Either(x => x + " Some more text", _ => "No Text");. Возможно, первую, та что с condition можно и вправду удалить за ненадобностью. Подумаю об этом.

IQueryableSpecification — В каком порядке отсортирует? сначала по Id потом по Name или наоборот?
В случае с лямбдой все понятно, а компилятор эту «спецификацию» создаст за нас.
Подробнее эта часть описана в другой статье. Эта ссылка есть и в тексте этого топика. Скорее всего вы не перешли, поэтому возник вопрос.

MaybeWhere — зачем проверять на Expression, если мы передаем всегда IPaging
Проще реализовать два метода расширения и переложить работу на компилятор.
Посмотрите на метод Paged. MaybeWhere нужен только для компоновки. Возможно его стоит сделать приватным.
> Я использую только вторую перегрузку (та, что проверяет на null).

Конечно, если использовать как

> .SingleOrDefault().Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult(«Not Found»));

Есть какой-то сакральный смысл в том, что бы сначала самому себе подсовывать Default, а потом вот так изгаляться?

> .SingleOrElse(new NotFoundResult(«Not Found»));
Всё это здорово, пока вы единственный разработчик и помните, что вы спрятали за этими 'PipeTo' и пр. Но отлаживать чужой подобный код то ещё удовольствие. В каждый такой extension method всё равно приходится залезать чтобы понять, делает ли он то, о чём вы подумали или, вообще, что именно он делает. А попробовав F# можно понять, что «крен функциональщины» в C# так себе.
Для ловли ошибок есть юнит тесты. А данный подход на оборот приятен, удобен и сокращает код.
Наличие юниттестов не исключает отладку, а даже наоборот. Насчет удобства, кому как. Я поделился своим опытом.
Ну ок, тест упал, в тестируемом коде ошибка. Это не избавляет от необходимости проанализировать код, а следовательно разобраться во всей этой кастомщине.
Можно позанудствовать?
Спасибо.
ById требует соблюдения некоторых соглашений. Что-то облегчили, в чем-то добавили сложности
Ваш Either не совсем та структура (монада, если угодно) Either, которая обычно используется в ФП…
Ну, и названия типа MaybeWhere глаз немного режут.
А вообще радует Ваш подход
MaybeWhere  мне потребовался, чтобы написать
query
    .MaybeWhere(/* может быть ты хочешь применить  where до проекции*/)
    .Select (...)
    .MaybeWhere(/* или после?*/)
    .ToArray()

Тогда можно лениться и делать так:
public class Spec: IQueryableSpec<TEntity>, IQueryableSpec<TProjection>

В таком случае будут применены оба Where. Обычно два не требуется — достаточно одного. Специально для этого в самом конце есть  Paged с явным указанием параметров. Мне в простых Crud проще вешать много интерфейсов на один класс. Главное понимать Flow: Entity => Dto. Соовтетственно фильтрации и сортировки применяются в таком-же порядке.

Either здесь действительно не имеет отношения к слове на букву М:) На данный момент я думаю, что в C# проще кидать исключения и писать только один try/catch на все приложение. Получится вполне себе Either<TResult, Exception>. Пытаться конвертнуть все Exception'ы в Failure в .NET Framework — задача так себе.

По поводу читабельности мы специально проводили эксперимент и давали нескольким программистам посмотреть код с Either и PipeTo. Все правильно ответили как работает код, поэтому оставили название методов такими. Можете предложить название лучше?

Either и PipeTo

Можете предложить название лучше?

Ну, Either тут больше похож на ContinueWith, не? На мой взгляд. Не претендую. Просто CPS, как он есть…

По поводу
Either<TResult, Exception>
— дело вкуса, конечно. Я, например, в одном из проектов использую Either<TError, TResult>. И так это меня радует, неимоверно. Почти как у взрослых :), вынуждает разработчика обрабатывать оба варианта, иначе не скомпилируется.
ContinueWith — вариант, но тогда придется пилить нечто вроде
flow.ContinueWith(x => ...).Error(e => ...). А это уже надо опять промежуточный объект создавать и получится тот-же самый Either<TResult, Exception>.

А не поделитесь вашей реализацией Either? Я написал одну, показалось не элегантно и отказался. Может есть реализации лучше моей?
Не то, чтобы моя реализация с нуля:
Как-то так
public class Either<TL, TR>
    {
        [DataMember]
        private readonly bool _isLeft;

        [DataMember]
        private readonly TL _left;

        [DataMember]
        private readonly TR _right;

        public Either(TL left)
        {
            _left = left;
            _isLeft = true;
        }

        public Either(TR right)
        {
            _right = right;
            _isLeft = false;
        }

        /// <summary>
        /// Checks the type of the value held and invokes the matching handler function.
        /// </summary>
        /// <typeparam name="T">The return type of the handler functions.</typeparam>
        /// <param name="ofLeft">Handler for the Left type.</param>
        /// <param name="ofRight">Handler for the Right type.</param>
        /// <returns>The value returned by the invoked handler function.</returns>
        /// <exception cref="System.ArgumentNullException">
        /// </exception>
        public T Match<T>(Func<TL, T> ofLeft, Func<TR, T> ofRight)
        {
            if (ofLeft == null)
            {
                throw new ArgumentNullException(nameof(ofLeft));
            }

            if (ofRight == null)
            {
                throw new ArgumentNullException(nameof(ofRight));
            }

            return _isLeft ? ofLeft(_left) : ofRight(_right);
        }

        /// <summary>
        /// Checks the type of the value held and invokes the matching handler function.
        /// </summary>
        /// <param name="ofLeft">Handler for the Left type.</param>
        /// <param name="ofRight">Handler for the Right type.</param>
        /// <exception cref="System.ArgumentNullException">
        /// </exception>
        public void Match(Action<TL> ofLeft, Action<TR> ofRight)
        {
            if (ofLeft == null)
            {
                throw new ArgumentNullException(nameof(ofLeft));
            }

            if (ofRight == null)
            {
                throw new ArgumentNullException(nameof(ofRight));
            }

            if (_isLeft)
            {
                ofLeft(_left);
            }
            else
            {
                ofRight(_right);
            }
        }

        public TL LeftOrDefault() => Match(l => l, r => default(TL));
        public TR RightOrDefault() => Match(l => default(TR), r => r);
        public Either<TR, TL> Swap() => Match((Func<TL, Either<TR, TL>>) (Right<TR, TL>), Left<TR, TL>);

        public Either<TL, T> Bind<T>(Func<TR, T> f)
            => BindMany(x => Right<TL, T>(f(x)));

        public Either<TL, T> BindMany<T>(Func<TR, Either<TL, T>> f) => Match(Left<TL, T>, f);

        public Either<TL, TResult> BindMany<T, TResult>(Func<TR, Either<TL, T>> f, Func<TR, T, TResult> selector)
            => BindMany(x => f(x).Bind(t => selector(_right, t)));

        public static implicit operator Either<TL, TR>(TL left) => new Either<TL, TR>(left);
        public static implicit operator Either<TL, TR>(TR right) => new Either<TL, TR>(right);

        public static Either<TLeft, TRight> Left<TLeft, TRight>(TLeft left)
            => new Either<TLeft, TRight>(left);

        public static Either<TLeft, TRight> Right<TLeft, TRight>(TRight right)
            => new Either<TLeft, TRight>(right);

        public static Either<Exception, T> Try<T>(Func<T> f)
        {
            try
            {
                return new Either<Exception, T>(f.Invoke());
            }
            catch (Exception ex)
            {
                return new Either<Exception, T>(ex);
            }
        }
    }


А прикладной код как выглядит?
Примерно так:
public interface ISomeService
{
    Task<Either<TradeError, Quote>> GetQuoteAsync(GetQuoteQuery query);
}
...
var result = await _service.GetQuoteAsync(query);
result.Match(SetQuoteError, SetQuote);
...

private void SetQuoteError(TradeError error)
{
    // do something
}

private void SetQuote(Quote quote)
{
    // do something
}





Или так
public interface ISomeService
{
    Task<Either<TradeError, Quote>> GetQuoteAsync(GetQuoteQuery query);
}

...
public async Task<Either<TradeError, QuoteModel>> GetQuoteModelAsync(GetQuoteQuery query)
        {
            var result = await _service.GetQuoteAsync(query.);
            return result.Bind(ToQuoteModel);
        }
...

private static QuoteModel ToQuoteModel(Quote source) => new QuoteModel{ ... };




Должно быть понятно, вроде… :)
А чем это лучше использования task.IsFaulted?
<оффтоп>У вас на работе трейдинг что-ли (трейд эрроры, котировки)?:)</оффтоп>
А потому, что Error возвращает удаленный сервер, вполне легитимный ответ и его надо обрабатывать. А Task может завалиться и по другой причине.
Что-то вроде :)
UFO just landed and posted this here
Потому что для написания CRUD'а C# по ряду причин подходит лучше. А когда этого CRUD'а ой как много, приходится как-то его структурировать.
UFO just landed and posted this here
Да хотя бы, чтобы не переучивать всю команду в середине проекта.
А так — здесь им идейку подкинул, тут структурку, глядишь, а они уже и сами упоролись куда надо.

Что это за новая мода — писать всю программу в одну строчку, а потом делать в ней переносы, когда она не входит в экран? Ладно еще запросы linq — это еще куда ни шло, но ветвления? Вы серьезно?

В условиях предпочитаю сначала писать rvalue:
public static TInput Do<TInput>(this TInput o, Action<TInput> action)
{
    if (null != o) 
        action(o);
    return o;
}


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

Даже на плюсах так никто уже не пишет.

В шарпе присваивание в условии вполне может быть
int a = 1;
if ((a = 2) == 3)
{
    a = 4;
}

В чем минус указанного подхода (с rvalue в первом операнде)?
А в чем преимущество присваивания в условии?
Все зависит от контекста. Например, при чтении из файла может быть удобно писать
while(null != (line = stream.ReadLine()))
{
}


Вместо
line = stream.ReadLine();
while(null != line)
{
/* ToDo */
line = stream.ReadLine();
}


Но мой вопрос был, в чем минус подхода в следующей записи:
if(null != expression)


При такой же читабельности получаем как плюс, исключение ошибки присваивания, и возможность единообразного использования как reference так и value типов.
То есть, стараться всунуть как можно больше в одну строчку, получая потенциальный рассадник ошибок и ухудшение понятности, это улучшение?
Не говоря уже о такой «ошибке присваивания, как
null = line;

Гипотетически, если тип приводится к bool, можно наворотить, но Вы же не пишете
if(b == false)
{
}
, правда?
Да, я пишу так:
if(false == b)
{
}

и не пишу так
if(!b)
{
}

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

Но вы отвечаете не на тот вопрос. Меня действительно интересует в чем минус, или возможная проблема случая когда в условии первым операндом стоит rvalue.
Про code style, naming conventions рассказывать?
Про семантику?

Сравните:
если мое_множество равно пустому_множеству то…
против
если пустое_множество равно моему_множеству то…

пустое множество не может равняться чему-то еще, оно одно такое, инициальный объект в этом типе.
Выше неудачное объяснение, громоздкое.
Смотрите, Вы говорите
If(false == b) {}
false — это константа, она ничему больше, кроме самой себя, равняться не может.
С этим, надеюсь, спорить не будете?
Так вот, странно семантически неверно сравнивать константу с чем-то. Что-то с константой- да, если это что-то может принимать разные значения.

Технически, никто Вам не запрещает писать так, как Вы пишете, ни компилятор, ни рантайм. Но читается это не очень…
Поэтому, собственно, весь остальной мир на шарпе и пишет if(value==null).

По итогу, ваш аргумент следующий, так писать не стоит потому что большинство пишет по другому? И никаких побочных, кроме нарушения стиля, нет?

Из комментария выше про множества, с моей точки зрения оба утверждения семантически равноценны, т.к. операция сравнения не предполагает порядок указания сравниваемых частей.
В том-то и дело, что не равноценны. Если Вы этого не видите, ну, что ж…
Про стиль и пальцы написали уже. У меня такое не то, что code review не прошло бы, даже commit завернуло бы.
К сожалению, не могу вспомнить точной терминологии про смысловые ударения, но основная идея в том, что предложения «мама мыла раму» и «рама мылась мамой», с одной точки зрения, описывают один и тот же процесс, но с другой, выделяют совершенно разные центральные объекты действия.

Языки программирования — они ж для человеков, не для роботов :)

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

ById тоже используем, только у нас в Code First, есть базовый абстрактный класс, от которого остальные наследуются, что-то типа:

    public abstract class Identified
    {
        public long Id { get; set; }
    }

и уже ему пишем расширение:

        public static T ById<T>(this IEnumerable<T> identifies, long id)
        where T : Identified
        {
            return identifies.SingleOrDefault(c=>c.Id.Equals(id));
        }


Кстати, почему у вас расширение только для IQueryable? можно сразу на IEnumerable, это может помочь дальше применять ById, но просто теперь для всех перечислений с этим типом.
А у нас так:
   public interface IHasId
    {
        object Id { get; }
    }

    public interface IHasId<out TKey> : IHasId
        where TKey: IComparable, IComparable<TKey>, IEquatable<TKey>
    {
        new TKey Id { get; }
    }


Для enumerable тоже бывает делаем расширения, но чаще нужны именно IQueryable.
Может, конечно, адово туплю, но для чего IHasId реализует IHasId? Честно говоря, не понимаю — зачем вообще первый интерфейс нужен.
Чтобы можно было сделать каст к IHasId без generic'а. Бывают случаи, когда тип T не доступен.
Если Т не доступен, то можно привести к
IHasId<object>
Спасибо за совет! Нужно проверить, что LINQ нормально отработает с ковариацией и выбросить без generic'а, если все ок.
Вспомнил, почему сделали так. У нас на IHasId<T> висит where T:IEquatable<T>. Так что привести к object нельзя, потому что object не реализует IEquatable. Убирать это условие не хочется, потому что тип ключа по определению должен быть сравним (чтобы иметь возможность быть уникальным). Интерфейс без T остался для поддержки композитных ключей.
А это все можно использовать с Entity framework? конкретно интересует пагинация.

Вот моя библиотека для программирования на C# в функциональном стиле: github repo
Получается код типа:


public IActionResult Add(string a, string b, string c, string d, int count = 1, int mod=0)
  => _dal.GetUrl(a, b, c, d).Convert(_dal.GetProduct)
     .IfNotNull(x => 
        _dal.AddProductToCart(_dal.GetCartBySession(HttpContext.Session.Id), x, count, mod)
     .Extend(x).Convert(PartialView), () => (IActionResult) NotFound());
UFO just landed and posted this here
Посмотрел. Пара замечаний:
1) В методах Add, Remove есть побочные эффекты.
2) Возможность добавить/удалить проверяется через list.GetType().IsArray и при передаче ReadOnlyCollection упадет с ошибкой. Лучше проверять через свойство IsReadOnly
C# — язык мультипарадигмальный

По мне так это язык ООП с элементами ФП, но никак не мультипарадигмальный. Не хватает например expression over statement или readonly на уровне аргументов и переменных для того что бы быть мультипарадигмальным.

Sign up to leave a comment.

Articles