Как стать автором
Обновить

Комментарии 102

var a = x.GetValueOrDefault(); // Сложное, большущее выражение. Не годится.
var b = x.OrDefault(); // Простое — как пишется, так и читается.
var c = x.Or(10); // А можно ещё вот как.

как по мне, первый вариант GetValueOrDefault более читабельный, можно сократить до GetOrDefault. На хешмапах начиная java8 так и сделали кстати
можно сократить до GetOrDefault

А потом, следуя советам второй статьи, мы убираем Get, и получается OrDefault — лаконично и просто, без излишеств.

У хешмапов есть еще putIfAbsent, put тоже будем убирать? :)
Неее, именно .GetOrDefault(key, 0) отлично ложится и читается

Put не будем, потому что это действие, и его мы хотим отразить.
Get будем, потому что это не действие, и его мы не хотим отражать.

Get будем, потому что это не действие
Речь про изменение состояния?

Не совсем понимаю, какое состояние меняется.


Вот есть словарь, а вот есть значение с ключом x. Что получается и что меняется?

Get это тоже глагол как и Put и это тоже действие. Но это действие не изменяет состояние/содержимое хэшмапа. Я и уточнил, что подразумевается под «это не действие»
Я и уточнил, что подразумевается под «это не действие»

Я подразумевал такую штуку: если нужно показать действие (Put), то мы используем глагол; если нужно показать просто какую-то вещь как часть другой — не используем.


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


Знаю, в Java всё равно пишут getWeight(), но, мне кажется, это кто-то зря придумал и очень-очень давно. Лучше weight(). Потому что вес — это не действие, которое яблоко умеет делать, а его свойство.

Почему «положить в» — это действие, а «вытащить из» — уже не действие?

Потому что "вытащить из" — промежуточный этап, который не так важен, как его результат.


Когда я кладу элемент в коллекцию Put, мне жизненно необходимо понимать, что нечто случится. Действие — конечная цель.
Когда мне нужен элемент коллекции по ключу, то не имеет значения, достаётся он, получается, и т.д. Даже если он достаётся из базы, он нужен всё равно. Элемент — конечная цель.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

Рекомендация сама по себе неплохая, но её слабость в следующем утверждении:


Because methods are the means of taking action

Это неверно.


Поэтому мы видим:


  • Where, Select и т.д.;
  • Enumerable.Empty<T>();
  • ImmutableArray.As<T>().
  • И т.д.
НЛО прилетело и опубликовало эту надпись здесь
А потом, следуя советам второй статьи, мы убираем Get

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

Вам бы попробовать AppleScript – возможно, мнение бы изменилось.
Максимум сходства с естественным языком… И это совсем не радует.

Максимум сходства с естественным языком обеспечивает разработчик. Язык программирования предоставляет для этого средства: лаконичный синтаксис, методы расширений, UFCS, шаблоны и макросы и т.д.


Что пользоваться этим всем можно не совсем элегантно — тоже верно.

НЛО прилетело и опубликовало эту надпись здесь

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


var a = x.GetValueOrDefault()"взять значение у x или значение по умолчанию (семантика Nullable выводится из контекста вызова) и поместить его в переменную a".


var a = x.OrDefault"a — это или x, или значение по умолчанию (семантика Nullable так же выводится)".


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


Дополню примером из Python, где синтаксис уже предполагает семантику or:


for item in items or []
НЛО прилетело и опубликовало эту надпись здесь
Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?

Одно и то же. Вы же не пишете int? numberOrNull, а пишете int? number (что, по вашей логике, неверно, поскольку там не число, а число или ничего). Некоторые детали отдаются типу и контексту, так что тут, на мой взгляд, всё в порядке.


Более того, мне интересно, как это противоречит самой идее включать название переменной в имя метода и тем самым добиваться fluent.


Нельзя заставлять миллионы разработчиков привыкать, им нужен четкий и понятный API.

Боюсь, я этого не утверждал...

Есть довольно устоявшиеся в шарпе First & Single. Какая семантика у вашего предложенного One?

Некоторые сокращения смущают, ибо нет понимания, что же они сделают. Меньше слов — не значит лучше. Например orders.All(); сам по себе — невнятен. Это может быть All от Linq (возвращает bool), может быть All с типом IQueryable (и я могу достроить выражение), а может быть что-то ещё. Читаю это примерно как «заказы всё», что «всё» — да просто всё, кончились. Угадывай, что там автор апи имел в виду.

UPD: для nullable есть простая старая конструкция:
int? x = null;
int value = x ?? 10;
заказы всё

Корректно: заказы все. Это значит — все заказы. Что это не вызов LINQ выведется из контекста.


UPD: для nullable есть простая старая конструкция:
int? x = null;
int value = x ?? 10;

Она тоже неплоха и лаконична, хотя и не так читаема, как x.Or(10).

Рассчитывать на контекст — плохая идея. Апи должно быть читабельным даже без него.

Кстати, что скажете насчет File.ReadLines и File.ReadAllLines? У меня к этим методам давний вопрос, как можно было сделать апи максимально неочевидным =)
Рассчитывать на контекст — плохая идея. Апи должно быть читабельным даже без него.

На мой взгляд, напротив: нужно всегда помнить, что у каждого вызова есть контекст, например, методы вызываются или в контексте имени переменной, или в контексте имени класса (если метод статический). Иначе получится Directory.CreateDirectory.


Проектируя методы с опорой на контекст, например, на определённую форму слова переменной, мы тем самым ещё и заставляем разработчика избегать всяческих двусмысленностей и коверканий: One лучше подходит к users, чем к usersRepository.


Кстати, что скажете насчет File.ReadLines и File.ReadAllLines? У меня к этим методам давний вопрос, как можно было сделать апи максимально неочевидным =)

И не подозревал о существовании File.ReadLines. Да уж, выглядит, конечно, не очень (с точки зрения API). Но я бы не хотел видеть File.EnumerateAllLines, которое, уверен, первым приходит в голову как хорошее решение.


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


public class FileLines
{
    public static FileLines Of(string path) =>
              new FileLines(path);

    private readonly string _path;

    private FileLines(string path) =>
        _path = path;

    public IEnumerable<string> AsEnumerable() =>
        File.ReadLines(_path);

    public string[] AsArray() =>
        File.ReadAllLines(_path);
}
Теперь не хватает только реализации всего этого.

Часть этих штук можно посмотреть у меня на GitHub.

НЛО прилетело и опубликовало эту надпись здесь
items.ExceptWith(other) на взгляд означает: вернуть items за исключением other,

Проблема в том, что ExceptWith ничего не возвращает.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
// Реальный классический подход
string path = ...;
Path.GetFileNameWithoutExtension(path);

path.Without(x).AtEnd определён для задач, предполагающих работу не только с расширениями файлов.


var x = path.Without(".exe"); // x это что? Я ожидаю, что это строка без всех вхождений ".exe", так ли это? И чем это отличется от path.Replace(".exe", "")?

К сожалению, C# не позволяет определить path.Without(".exe") так, чтобы он имел смысл, если не было вызвано никакое из продолжений вроде End. Тем не менее, это решается с помощью path.Without(".exe").Everywhere.

НЛО прилетело и опубликовало эту надпись здесь

Тут с вами соглашусь! Пожалуй, действительно, слабый момент. Я очень долго думал над тем, как избежать неоднозначности в таких случаях, когда мы ограничиваем какое-то значение нижним или верхним пределом (здоровье не может опуститься ниже нуля).


Улучшить, вероятно, стоит так:


Creature creature = ...;
int damage = ...;

// Было.
creature.Health = (creature.Health - damage).Or(0).IfLess();

// Стало.
creature.Health = (creature.Health - damage).ButNotLess(than: 0);
НЛО прилетело и опубликовало эту надпись здесь

Это находится за пределами темы статьи и обсуждения.

theCreature.Health = (theCreature.Health - damage).ButNotLess(than: 0).However().IfAndOnlyIf(theCreature is (Terribly)Lucky).Then(theCreature.Health + 2 * damage);

Не уверен, иронизируете вы или нет, но код действительно понятный и читаемый (хотя и непрактичный). Лучше в десятки раз, чем любые Utils.

Стойкое ощущение что автор предлагает тараканов из голов одних разработчиков (которые предложили те или иные подходы) заменить на тараканов из своей головы. Но беда в том, что тараканы от этого не перестают быть тараканами (что прекрасно подтверждают комментарии к статье).
Можно было бы тоже начать критиковать конкретные предложения из статьи (а многое из предложенных конкретных примеров и правда — очень плохо), но в этом нет никакого прикладного смысла. Ибо изначальная-то идея совершенно здравая — много думать над понятностью кода. А вот с реализацией — как в той картинке с программистом «Теперь-то я сделаю всё правильно!». Но это объясняется тем, что сложность, в целом, никуда нельзя убрать из индустрии разработки ПО. Это действительно сложно.
НЛО прилетело и опубликовало эту надпись здесь
Мне понятно так: выбрать все слова в Паскалевском стиле и объеденить их пробелами.

Вы всё верно поняли.

НЛО прилетело и опубликовало эту надпись здесь
Во всем этом семантичном и текстовом коде есть главная проблема. Программисты все сплошь в лучшем случае б2 (конечно, за исключениями), и поскольку кроме как на галеры английский язык на собесах не спрашивают (да и там не парятся), получаются вот эти перлы типа separated with вместо by.
Вот мы например по этой причине отказались от английского геркина в bdd и перешли на русский.
Программисты все сплошь в лучшем случае б2

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


получаются вот эти перлы типа separated with вместо by

Это несущественно, но точнее, если не ошибаюсь, как раз with, а не by. Пожалуйста.

Туше. Посыпаю голову пеплом и иду левелапать свой б2 =)
НЛО прилетело и опубликовало эту надпись здесь
Учитывая, что System.Linq.IEnumerable имеет аналогичный метод, читается как… ну, Вы поняли?

Согласен, двусмысленность есть. Но она — в примере, в демонстрации, общий принцип остаётся таким же: использовать using static, чтобы упрощать понимание и читаемость некоторых участков кода.


Приведу другой:


public enum JobStatus
{
    None = 0,
    Running = 1,
    Completed = 2,
    Cancelled = 3,
    Failed = 4,
}

public class Job
{
    public JobStatus Status { get; }

    public bool Is(JobStatus status) => Status == status;
}

// Как вызывать метод `Is`?
// Как обычно.
job.Is(JobStatus.Running);

// С помощью `using static`.
job.Is(Running); 

Разумеется, у using static есть свои нюансы (нужно, чтобы не было коллизий в именах), но, тем не менее, это инструмент!

Тема Лисп-макросов не раскрыта :)
Не, к C# они отношения не имеют, но это пример того, как в других языках есть средства создания «кода, похожего на естественный язык».
В отношении лиспа много где обсуждаются достоинства и недостатки такого подхода. Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому. В итоге шаблонный код оказывается в среднем более понятным и поддерживаемым, чем изящный и литературный.
var numbers = new [] { 1, 2, 3 };

// "Присоединяем" запятую к числам — не звучит.
var x = string.Join(", ", numbers);

// Разделяем числа запятой — интуитивно!
var x = numbers.Separated(with: ", "); 

;; Pure beauty!!!
(join numbers :into 'string :having-in-between ", ")
Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому.

Поэтому, как мне видится, в качестве судьи нужно использовать язык. Например, string.Join(", ", numbers) можно прочитать разве что как "присоединить к строке запятую и числа", но разве так можно сказать? Да и зачем, если уже есть выражение "separate with a comma"?


Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры. Ведь суть того же примера со string.Join не в самом string.Join, а в том, что над словом нужно думать, искать, подбирать (что также демонстрирует пример с ExceptWith). А то подчас простой по содержанию код похож на философский трактат с его интенциями, трансцендентностями и прочим.

Вот хоть убейте, Separate должен возвращать нечто разделенное (аналог string.Split). А у вас возвращается строка, склеенная из частей. Если бы я разбирался в АПИ незнакомой библиотеки, я искал бы concat, combine, join, glue, aggregate или что-то в этом роде. И когда, не найдя, я полез бы в справку и нашел там, что мне надо использовать separate, я подумал бы, что кто-то сошел с ума — или я, или автор библиотеки.
Особенно выразительно смотрится с пустым разделителем. chars.separate(""). Awesome

НЛО прилетело и опубликовало эту надпись здесь
Когда на входе — коллекция, а на выходе — строка, то это-таки «элементы, соединенные запятыми». Если же наоборот — то можно получить, допустим, список слов, разделенных [в исходной строке] пробелами.
НЛО прилетело и опубликовало эту надпись здесь
Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат.

Даже если оба слова сделать активными (join, separate) или пассивными глаголами (joined, separated), суть не изменяется. Первый про объединение, второй про разделение, и действия эти направлены противоположно. Увидеть separated там, где ожидаешь увидеть joined, не ложится в прицнип наименьшего удивления.
НЛО прилетело и опубликовало эту надпись здесь
С точки зрения непрограммиста типов данных не существует, и число 123 и строка «123» — это одно и то же. А в программировании это не так.
НЛО прилетело и опубликовало эту надпись здесь
Вот хоть убейте, Separate должен возвращать нечто разделенное

Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", "), как мне кажется, вполне однозначно говорит, что произойдёт. Не ожидаю же я, что мы разделим уже разделённое?


Кстати, именованный параметр with обязателен. Сделать его таковым язык не позволяет, но без него конструкция неполна.

Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", ")...

… как мне кажется, вызывает ступор и InvalidOperationException в мозгу.

SQL шел (идет?) по пути приближения к человеческому языку. Результат, как минимум, спорный, но и там не стали для объединения строк использовать ключевое слово SEPARATED BY. Назвать функцию STRING_AGG, думаю, тоже не было идеальным решением, но она хотя бы образована от AGGREGATE, не SEPARATE.
… как мне кажется, вызывает ступор и InvalidOperationException в мозгу.

Открываю руководство по расстановке запятых в английском языке, вижу "Thus, happy and lively are coordinate adjectives in the example and should be separated by a comma.".


Вот ещё по теме.


На мой взгляд, тут нужен mind-shift с императивного мышления на декларативное, где нет инструкций, а есть описания.

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

Тут я согласен. Поэтому поспорю :)


Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.


Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.

Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.

Как мне кажется, C# в этом смысле пока сильнее многих других языков: он позволяет создавать и субъектные выражения с помощью методов расширений, и безсубъектные с помощью using static. Кроме этого, он разрешает именованные параметры, а это также многократно улучшает читаемость и fluent-составляющую многих вызовов. Но мы всё ещё на этапе хелперов.


Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.

Как раз недавно искал какие-то новые языки, которые переосмысливают всю ту императивную кашу, которая осталась ещё с доисторических времён. Понравился Nim. Там кроме элегантного синтаксиса, есть шаблоны и макросы, позволяющие настроить язык под себя, как вы и сказали.

Fluent-подход отлично заходит, когда видишь одну строчку кода, и по ней понятно, что тут происходит. Например, создание маппинга в автомаппере:

configuration.CreateMap<OneThing, OtherThing>()
.ForMember(d => d.StatusId, ctx => ctx.MapFrom(s => s.Status))

(у меня тут правда большой вопрос к авторам, почему в CreateMap сначала Source, потом Destination, а в маппинге полей — наоборот. Ну да ладно, это к делу не относится)

Но когда в маппинг надо добавить выражения для 5, 10, 20 полей эта многословность начинает бесить. Куда выразительнее написать, к примеру,

configuration.CreateMap<OneThing, OtherThing>()
{
Status => StatusId,
Price => Cost,
Amount => Quantity,
Count => Number
};


Конечно, учитывая ограничения и синтаксис языка, это писалось бы, скорее так:
{ s => s.Price, d => d.Cost },. И тут, не зная контекста, сложнее понять что происходит, чем в случае fleunt-подхода. Но при многогратном повторении смотрится чище.

Судя по комментариям дальше, во мнении "коллегам удобно думать по-другому" я всё же не одинок.


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


var name1 = some_file_name;
var name2 = name1.Without(".exe");
var x = name2.AtEnd;

Ожидание — получить то, что на конце у name2, а name2 — это name1 без ".exe". x — это последняя буква имени файла без расширения! Разве не так? Именно это же написано.


Это не говоря о контекстной зависимости естественного языка, т.к. я обычно не добавляю "на конце" к фразе "имя файла без .exe". Ваши конструкции вынуждают всегда уточнять, где именно искать подстроку, т.е. программа всё равно пишется на некотором формальном языке, а не на естественном. А раз так, то мне лично было бы уже всё равно — это будет s.Without(".exe").AtEnd, s.RemoveSubstringFromEnd(".exe") или StringTools.RemoveSubstringFromEnd(".exe", s). Но если .AtEnd ещё терпимо, то вот необходимость писать s.Without(substr).Anywhere как-то совсем на естественный язык не похоже.


Как мне кажется, вам всё-таки надо брать язык с развитым метапрограммированием и демонстрировать применение этого подхода там, потому что там возможности расширения синтаксиса есть by design, без натягивания совы на глобус.

В итоге, делаются промежуточные объекты, бесполезные сами по себе.

А как быть, например, с FluentAssertions, которые позволяют:


// И так.
instance.Should().BeNotNull();
// И этак:
instance.Should().Should().Should().Should();
// И даже так:
var x = instance.Should();
var y = x.BeNull();

Как видите, не имеет значения, что объекты могут быть бесполезными сами по себе. Когда вы знаете грамматику, вам и в голову не придёт коверкать её только потому, что это возможно.


А раз так, то мне лично было бы уже всё равно — это будет s.Without(".exe").AtEnd, s.RemoveSubstringFromEnd(".exe") или StringTools.RemoveSubstringFromEnd(".exe", s)

Интересно, как из того факта, что "писать приходится на формальном языке" вырастает StringTools? Почему его не нужно всегда уточнять, а AtEnd нужно? Как отменяется сама форма естественного языка: субъект предложения, объект? Почему из невозможности написать совсем понятно, следует то, что нужно писать совсем непонятно?


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


Да, среди множества способов улучшения читаемости, существуют такие, которые поддерживаются языком или недостаточно полно, или совсем частично. Но из этого, как мне кажется, не следует, что:
а) так писать не нужно;
b) нужен другой язык;
c) нужно писать Tools и Utils.

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


Когда вы знаете грамматику, вам и в голову не придёт коверкать её только потому, что это возможно.

А где коверкание в x.Without(".exe")? Я вижу грамматически корректное выражение.


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

КакМнеКажется.Эта(Идея).ПлохоПодходит(для: РеализацииВ(C#)).


Просто с метапрограммированием и макросами вы можете реально ввести новый синтаксис, выглядящий как предложения. И это как раз будет нормально демонстрировать идею. А это вот We.Can(Write.CallChainsThatLook(like: "real sentences")) — по выражению Кристиана Шафмайстера, по сравнению с макросами Лиспа — что налоговая декларация по сравнению с сонетами Шекспира.

А где коверкание в x.Without(".exe")? Я вижу грамматически корректное выражение.

В x.Without(".exe") его нет. Оно есть в x.Without(".exe").AtEnd. Вот какое:


var name1 = some_file_name;
var name2 = name1.Without(".exe");
var x = name2.AtEnd;

КакМнеКажется.Эта(Идея).ПлохоПодходит(для: РеализацииВ(C#)).

Если дискуссия об этом, то не скажу, что совсем уж плохо, но что есть ряд особенностей — это верно.


И это как раз будет нормально демонстрировать идею.

Но идея как раз в том, чтобы писать код понятно, отвлечённо от языка. Если вы можете, то почему нет?


Посмотрите, например, на Spark. Вот пример API сохранения сразу на нескольких языках:


Python


x.write.save("...")
x.write.format("json").save("...")

Java


x.write().save("...")
x.write().format("json").save("...")

Не будем же мы писать вот такое:


a = x.write
b = a.format("json")
c = b.save("...")

И утверждать, что ребята полезли не в тот язык не с той концепцией? Боюсь, нет.


А это вот We.Can(Write.CallChainsThatLook(like: "real sentences")) — по выражению Кристиана Шафмайстера, по сравнению с макросами Лиспа — что налоговая декларация по сравнению с сонетами Шекспира.

Сравнение броское, но, как это часто бывает со сравнениями, отвлекает от сути. Чья-то действительность — C# или Java. В которых можно писать красивее, элегантнее, проще и опрятнее, чем GetUserById или UtilsManager.

Но идея как раз в том, чтобы писать код понятно, отвлечённо от языка. Если вы можете, то почему нет?

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


Не будем же мы писать вот такое:
a = x.write
b = a.format("json")
c = b.save("...")


И утверждать, что ребята полезли не в тот язык не с той концепцией? Боюсь, нет.

Так тут как раз всё вроде понятно, кроме того, что x.write не записывает непосредственно. И, опять же, интерфейс не стремится быть похожим на естественный язык ради похожести на естественный язык.


В целом, то, что надо стремиться, учиться и практиковаться писать кратко, красиво и понятно — это бесспорно. Но с какого-то момента начинается притягивание за уши. В языке нельзя вставлять аргумент посередине имени оператора — придумаем .Without("t").AtEnd. Ну чем оно принципиально лучше s.RemoveSubstring("suffix", fromEnd: true, count: 1) или хотя бы s.Without("t", at: "end")? Во втором случае s.Without("substring") вполне может возвращать строку без подстроки без необходимости городить дополнительные .Anywhere, бонусом можно этот метод реализовать и для других коллекций, а не только строк.

В языке нельзя вставлять аргумент посередине имени оператора — придумаем .Without("t").AtEnd.

Но почему нет?


Ну чем оно принципиально лучше s.RemoveSubstring("suffix", fromEnd: true, count: 1)

Давайте отойдём на секунду от x.Without(y).AtEnd, и посмотрим только на предложенное s.RemoveSubstring("suffix", fromEnd: true, count: 1) с точки зрения понятности и лаконичности.


  1. Remove хуже, потому что имеется ввиду не Remove, а WithRemoved. Нужно что-то, что покажет, что результирующая строка — это исходная без какой-то подстроки.
  2. Substring — как мне кажется, лишнее. Удаляя что-то из строки, мы итак понимаем, что это что-то — подстрока (substring).
  3. count: 1 — что-то новенькое. Опустим, как поведение по умолчанию.

Пока получается s.Without("suffix", fromEnd: true). Но что значит тогда: s.Without("suffix", fromEnd: false)? Вообще везде или на старте?


Можно было бы: s.Without("suffix", From.End) или s.Without("suffix", From.Start), но тогда перечисление From получается слишком общего назначения.


Хотя вот ещё: s.Without("suffix", from: End), где End — значение перечисления с более конкретным именем, например, PositionKind. Его импортируем через using static, и готово. Но это попросту неудобно, каждый раз писать и импортировать.


Можно через лямбды: s.Without("suffix", _ => _.FromEnd), s.Without("suffix", _ => _.FromStart). Но это тоже не очень удобно и громоздко.


s.Without("suffix", from: "end") — упираемся в возможность ошибиться при вводе "end".


Вероятно, лучше всего использовать совет от qw1 и взять s.WithoutSuffix("Builder"), s.WithoutPrefix("Abstract") и s.Without("part") как замены предложенным мной вариантам с цепочками.


Да, ещё можно остановиться на s.RemoveFromStart("suffix"). Как можно остановиться на Directory.CreateDirectory или QueueUserWorkItem.


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


А то, что язык искусственно созданный и формализованный — не значит, что там нет каких-то своих традиций и идиом. Тащить туда конструкции естественного языка "чтобы было удобнее читать" — это делать какую-то смесь французского с нижегородским.

К счастью, традиции и идиомы устаревают. И уже сегодня написано огромное количество fluent-библиотек (хотя не все они Fluent). Скажем, FluentAssertions и NSubstitute.

Вероятно, лучше всего использовать совет от qw1 и взять s.WithoutSuffix("Builder"), s.WithoutPrefix("Abstract") и s.Without("part") как замены предложенным мной вариантам с цепочками.

С этим согласен, особенно если суффиксы или префиксы из строк приходится убирать часто.


Ещё я понял, что куда-то со своими рассуждениями ушёл в сторону, потому что существенная часть — я тоже вижу проблему в стиле написания ObjectFactory.CreateObjectFromInput(InputContainer.Input).


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


Поэтому, например, x.Should().BeEqualTo(42, "because reasons") мне не нравится — assert уже вполне устоявшаяся идиома в программировании, причём в обычной речи слово редко встречается, а "should" выглядит как-то бесформенно.


То же самое относительно примера с Max() — это математическая нотация, так же как и символ присваивания в том же самом выражении. C Number.ButNotLessThan(LowerBound) теряется тонкая математическая красота и равнозначность аргументов, вместо неё одно значение становится "ведущим", т.к. метод вызывается из него (ну да, я технарь, я так вижу). Относительно близкая к естественному языку конструкция, которая у меня ещё не вызывает такого отторжения — что-то вроде GreaterOf(2, evils). Насчёт a > b ? a : b согласен — хотя и читаемо в принципе, но не очень красиво. В частности, потому, что Max() сразу говорит, что сравниваются числа, а в тернарном операторе это ещё визуально распарсить надо.

Насчёт Max тоже согласен с тем, что он, вероятно, лучше всего подходит для большинства случаев, особенно в записи вида: Max(of: a, b).


Но иногда получается вот как: скажем, если минимум здоровья — 0, то мы пишем при этом Health = Max(of: Health - Damage, 0), что, на мой взгляд, именно в этом конкретном случае не так интуитивно, как хотелось бы. Поэтому Health = (Health - Damage).ButNotLess(than: 0) как будто лучше передаёт идею.

Конкретно для этого случая — скорее, нужно иметь метод Character.TakeDamage(points), который всем этим занимается.


Вот, кстати, возникает вопрос тоже. В вашем стиле получается, что нужно писать hero.Take(damage) вместо hero.TakeDamage(damage). Но как тогда отличать Take(damage) от Take(pen)? Т.е. понятно, что можно сказать: Take — это у нас именно для принятия урона, а для "взятия" предмета у нас Pick, а для "выбора" (pick — это ж ещё "выбирать") будет ChooseOneAmong(). Но это же улучшит читаемость только правильно написанного кода, зато усложнит написание и кодревью — hero.Take(pen) vs hero.Pick(pen) не выглядит очевидной ошибкой, в отличие от hero.TakeDamage(pen) vs hero.PickObject(pen).


Ещё мне не очень нравится, что статические по сути методы вы предлагаете делать методами объекта. Во-первых, теряется единообразие — например, наряду с Math.sqrt(x) может возникнуть x.WithoutSign() вместо Math.Abs(x) "потому что мы говорим 'корень из x' и 'x без знака'" (а "модуль" — для наших разработчиков слишком сложное понятие). Во-вторых, нарушается пресловутая идиоматика "операции без побочных эффектов — в статические методы, изменение состояния — в методы объекта". В-третьих, как-то странно, что объект должен знать, какие функции можно вычислить от его значения — попахивает нарушением SRP. Далее мои домыслы: если в рантайме методы объекта создаются как лексические замыкания, то при создании каждого нового числа (напр., как результата промежуточного математического выражения) нужно для него создать ещё и все эти замыкания, которые ещё не факт что будут вызваны. Это просто гигантский оверхед на пустом месте.

Далее мои домыслы
Методы расширения это просто синтаксический сахар для вызова статического метода, у которого первый параметр — ссылка на объект. Компилятор даже позволяет использовать оба синтаксиса для вызова метода расширения:
как
player.TakeDamage(500),
так и
PlayerUtil.TakeDamage(player, 500);

В последнем случае можно указывать префиксом полный namespace (и если надо, сборку из которой импортирован метод), и разрешать конфликты с пересечением наименований.

Спасибо, понял.

Конкретно для этого случая — скорее, нужно иметь метод Character.TakeDamage(points), который всем этим занимается.

Но само выражение Health = (Health - damage).ButNotLess(than: 0) где-то написать придётся. Кстати, то же самое касается, например, восстановления здоровья. На мой взгляд, запись вида: Health = Min(of: Health + heal, MaxHealth) заставляет задуматься, а вот: Health = (Health + heal).ButNotGreater(than: MaxHealth) — совсем fluent.


В вашем стиле получается, что нужно писать hero.Take(damage) вместо hero.TakeDamage(damage). Но как тогда отличать Take(damage) от Take(pen)?

Альтернатив много:
character.Take(damage) и character.Inventory.Put(item),
character.Take(damage) и character.Pickup(item) и т.д.


Но в играх предметная область слишком сложна, чтобы можно было ограничиться такими простыми выражениями. Скорее всего, здоровье придётся менять напрямую с помощью character.Health.Change(by: -damage), ибо есть промах, крит. удар, броня, эффекты, и т.д.


Если совсем не избежать, то дублирование по имени параметра hero.TakeDamage(damage) не та уж страшно (хотя подумать, как избежать, точно стоит), как, скажем, Directory.CreateDirectory (но почему-то Directory.Delete). Они качественно отличаются.


Во-первых, теряется единообразие

Единообразие теряется только в каких-то отдельных случаях, но и этим можно пожертвовать. Общая идея методов расширений, как я уже писал, не просто в синтаксисе, а и в семантике того, что получается в записи: у статического метода появляется субъект. Например: char.IsDigit(c) не то же самое, что c.IsDigit().


Во-вторых, нарушается пресловутая идиоматика "операции без побочных эффектов — в статические методы, изменение состояния — в методы объекта"

Впервые слышу об этой идиоматике. Что она постулирует и чем чревато её нарушение?

НЛО прилетело и опубликовало эту надпись здесь
Часть этих штук можно посмотреть у меня на GitHub.
Ну так, полистал и не нашёл всякое типа
.Without(".exe").AtEnd;
.Or(0).IfGreater();
.ButNotLess(than: 0);

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

1) писать такое тяжко, т.е. в одноразовых (не core) классах, которые переписываются часто, не применимо.

2) если писать такое в core-классах, возникает overhead на создание промежуточных классов и копирования в них всего констекста.

А между прочим, какое-то время назад тут было интервью с экспертом по перфомансу в .NET, который советовал не париться насчёт производительности бизнес-логики, которую всё равно пишут люди разной квалификации, но вот core, common, util и т.п. серьёзно оптимизировать (вплоть до отказа от linq), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.
Ну так, полистал и не нашёл всякое типа

А вот же.


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

Это размазало бы статью. Особенно если учесть, что реализовать такое нехитро, речь ведь о форме и именах, а не алгоритмах.


писать такое тяжко

Не уверен. Как правило, так же, как и всё остальное. Что непривычно кому-то, кто всю жизнь писал GetUserById, — это верно.


если писать такое в core-классах, возникает overhead на создание промежуточных классов

Если вы про создание в рантайме, то overhead'а почти всегда нет, ведь создаются структуры. Если про написание кода, то да, определённый overhead появляется ввиду того, что в языке C# нет удобных способов реализовывать однозначные цепочки запросов.


А между прочим, какое-то время назад тут было интервью с экспертом по перфомансу в .NET, который советовал не париться насчёт производительности бизнес-логики, которую всё равно пишут люди разной квалификации, но вот core, common, util и т.п. серьёзно оптимизировать (вплоть до отказа от linq), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.

Большая часть материала — про имена и названия (которые определяют содержание), так что на производительность они никак не влияют.


В вопросах производительности контекст первичен. Иначе разработчики платформы .NET не написали бы LINQ, следуя указанным выше критериям.


Иными словами, если красивый код чего-то стоит, пусть стоит, не вызывайте его в while (true).


В моём опыте в основном было так: люди считают замыкания и упаковки, не понимая количество вызовов и общий трафик; избавляются от LINQ (пусть и в Common), хотя общая тенденция проектов такая, что 99% кода не влияют на производительность. Всё это излишне. Как правило, и так понятно: "О, вот тут что-то мне надо рекурсивно будет считать, теоретический масштаб вот какой, тогда, пожалуй, проверю, как оно себя ведёт, и подумаю, заменить ли List на HashSet".

Все аргументы справедливы.

Но лично для меня, конструкция
.Without(".exe").AtEnd;
имеет большие затраты на чтение и работу с ней, чем обычное
Path.GetFileNameWithoutExtension(...)

Я всегда стараюсь понимать до конца, что происходит в каждой строке. Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».

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

Во втором случае — обычный вызов статического метода, который делает ровно то, что у него в названии. Никаких лишних сущностей.
Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».

Не уверен, что утверждал именно это, предлагая x.Without(y).AtEnd.


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

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


С такой точки зрения "Name_postfix".Without("_postfix").AtEnd читается ровно так, как вы бы ожидали услышать в естественной речи. Или, скажем:


// На мой взгляд, вполне очевидно, что будет в переменной `x`.
var x = "D:\\git\\Repository".AllAfter("git\\");

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

Почему эти идеи встречают неприятие? Они заставляют переучиваться на новый стиль, и неизбежно ухудшать навыки понимания стиля, уже принятого повсеместно. Перейдя на новый стиль, старый код будет раздражать и появится желание всё переписать. Но в то же время, объективных плюсов (как, например, при переходе с C++ на C#) я не вижу, просто другой синтаксис.

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

Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.
Они заставляют переучиваться на новый стиль

Писать понятный и читаемый код — это не новый стиль.


Ещё лет двадцать назад Гради Буч в книге "Object Oriented Analysis and Design with Applications" цитировал другую работу 1989 года (Lins, C. 1989. A First Look at Literate Programming. Structured Programming.) — "Software should be written as carefully as English prose, with consideration given to the reader as well as to the computer".


Ему же приписывают: "Clean code reads like well-written prose".


Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.

На мой взгляд, разница утрирована. Куда точнее: "Все вокруг пишут философские трактаты, а нужно, оказывается, писать понятно и просто? Зачем?".

Это не просто и понятно, это какой-то ребус…
Вот реально,
fileName.Without(".exe").AtEnd
для меня читается как
Скрытый текст

Скрытый текст


Намного лучше же глагол+существительное:
fileName.RemoveSuffix(".exe")
Намного лучше же глагол+существительное:

Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано fileName.Without(".exe").AtEnd — ребус, а вот если скажет заказчик: "Here I'll need to show file name without .exe at end, can you do that?", то вопросов нет?


Кстати, если "show file name without extension", то сразу: fileName.WithoutExtension() (что предполагает AtEnd).


fileName.RemoveSuffix(".exe")

Но fileName не меняется после вызова Remove. Кроме того, суффикс — это понятие слова, а не целой строки.

Непонятен ваш критерий. Мой критерий — английский язык
Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.
Но fileName не меняется после вызова Remove
Получается название в функциональном стиле, где результат это применение функции ко входу, а вход не меняется )))

Кроме того, суффикс — это понятие слова, а не целой строки.
В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).
Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.

Из этого не следует, что она сложнее. Для LINQ запросов тоже нужны функции для завершения, и, тем не менее, они не являются сложными.

В LINQ-запросах промежуточные результаты являются осмысленными, и не требуют специальных «конструкций для завершения».
В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).

Кстати, верно, этого я не знал, прошу прощения!


Тем не менее, сравните сложность слов Remove и Suffix и Without и End (At мы в расчёт не берём). В первом случае вам нужно привлекать специалиста по computer science, а во втором — не обязательно. При этом мы, разумеется, ещё учитываем почти полную тождественность Without(x).AtEnd тому, как это произносится в речи.


Вот то самое стремление к минимальной сложности (т.е. к упрощению) и получается.

Я считаю сложность не по длине идентификаторов (тут auto-complete помогает), а по количеству синтаксических единиц. RemoveSuffix — один токен, Without+AtEnd+лишняя точка — три токена.
"D:\Source".AsDirectory().Copy().Files.To("D:\Target")

Где-то за деревьями потерялся лес.
Как по-вашему будет удаление файла?


"D:\\Source".AsDirectory().Delete().Files.Where(f => f.Name=="temp.txt")

Обычно пользователь сначала выбирает файлы, а потом действие.

Обычно пользователь сначала выбирает файлы, а потом действие.

Но говорим-то мы "copy files".

Возращаемся обратно к
CopyUtil.Copy(files);
или, используя языковые возможности C# 6,
using static CopyUtil;
Copy(files);

(если не нравится указывать префикс класса)
CopyUtil.Copy(files);

Потерялась папка.


Я бы предложил тогда:


Copy.Files(from: source, to: destination);

Зачем CopyUtil? Что это такое?

Стиль мышления технаря против стиля мышления гуманитария.

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

Класс Copy — это что? Копия? Нет, это класс, выполняющий копирование, т.е. копировщик (Copier, например).

Метод Files — это что? В отрыве от имени класса вообще бред.

Т.е. для аналитического типа мышления это категорически не заходит. Читать код как поэму, на одном дыхании — увы, не ко мне.
Стиль мышления технаря против стиля мышления гуманитария.

Мне кажется, это придуманное, а не фактическое положение вещей.


Что технического в CopyUtil?


Нет, это класс, выполняющий копирование

Это и есть копирование.


В отрыве от имени класса вообще бред.

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


Мало написать и реализовать алгоритм — это ничто по сравнению с тем, чтобы написать его доступным для других.

Это и есть копирование.
Сразу видно, вы не математик и в формализмы не умеете. Потому что формально, что CopyUtil, что ваш класс Copy — это класс, который… и далее по тексту.

То есть, вы привязываете интерфейс к английскому языку.
Если, например, на хинди порядок слов в предложении другой, то индусам надо выдать принципиально другой API?

То есть, вы привязываете интерфейс к английскому языку.

Мне кажется, вполне ожидаемо, что интерфейс на английском языке привязывается к английскому языку.

На мой взгляд, главная проблема с написанием кода, максимально похожего на естественный язык, в том, что фразы на естественном языке многозначны и могут быть истолкованы по разному. Часть работы программиста как раз и заключается в том, чтобы описание задачи на естественном языке перевести в однозначный формальный язык и от этого "перевода" никуда не деться. Взять хотя бы ваш IfLess. Вызывающему коду предоставляются многозначные "фразы" вроде "a или b, если меньше", а внутри — строго однозначный тернарный оператор.


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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории