Комментарии 127
Тут даже выглядит не сколько "слишком функционально", сколько "слишком похоже на Scala" (а это не совсем одно и то же).
Я бы даже больше сказал — скаловские решения в какой-то степени были обобщены до спарка, и при этом маштабирутся далеко за пределы 100 чисел (т.е. на терабайты примерно).
просто позвольте ему это
Спасибо, но нет. Аналогичный код в "классическом" исполнении не занимается лишними аллокациями и вызовами через делегаты, что даёт существенно лучшую скорость и существенно меньшую нагрузку на GC:
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------- |---------:|----------:|----------:|-------:|------:|------:|----------:|
| Normal | 1.344 us | 0.0261 us | 0.0382 us | 0.2441 | - | - | 1.5 KB |
| Functional | 2.005 us | 0.0401 us | 0.0588 us | 0.4120 | - | - | 2.55 KB |
(результаты приведены при замене Console.WriteLine на заглушку)
А можно привести конкретные фрагменты кода, использованные в бенчмарке?
Если код в точности соответствует приведенному в статье, то разница скорее всего на .ToList()
. В "классическом" случае у вас просто цикл по RangeIterator
, а в "функциональном" как минимум выделение памяти под список.
Наличие простого static void ForEach<T>(this IEnumerable<T> @this, Action<T> action)
должно ускорить процесс (но это неточно).
Плюс, говоря про фишки C# 8
, локальную функцию наверное нужно было объявить статической, хотя в данном случае это ни на что не повлияет.
А зачем вообще нужен tolist?
[MemoryDiagnoser]
public class FizzBuzz
{
static void WriteLine(string s)
{
}
[Benchmark]
public void Normal()
{
for (var c = 1; c <= 100; c++)
{
if (c % 3 == 0)
{
if(c%5==0)
WriteLine("FizzBuzz");
else
WriteLine("Fizz");
}
else if(c%5==0)
WriteLine("Buzz");
else
WriteLine(c.ToString());
}
}
[Benchmark(Baseline = true)]
public void Functional()
{
string FizzBuzz(int x) =>
(x % 3 == 0, x % 5 == 0) switch
{
(true, true) => "FizzBuzz",
(true, _) => "Fizz",
(_, true) => "Buzz",
_ => x.ToString()
};
Enumerable.Range(1, 100 )
.Select(FizzBuzz).ToList()
.ForEach(WriteLine);
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<FizzBuzz>();
Console.WriteLine(summary);
}
}
После замены ToList.ForEach
на рукописный ForEach<T>
:
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|----------- |---------:|----------:|----------:|-------:|----------:|
| Normal | 1.315 us | 0.0244 us | 0.0251 us | 0.2441 | 1.5 KB |
| Functional | 2.393 us | 0.0476 us | 0.0683 us | 0.2785 | 1.71 KB |
А можно ли на функциональном F# написать производительный код? Вряд ли, зато на C# — можно функциональный или производительный.
Честно — я не вижу где
foos.ToList().ForEach(x => doStuff(x))
понятнее и проще чем
foreach (var x in foos) { doStuff(x) }
Что забавно, для первого даже подсветка работает хуже, из-за чего читается (по крайней мере в некоторых редакторах, как, например, хабровском) труднее. Подробее со ссылкой на Липперта написал ниже.
Добавил:
var filteredGrouppedFoos = foos
.Where(x => x.Something == somethingElse)
.GroupBy(x => x.Id);
foreach (var x in filteredGrouppedFoos) { doStuff(x) }
Ну и я бы предположил что на шарпе большинство людей пишет не в хабровском редакторе, а всё-таки в VS и там я проблем с подсветкой не замечал.
Но как бы на вкус и цвет все фломастеры разные. И если вам удобнее так, то значит вам удобнее так.
Погодите, но где вы тут одну строчку-то увидели?
foos
.Where(x => x.Something == somethingElse)
.GroupBy(x => x.Id)
.ToList()
.ForEach(x => doStuff(x));
Если же вы это всё собираетесь в одну строчку запихать — то я на циклах тоже так могу:
foreach (var x in foos.Where(x => x.Something == somethingElse).GroupBy(x => x.Id)) { doStuff(x) }
Погодите, но ведь это именно тот вариант который вы предлагали))
Можно записать вот так
foos.Where(x => x.Something == somethingElse).GroupBy(x => x.Id).ToList().ForEach(x => doStuff(x));
И да на хабре это выглядит действительно ужасно. В нормальном IDE и пока не превышаешь «ширину экрана» читается нормально.
Но опять же это вопрос привычки. Я к linq и его форме записи тоже долго не мог привыкнуть. И это не значит что всем надо привыкать и переходить.
И вообще эти «холивары» по поводу что как удобнее или неудобнее писать/читать идут по моему по поводу каждого варианта, который шарп позволяет записать по разному. Я как-то уже и не вижу смысла по таким вопросам спорить.
4 строчки по 20 символов читать куда проще, чем одну на 80. Хотя бы потому что можно сразу выцепить ключевые слова "что", "откуда", "как". Даже если мне ширины экрана хватает, я пишу на каждой отдельной строке, если больше двух комбинаторов типа where(..).Select(..). Даже в IDE ваш "однострочник" читается фигово.
Попробуйте почитать старый html-сайты без css, где текст занимает всю возможную ширину. А потом посмотрите на хабр, который использует треть возможного пространства, но читается куда проще.
И вообще эти «холивары» по поводу что как удобнее или неудобнее писать/читать идут по моему по поводу каждого варианта, который шарп позволяет записать по разному. Я как-то уже и не вижу смысла по таким вопросам спорить.
Тогда зачем вы начали этот спор? :confused:
Даже если мне ширины экрана хватает, я пишу на каждой отдельной строке, если больше двух комбинаторов типа where(..).Select(..). Даже в IDE ваш «однострочник» читается фигово.
Я же говорю: вопрос привычки. Мне лично ваш вариант пришлось действительно пару раз «перечитать» прежде чем я его понял. А вот «однострочники» linq'a я понимаю обычно с первого взгляда.
Хотя да, если логигка сложная, то естественно всё не пишется в одну строку. Но такое у нас редко встречается.
Тогда зачем вы начали этот спор? :confused:
Да я как-то спор начинать и не хотел. Просто думал может если вы взглянете немного «под другим углом», то возможно и поменяете своё мнение. Ну а нет так нет.
Да я как-то спор начинать и не хотел. Просто думал может если вы взглянете немного «под другим углом», то возможно и поменяете своё мнение. Ну а нет так нет.
Как раз я раньше занимался экономией бумаги, и пытался максимально однострочно всё писать. Потом понял, что никому это не надо. Код с переносами куда легче читается, ну а то что он еще и не выглядит как хрень в онлайн редакторах, где я часто кидаю сниппеты в телеграм, например, это бонус.
Покажите, какой вариант читается проще и понятнее, а то я не представляю как это записать в 1 строку хоть как-то.
Ну и я бы предположил что на шарпе большинство людей пишет не в хабровском редакторе, а всё-таки в VS и там я проблем с подсветкой не замечал.
До 2019 студии которая начала всё подряд раскрашивать этот код не подсвечивается никак.
Продолжение истории с .ForEach() на конце в таком коде выглядит, естественнее, что-ли. Хотя в идеале хочется .Tap(), который дает возможность воткнуть эффект в любом месте цепочки, а не только в конце.
Вот только такой способ рассуждений никакого отношения к ФП не имеет, это именно ООП-мышление.
На том же чистом функциональном Haskell подобный код будет записан как-то так:
forM doStuff $ groupBy Id $ filter (\x -> Something x == somethingElse) $ foos
Удачи прочитать это в том же порядке, в каком записано :-)
В этой матрице нужно провести достаточно часов, чтобы быстро улавливать суть сквозь нагромождения синтаксиса. Есть подозрение, что ваш код не скомплируется. Проверьте)
С точностью до порядка аргументов (у forM сначала значение, А потом функция, а не наоборот):
import Data.Traversable
data FooB = FooB { id :: Int, something :: Int } deriving Show
foos :: [FooB]
foos = [FooB 10 20]
main :: IO ()
main = do
_ <- forM (filter (\x -> something x == somethingElse) foos) $ doStuff
pure ()
where
doStuff :: FooB -> IO ()
doStuff x = print x
somethingElse = 20
Что если forM заменить на forM_, ведь результат по сути не нужен?
Да, можно, просто его нет в Data.Traversable
, и я забыл где он лежит. Оказывается, в Control.Monad.
И как на счет groupBy id?
Пожалуйста
import Data.Function (on)
import Data.List
import Data.Traversable
import Control.Monad
data FooB = FooB { id2 :: Int, something :: Int } deriving Show
foos :: [FooB]
foos = [FooB 10 20]
main :: IO ()
main = do
forM_ (groupBy ((==) `on` id2) $ filter (\x -> something x == somethingElse) foos) $ doStuff
where
doStuff x = print x
somethingElse = 20
id2
потому что у хаскелля станадртная проблема с тем что прелюд экспортирует id x = x
.
С mapM_
поменьше скобочек получается, читается лучше:
import Data.Function (on)
import Data.List
import Data.Traversable
import Control.Monad
data FooB = FooB { id2 :: Int, something :: Int } deriving Show
foos :: [FooB]
foos = [FooB 10 20]
main :: IO ()
main = do
mapM_ doStuff $ groupBy ((==) `on` id2) $ filter (\x -> something x == somethingElse) foos
where
doStuff x = print x
somethingElse = 20
main :: IO ()
main = foos
& filter (\x -> somethingElse == something x)
& groupBy ((==) `on` id2)
& mapM_ doStuff
where
doStuff x = print x
somethingElse = 20
Выглядит как на C#. C той лишь разницей, что mapM_ там будет называться .ForEach(). Если даже хаскелю так можно, тогда почему функциональным шарпам нельзя?
А что касается коллизии с id, то records в хаскеле это и правда боль. Хорошо, что заметили. Ежики колются и продолжают грызть. В PureScript этот момент к счастью исправили, как и ряд других.
Выглядит как на C#. C той лишь разницей, что mapM_ там будет называться .ForEach(). Если даже хаскелю так можно, тогда почему функциональным шарпам нельзя?
Потому что forM_ всё же вычисляет IO ()
, а форыч непосредственно исполняет. На мой взгляд разница существенная, хотя, наверное, не все согласятся.
Ну да, в Хаскеле, оказывается, надо провести достаточно часов чтобы быстро улавливать суть. А в C# надо, значит, заменять оператор foreach
на .ForEach(...)
ради читаемости теми, кто достаточно часов ещё не провёл?
Я просто завел локальную переменную, чтобы не писать километровыое выражение в in
. Вы имеете что-то против локальных переменных? Может в хаскелле их нет? Не понимаю претензии, ни по факту, ни по теме "способа мышления".
foreach (var x in filteredGrouppedFoos) { doStuff(x) }
когда мысль без потери смысла выражается буквально в двух словах:.ForEach(doStuff)
Затем, что:
- вы не маскируете действие под функцией. Да Foreach по названию показывает что происходит, но когда строк кода сотни, легко можно не заметить
- я не раз видел как такой foreach реализовывали через ToList, потому что кто-то любит модифицировать коллекцию, по которой итерируются. Как-то один человек добавил в этот метод расширения ToList и только через месяцы когда в одном месте производительность совсем неприлично просела это вскрылось
- Сложно, но всё же можно получить ForEach(null)
- Это потеря производительности на ровном месте
- Как только понадобится дописать чуть-чуть кода, например doStuff(x, 10) то сразу же эту красоту придется переписать. И да, в большинстве случаев форыч состоит из по крайней мере двух строк.
- ...
Достаточно причин? Часть из них немного надуманные, часть — опыт таких "форычей" в некоторых проектах. Ах да, еще забыл
7.
В тех проектах что я видел такой форыч происходил в конце километрового LINQ-запроса, под конец которого забываешь, что собственно произошло. Наличие переменной которую можно хотя бы в watch запихнуть и посмотреть что же там находится — весьма ценно. Не говоря про то, что дебажить такой форыч — удовольствие на любителя, когда флоу скачет из лямбды в реализацию форыча и назад.
Ну это как возможный вариант для «6. ...»
ForEach
с yield return
это Select :)
Что смешно я не помню когда последний раз его использовал. С универа наверное. Всегда комбинаторы удобнее — select/where/aggregate/...
А Select их вам по идее выдаст сразу все.
И в зависимости от того как вы эти иксы получаете/высчитываете вы можете иногда при помощи yeld очень сэкономить.
Так в форыче у вас в каждый момент времени только текущий элемент, предыдущий и следующий не получить, только если вы аккумулятор какой не держите. Но всё это можно переписать с Aggregate 1в1 всегда.
Select не выдает сразу все. Он такой же ленивый. Skip/Take/… чтобы взять что нужно — всегда есть.
Но правда всё равно foreach и Select у нас не абсолютно эквивалентны и одно на другое не всегда можно заменить.
Тогда уж SelectMany...
В C#
функциональная запись не всегда выглядит внятно, но имеется ввиду что-то типа:
let sq x = x * x
seq {yield 1; yield 2; yield 3; yield 4; yield 5;}
|> Seq.map sq
|> Seq.map (printfn "%d")
Довольно сложную логику можно свести к такому последовательному применению функций через пайп |>
(и другие пайпы), и субъективно это может быть более читаемо, чем foreach
с телом.
linq хорош в ситуациях типа:
var groundNodes = switchings.Where(sw => sw.on == true).Select(sw => sw.chainSwitch).OfType<GroundDisconnector>().SelectMany(gr=>gr.Terminals).Select(t => tp.GetNodeNum(t.ConnectivityNode)).ToArray();
ForEach(x => doStuff(x))
Зачем создавать лишнюю лямбду, когда doStuff
уже имеет подходящую сигнатуру?
ForEach(doStuff)
Да, это касается только linq аллокаций, потому что по паттерн-матчингу код генерируется такой же оптимальный (sharplab.io).
Да, но паттерн-матчинг — это совсем не про функциональную парадигму, это про более удобный switch. Тормозят-то тут как раз функциональные элементы в виде функций высшего порядка (передача FizzBuzz в Select).
Т. е. в данном конкретном примере кода мы теряем на:
1) аллокация энумератора для Enumerable.Range
2) виртуальный вызов MoveNext у энумератора Enumerable.Range
3) вызов метода FizzBuzz через делегат
4) аллокация энумератора у Select
5) виртуальный вызов MoveNext у энумератора Select
6) аллокация списка в ToList
7) вызов Console.WriteLine через делегат из ToList
Эти все вещи жрут производительность. Причём не так страшно, что именно чистую производительность (тут потери могут быть мизерными), как memory traffic.
Не в любом. В том же Haskell используется параметрический полиморфизм на этапе компиляции вместо полиморфизма подтипов в рантайме, что открывает возможность инлайнингу и многим оптимизациям.
Возможно, после появления HKT что-то поменяется, но я бы на это не рассчитывал: если "в лоб" оптимизировать всё подряд, это увеличит нагрузку на JIT.
Если компилятор дотНет или JIT сделает инлайнинг, то они тоже сделают такие оптимизации. Не понимаю почему будет по другому.
Другое дело что код того же Select достаточно большой и я не думаю что компилятор будет его инлайнить. Как сделает хаскель я не знаю.
HKT наоборот сделает еще больше кода «однообразным». Сейчас код «дублируют» вручную, что позволяет делать микро оптимизации. HKT не для производительности.
Эмм, нет, как раз хкт для производительности, я в прошлой статье это как бы писал.
Потому что сейчас майкрософт берут, и пишут оптимизации для списка и тасок. Понадобилось чутка допилить — сделали ValueTask. Если у вас собственный awaitable или структура данных, чуть менее популярная, чем список (например, ImmutableSet), то все эти оптимизации вас не касаются.
Другое дело, если бы оптимизации были для любого траверсабла, как например сделаны некоторые в хаскелле.
Аргумент из зазряда "в го генерики не нужны потому что сейчас код «дублируют» вручную, что позволяет делать микро оптимизации". И дублируют не в кавычках, а совершенно в прямом смысле.
Аргумент из зазряда «в го генерики не нужны потому что сейчас код «дублируют» вручную, что позволяет делать микро оптимизации». И дублируют не в кавычках, а совершенно в прямом смысле.
Вы меня не поняли, я как раз за дженерики и тем более для классов, я даже такие такие ПР на гитхабе писал.
Я о том что вручную можно больше оптимизировать, но вручную как раз для всего не напишешь. Тут я с вами полностью согласен.
Не думаю что хаскель всегда делает параметрический полиморфизм. Это же сотни, если не тысячи варианты «одинакового» кода.
Весь этот "одинаковый код" должен после инлайнинга свернуться в простой цикл. Так что да, делает.
Не говоря уже о том, что тип полиморфизма зависит от спецификации языка, а не от желаний оптимизатора.
HKT наоборот сделает еще больше кода «однообразным».
Для программиста — да. Для JIT — наоборот.
Другими словами инлайнить код из библиотек должен JIT, а его делать слишком умным затратно по времени. Поэтому JIT делают двух, а то и трех проходными в зависимости от статистики выполнения кода.
Как хаскель относится к инлайну кода из библиотек во время компиляции в байт код? Я не думаю что он так делает…
У Хаскеля нет отдельного байт-кода (в ghc-версии), там из исходников собирается нормальный бинарник. Библиотеки поставляются, насколько я знаю, в исходниках.
Другими словами инлайнить код из библиотек должен JIT, а его делать слишком умным затратно по времени.
Вы сейчас повторили то, что я вам писал двумя комментариями ранее.
У Хаскеля нет отдельного байт-кода (в ghc-версии), там из исходников собирается нормальный бинарник. Библиотеки поставляются, насколько я знаю, в исходниках.
Это объясняет большую разницу в подходе к компиляторам между дотнет и хаскель.
Вы сейчас повторили то, что я вам писал двумя комментариями ранее.
Я читал. Просто я не думал что у хаскеля настолько другой подход к библиотекам и у него нет JIT компиляции.
Ничто не мешает сделать аот компилятор (для джавы давно сделали, для дотнета делали, но потом бросили), и делать сколько угодно проходов с любыми оптимизациями.
Зависит от компилятора, хаскельный например всё это дело наотлично фьюзит. Но шарповый джит — тупенький, и так делать не умеет и не будет уметь. См. project steno, там майкрософт это детально исследовал.
А вот если взять раст, то там итераторы зирокост, и разницы между двумя вариантами там не будет. Скорее наоборот, иногда из обычного цикла компилятор не догадывается выкинуть баунд чеки, а из итераторного кода — всегда.
Аллокации в паттерн матчинге со временем уйдут, во-первых мой PR для .NET 5: https://github.com/dotnet/runtime/pull/1817
Во-вторых, когда пофиксят вот это: https://github.com/dotnet/runtime/issues/9118 (который в идеале уберет очень много ненужных боксингов)
Если оригинальный код тоже функциональный — то зачем его было приводить? Зачем чинить то, что работает?)
Мне показалось, что основная мысль статьи в том, что некоторым людям код с использованием лямбд показался «слишком функциональным», а автор говорит о том, что «функциональность» кода кроется, в первую очередь, в использовании выражений (expressions) как «чистых» функций.
А насчет «чинить то что работает», рефакторинг разве не про это?
Не в коем случае не топлю за ООП или ФП. Считаю их инструментами, которые нужно применять по назначению.
P.S. Статью нашел в ходе подготовки к собеседованиям. В одном из описаний вакансии увидел, что бонусом кандидату будет способность поддержать беседу насчет ООП VS ФП. Надеюсь, что уже слегка готов.
P.P.S. И да, Ваши статьи по теме в очереди на вдумчивое изучение.
Меньше чем на последующие выяснения и исправления "а какого хрена у нас система на 100 пользователях проводит 99% времени в blocking GC". Пушто в профайлере мы увидим так называемую смерть от тысячи порезов, когда аллоцирует и притормаживает вообще всё.
Но если вы на проект пришли на год, а потом сбежите на следующую работу, то можно и не "оптимизировать", да.
по Парето 80/20: только 20% кода требуют оптимизации, которые принесут 80% производительности.
В случае с бездумным использованием LINQ на ряде проектов это просто не работает. В профайлере видна картина, когда нет конкретного места, которое тормозит. И это очень и очень дорого потом исправлять.
Про чистый код и оптимизации — несколько неверно. Тайм критикал задачи почти всегда превращаются в лапшекод, либо в вызовы простейших методов, на которых висит AggressiveInlining.
В случае тайм критикал нужно максимально избегать аллокаций и дальних переходов.
И естественно минимальное количество async/await, LINQ и т.д.
Тогда стоит брать подходящий язык. Например, у нас один из микросервисов на расте написан. Всё красиво, высокоуровнево. Инлайн атрибуты конечно висят, некоторые места подправлены в угоду производительности, но в целом всё же высокоуровнево и красиво. При том что у нас там мой коллега уже полгода лазит с интеловым vtune и оптимизирует каждую мелочь. Сейчас выдает 84% использования архитектуры, если вы знаете о чем речь, то это должно о многом сказать.
В общем, оптимизации не всегда связаны с вырвиглазным кодом, иногда можно просто чуть-чуть поменять инструмент. Во времена микросервисов можно буквально парочку написать на слегка другом стеке (у нас везде сишарп, а этот сервис — раст), и получить очень приличный выигрыш (в нашем случае — 2 порядка производительности).
Мне кажется, у вас просто разный опыт и разные проекты. Например, отношение к ОРМ у людей с опытом работы с БД 10gb- и 1tb+ диаметрально противоположенный. Не стоит переносить свой опыт на другие проекты.
Впрочем, это общий совет. По тексту статьи я ниже высказался — форыч как метод не нужен.
О, классический аргумент апологетов ФП: "это не наш код тормозит, это компилятор плохой".
0) Как что-то плохое
Ну просто почему-то выходит, что компиляторы все плохие, апологеты все хорошие, а код в итоге всё равно тормозит.
Код с идентичной семантикой должен транслироваться в идентичный байткод, разве нет?
На каком основании? Компилятор ничего не знает про "семантику", для него это просто вызовы методов .Range
, .Select
, .ToList
и .ForEach
, реализация которых может измениться уже после компиляции.
Объявление лямбды в методе, если конечно он не предназначен только для объявления лямбд, выглядит как нарушение принципа единственной ответственности.
string[] GetUserEmails(IdOf<User> userId) =>
_dataContext.UserEmails.Where(x => x.UserId == userId.Value).ToArray();
Этот код предназначен для объявления лямбд или нарушает принцип единой ответственности? Можете поподробнее рассказать об этом?
Представьте, что у вас несколько мест, где нужно применить этот фильтр.
Везде вызову функцию GetUserEmails
. Если мне нужен будет queryable поменяю возвращаемый тип. Если мне надо будет сам фильтр переиспользовать — ну ок, вынесу его как функцию:
Expression<Func<UserEmail, bool>> GetFilterByUserId(IdOf<User> userId) =>
x => x.UserId == userId.Value;
И применяю везде, где надо.
- константы выносят обычно ради комментирования "что за константа". Например foo 1800 непонятно, а foo secondsInHour/2 уже лучше. Причем деление на два не выносится, потому что из контекста должно быть понятно, что к чему
даже с константами их часто пишут инлайн.
_logger.Log("Bad thing {badThing} happened at address {address}", badThing, address);
вот в таком виде я часто вижу что пишут. А вот чтобы это в константу выносили — разве что у майкрософта, озабоченного локализацией ошибок.
Например, если есть некий массив данных, по которому надо пройти, проверить много условий с привлечением множества данных и в этом же массиве что-то скорректировать по результатам, тогда это явно императивного стиля постановка задачи и нужен foreach и if-ы.
Если есть массив данных, по которому надо пройти с условиями и построить новый массив, то здесь linq сработает прекрасно и дебажить его не придется.
Только в стандартный библиотеках C# нет ForEach или аналога для нумератора.
Его можно написать самому или использовать обычный цикл foreach.
просто позвольте ему это
А есть способ не «позволить» а заставить? Хочу чтобы программист не имел альтернатив в некоторых случаях?
Насчет форычей например можно процитировать Эрика:
I am philosophically opposed to providing such a method, for two reasons.
The first reason is that doing so violates the functional programming principles that all the other sequence operators are based upon. Clearly the sole purpose of a call to this method is to cause side effects. The purpose of an expression is to compute a value, not to cause a side effect. The purpose of a statement is to cause a side effect. The call site of this thing would look an awful lot like an expression (though, admittedly, since the method is void-returning, the expression could only be used in a “statement expression” context.) It does not sit well with me to make the one and only sequence operator that is only useful for its side effects.
The second reason is that doing so adds zero new representational power to the language. Doing this lets you rewrite this perfectly clear code:
foreach(Foo foo in foos){ statement involving foo; }
into this code:
foos.ForEach((Foo foo)=>{ statement involving foo; });
which uses almost exactly the same characters in slightly different order. And yet the second version is harder to understand, harder to debug, and introduces closure semantics, thereby potentially changing object lifetimes in subtle ways.
Паттерн матчинг вместо ифчиков — дейсвтительно выглядит лучше, но к такому стилю нужно привыкнуть.
В тему статьи могу добавить, что после релиза восьмого шарпа вот так проверяю на нулл:
if (xs.FirstOrDefault() is null) { ... }
Так на не нулл:
if (xs.FirstOrDefault() is {} x) { ... }
Почему так? Две причины:
- этот код валидно работает со структурой такого вида:
class Weird : IEquatable<Weird> { public bool Equals(Weird other) => true; public override bool Equals(object obj) { return obj is Weird other && Equals(other); } public override int GetHashCode() => 0; public static bool operator ==(Weird left, Weird right) => left.Equals(right); public static bool operator !=(Weird left, Weird right) => !left.Equals(right); }
а обычные проверки на нулл — нет.
- переменная `x` существует только в скоупе ифа, и компилятор при попытке обратиться к ней за пределами блока, где переменная null, ругнётся на попытку использовать неинициализированное значение. Весьма удобно
Это всё не является «фпшностью», просто можно больше использовать компилятор и меньше полагаться на несовершенную человеческую (не)внимательность.
Код который с кортежем двух булов — полная хрень. Задача fizzbuzz не про то, как написать красиво или с меньшим количеством ифов. Она про то, как дальше будет работаться с этим кодом. Ответ — будет хреново, потому что при изменении одного условия (x % 4 для fizz, например) нужно будет это полностью выкинуть и написать заново.
Поменял (x % 3 == 0, x % 5 == 0)
на (x % 4 == 0, x % 5 == 0)
, ничего выкидывать не пришлось. Чяднт?
То что я изменил условие только на fizz, a на fizzbuzz не менял. Давай ещё раз.
А, в этом плане. Ну тут предполагается, что эти условия зависимые. Это из разряда отнаследовать два класса от одного базового, где поля совпадают. А потом вопрос "а что если я в этот базовый класс 100500 новых методов добавлю, которые второму классу не нужны?". Ну, фигня получится, нужно будет рефачить.
Поэтому вопрос — связаны эти условия друг с другом или нет. Потому что если да, то у вас обратная проблема — в одном месте поменяли, а во втором — забыли...
К слову это не надуманная фигня — я так код пишу, и очень симпатично выходит.
Не-е, вы не путайте, там совершенно не так написано. Аналогом обсуждаемого кода был бы вот такой код:
let processing_info = match (update.message.from.is_some(), update.message.document.is_some(), update.message.photo.is_some()) {
(true, true, _) => Some((update.message.from.unwrap(), update.message.document.unwrap().file_id)),
...
}
нет, это как раз такой же код. Можно было бы написать:
if let Some(from) = update.message.from && let Some(document) = message.document {
...
}
else if if let Some(from) = update.message.from && let Some(photo) = update.message.photo {
...
}
else { ... }
Тот же вопрос: ифы или матч. Мне матч нравится больше.
А вообще boolean blindness, в реальном коде который не злоупотребляет булами код выглядит скорее как у меня по ссылке, нежели как в физзбаззе.
То же самое про оптимизаторов, которые начинают выводить не строчку «fizzbuzz», как было в условии, а «fizz» и «buzz» отдельно. Это просто повезло, что она состоит из двух этих слов, в условии нет никакой связи.
На вопрос связаны ли 3 и 3, и fizz c fizzbuzz, я бы ответил — это неизвестно.
Ну раз неизвестно, значит я могу предполагать любой вариант. Из двух вариантов стоит взять тот, который проще, потому что зачем платить за рефакторинг заранее? Если понадобится — сделаю третье условие:
string FizzBuzz(int x) =>
(x % 3 == 0, x % 5 == 0, x % 3 == 0 && x % 5 == 0) switch
{
(_, _, true) => "FizzBuzz",
(true, _, _) => "Fizz",
(_, true, _) => "Buzz",
_ => x.ToString()
};
В таком виде он не лучше ифов, конечно, но сама возможность имеется.
Но да, то что физзбазз не связан с физз и базз это конечно совершенно нелепое предположение.
И да, ты сделал именно то, о чем я говорил в первом комментарии — переписал код на 100%.
Мне жаль людей, которые тебе платят за код, в общем:) Удачи.
Даже, если, в общем случаем, это было бы две функции:
1. проверка всех условий (по-отдельности для каждой замены числа на строку + вариант, когда выводим всё-же само число) и возврат какого-то перечисления.
2. Сопоставление перечисления на конечный результат.
В таком случае мы можем не переписывать вторую функцию, ограничившись изменением первой.
Но, для примера из данной статьи, это, думаю, излишне.
string FizzBuzz(int x) =>
(x % 3 == 0, x % 4 == 0, x % 5 == 0) switch
{
(_, true, _ ) => "Fizz",
(true, _, true) => "FizzBuzz",
(_, _, true) => "Buzz",
_ => x.ToString()
};
или так:
string FizzBuzzInt(int x) =>
(x % 3, x % 4, x % 5) switch
{
(_, 0, _) => "Fizz",
(0, _, 0) => "FizzBuzz",
(_, _, 0) => "Buzz",
_ => x.ToString()
};
(x % 3, x % 5) switch
{
(0, 0) => "FizzBuzz",
(0, _) => "Fizz",
(_, 0) => "Buzz",
_ => x.ToString()
};
Ваш C# уже «функциональный», просто позвольте ему это