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

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

Тут даже выглядит не сколько "слишком функционально", сколько "слишком похоже на Scala" (а это не совсем одно и то же).

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

Я бы даже больше сказал — скаловские решения в какой-то степени были обобщены до спарка, и при этом маштабирутся далеко за пределы 100 чисел (т.е. на терабайты примерно).
Да я и не говорю, что это плохо (ибо сам на скале пишу). Другое дело, что «функционализм» скалой не ограничивается.
Pattern matching — функциональная конструкция. Только Rust умеет заимствовать ссылки на подобъекты и их модифицировать, но и там это экзотика.
просто позвольте ему это

Спасибо, но нет. Аналогичный код в "классическом" исполнении не занимается лишними аллокациями и вызовами через делегаты, что даёт существенно лучшую скорость и существенно меньшую нагрузку на 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);
        }
    }
а почему JIT не выбросил код с L002f по L004a? в c++ в цикле ничего лишнего не осталось.

После замены 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 |

Ну памяти стало выделяться меньше, так что ToList() действительно у автора был лишний. В любом случае в Functional дергается Linq, поэтому он всяко будет медленее, а убрав Linq — потеряется "функциональность" кода.


Спасибо за бенчмарки и разъяснения.

С быстродействием это так и есть, но в большой части приложения это разные задачи: для одного кода прежде всего важно, чтобы он работал быстро, а для другого кода важно победить сложность и написать красиво и понятно. Обычно можно в готовой программе переписать несколько критических функций с linq на for(i=… и этого достаточно.

А можно ли на функциональном F# написать производительный код? Вряд ли, зато на C# — можно функциональный или производительный.

Честно — я не вижу где


foos.ToList().ForEach(x => doStuff(x))

понятнее и проще чем


foreach (var x in foos) { doStuff(x) }



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

А вы добавьте ещё какой-нибудь «where» и/или «select» и/или «orderBy/groupBy».

Добавил:


var filteredGrouppedFoos = foos
    .Where(x => x.Something == somethingElse)
    .GroupBy(x => x.Id);
foreach (var x in filteredGrouppedFoos) { doStuff(x) }
И уже четыре строки вместо одной. Я понимаю что это вопрос привычки, но linq однозначно получается компактнее. То есть в среднем надо меньше скроллить и больше видно за раз.

Ну и я бы предположил что на шарпе большинство людей пишет не в хабровском редакторе, а всё-таки в 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 студии которая начала всё подряд раскрашивать этот код не подсвечивается никак.

Вот этот пример, на мой взгляд, самый вырвиглазный. Язык отражает способ мышления. Ведь ход рассуждений наверняка был примерно такой: из foos выбрать определенные x, cгруппировать их по id и…. А тут мы блин делаем паузу, приседаем, подпрыгиваем возвращаемся к началу, заводим переменную, а потом приседаем еще раз вниз, вставляем foreach и продолжаем уже в императивном стиле.

Продолжение истории с .ForEach() на конце в таком коде выглядит, естественнее, что-ли. Хотя в идеале хочется .Tap(), который дает возможность воткнуть эффект в любом месте цепочки, а не только в конце.

Вот только такой способ рассуждений никакого отношения к ФП не имеет, это именно ООП-мышление.


На том же чистом функциональном Haskell подобный код будет записан как-то так:


forM doStuff $ groupBy Id $ filter (\x -> Something x == somethingElse) $ foos

Удачи прочитать это в том же порядке, в каком записано :-)

Лишь с TeX и Perl порой на хаскеле код по читабельности соревноваться может. Хаскель дает массу возможностей для перестановки слов местами, оставляя программу понятной компилятору (как первое предложение в этом абзаце). Только проблемы PEBKAC это не отменяет.

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

С точностью до порядка аргументов (у 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_, ведь результат по сути не нужен? И как на счет groupBy id?
Что если 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

Спасибо хуглу :)

Если импортировать (&) из того же Data.Function, тогда выражение можно записать в прямом порядке, а не задом-наперед:
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)

Затем, что:


  1. вы не маскируете действие под функцией. Да Foreach по названию показывает что происходит, но когда строк кода сотни, легко можно не заметить
  2. я не раз видел как такой foreach реализовывали через ToList, потому что кто-то любит модифицировать коллекцию, по которой итерируются. Как-то один человек добавил в этот метод расширения ToList и только через месяцы когда в одном месте производительность совсем неприлично просела это вскрылось
  3. Сложно, но всё же можно получить ForEach(null)
  4. Это потеря производительности на ровном месте
  5. Как только понадобится дописать чуть-чуть кода, например doStuff(x, 10) то сразу же эту красоту придется переписать. И да, в большинстве случаев форыч состоит из по крайней мере двух строк.
  6. ...

Достаточно причин? Часть из них немного надуманные, часть — опыт таких "форычей" в некоторых проектах. Ах да, еще забыл


7. В тех проектах что я видел такой форыч происходил в конце километрового LINQ-запроса, под конец которого забываешь, что собственно произошло. Наличие переменной которую можно хотя бы в watch запихнуть и посмотреть что же там находится — весьма ценно. Не говоря про то, что дебажить такой форыч — удовольствие на любителя, когда флоу скачет из лямбды в реализацию форыча и назад.

Кстати а yeld c ForEach() будет работать? Кто-то знает точно? Настолько привык использовать его с foreach что до сих пор даже не пришло в голову попробовать.

Ну это как возможный вариант для «6. ...»

ForEach с yield return это Select :)


Что смешно я не помню когда последний раз его использовал. С универа наверное. Всегда комбинаторы удобнее — select/where/aggregate/...

Далеко не всегда. Прикол yeld в том что он выполняется «секвенциально» и вы в любой момент можете этот процесс остановить. То есть если вы скажем ищите x==3, и их у вас несколько, то вы их получите один за другим и после первого «попадания» вы можете остановиться.

А Select их вам по идее выдаст сразу все.

И в зависимости от того как вы эти иксы получаете/высчитываете вы можете иногда при помощи yeld очень сэкономить.

Так в форыче у вас в каждый момент времени только текущий элемент, предыдущий и следующий не получить, только если вы аккумулятор какой не держите. Но всё это можно переписать с Aggregate 1в1 всегда.


Select не выдает сразу все. Он такой же ленивый. Skip/Take/… чтобы взять что нужно — всегда есть.

Вот что значит писать технические комментарии на ночь глядя. Естественно вы правы и это применимо к любому foreach. А yeld дополнительно позволяет итерировать по списку, который будет создаваться по необходимости.

Но правда всё равно foreach и Select у нас не абсолютно эквивалентны и одно на другое не всегда можно заменить.

Ну тогда SelectMany, как ниже верно говорят. Если вам кажется, что что-то нельзя реализовать — приведите пример.

Да, SelectMany похоже действительно вариант.
Потому, что SelectMany это прямой аналог fmap.
Здесь вы правы :)

Тогда уж 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 с телом.

Мне не нравится идея писать в экспрешн-виде код с сайд-эффектами, а уж тем более код, единственной целью которого является сайд эффект. Возвращал бы этот код IO по поторому делаем sequence — да не вопрос. Но ведь это сишарп, и тут происходит не это

Не проще конечно, если код императивный по сути, то 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.

В любом функциональном языке будет тоже самое. Ну кроме вызова ToList он тут «немного лишний» (нужен только потому что у нумератора нет метода ForEach).

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


Возможно, после появления HKT что-то поменяется, но я бы на это не рассчитывал: если "в лоб" оптимизировать всё подряд, это увеличит нагрузку на JIT.

Не думаю что хаскель всегда делает параметрический полиморфизм. Это же сотни, если не тысячи варианты «одинакового» кода.

Если компилятор дотНет или JIT сделает инлайнинг, то они тоже сделают такие оптимизации. Не понимаю почему будет по другому.

Другое дело что код того же Select достаточно большой и я не думаю что компилятор будет его инлайнить. Как сделает хаскель я не знаю.

HKT наоборот сделает еще больше кода «однообразным». Сейчас код «дублируют» вручную, что позволяет делать микро оптимизации. HKT не для производительности.

Эмм, нет, как раз хкт для производительности, я в прошлой статье это как бы писал.


Потому что сейчас майкрософт берут, и пишут оптимизации для списка и тасок. Понадобилось чутка допилить — сделали ValueTask. Если у вас собственный awaitable или структура данных, чуть менее популярная, чем список (например, ImmutableSet), то все эти оптимизации вас не касаются.


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




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

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


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

Я о том что вручную можно больше оптимизировать, но вручную как раз для всего не напишешь. Тут я с вами полностью согласен.

Поэтому наилучший подход, что я знаю: обобщённый код + специализация.

Не думаю что хаскель всегда делает параметрический полиморфизм. Это же сотни, если не тысячи варианты «одинакового» кода.

Весь этот "одинаковый код" должен после инлайнинга свернуться в простой цикл. Так что да, делает.


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


HKT наоборот сделает еще больше кода «однообразным».

Для программиста — да. Для JIT — наоборот.

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

Другими словами инлайнить код из библиотек должен JIT, а его делать слишком умным затратно по времени. Поэтому JIT делают двух, а то и трех проходными в зависимости от статистики выполнения кода.

Как хаскель относится к инлайну кода из библиотек во время компиляции в байт код? Я не думаю что он так делает…

У Хаскеля нет отдельного байт-кода (в ghc-версии), там из исходников собирается нормальный бинарник. Библиотеки поставляются, насколько я знаю, в исходниках.


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

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

У Хаскеля нет отдельного байт-кода (в ghc-версии), там из исходников собирается нормальный бинарник. Библиотеки поставляются, насколько я знаю, в исходниках.


Это объясняет большую разницу в подходе к компиляторам между дотнет и хаскель.

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

Я читал. Просто я не думал что у хаскеля настолько другой подход к библиотекам и у него нет JIT компиляции.

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

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

Зависит от компилятора, хаскельный например всё это дело наотлично фьюзит. Но шарповый джит — тупенький, и так делать не умеет и не будет уметь. См. project steno, там майкрософт это детально исследовал.


А вот если взять раст, то там итераторы зирокост, и разницы между двумя вариантами там не будет. Скорее наоборот, иногда из обычного цикла компилятор не догадывается выкинуть баунд чеки, а из итераторного кода — всегда.

Аллокации в паттерн матчинге со временем уйдут, во-первых мой PR для .NET 5: https://github.com/dotnet/runtime/pull/1817
Во-вторых, когда пофиксят вот это: https://github.com/dotnet/runtime/issues/9118 (который в идеале уберет очень много ненужных боксингов)

Отличный фикс! Почитал дифф, очень понравилось. Спасибо за работу)

Нельзя не согласиться с Вами. Но мне кажется статья немного о другом. Автор говорит, что «функциональность» заключается не в лямбда выражениях или других языковых конструкциях. Идея в том, что «функциональным» код становиться при использовании expressions, со строго определенной сигнатурой. То есть «классический» код из статьи тоже «функционален».

Если оригинальный код тоже функциональный — то зачем его было приводить? Зачем чинить то, что работает?)

Я наверное рискую, потому что пока еще слишком далек от профессиональной разработки. Но выскажу свое мнение.
Мне показалось, что основная мысль статьи в том, что некоторым людям код с использованием лямбд показался «слишком функциональным», а автор говорит о том, что «функциональность» кода кроется, в первую очередь, в использовании выражений (expressions) как «чистых» функций.
А насчет «чинить то что работает», рефакторинг разве не про это?
Не в коем случае не топлю за ООП или ФП. Считаю их инструментами, которые нужно применять по назначению.
P.S. Статью нашел в ходе подготовки к собеседованиям. В одном из описаний вакансии увидел, что бонусом кандидату будет способность поддержать беседу насчет ООП VS ФП. Надеюсь, что уже слегка готов.
P.P.S. И да, Ваши статьи по теме в очереди на вдумчивое изучение.
не готов. ООП ортогонален ФП
НЛО прилетело и опубликовало эту надпись здесь

Меньше чем на последующие выяснения и исправления "а какого хрена у нас система на 100 пользователях проводит 99% времени в blocking GC". Пушто в профайлере мы увидим так называемую смерть от тысячи порезов, когда аллоцирует и притормаживает вообще всё.


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

НЛО прилетело и опубликовало эту надпись здесь
по Парето 80/20: только 20% кода требуют оптимизации, которые принесут 80% производительности.

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

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

Про чистый код и оптимизации — несколько неверно. Тайм критикал задачи почти всегда превращаются в лапшекод, либо в вызовы простейших методов, на которых висит AggressiveInlining.
В случае тайм критикал нужно максимально избегать аллокаций и дальних переходов.
И естественно минимальное количество async/await, LINQ и т.д.

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

В целом, соглашусь, если речь про стандартные учётные системы / CRM и т.д.

Тогда стоит брать подходящий язык. Например, у нас один из микросервисов на расте написан. Всё красиво, высокоуровнево. Инлайн атрибуты конечно висят, некоторые места подправлены в угоду производительности, но в целом всё же высокоуровнево и красиво. При том что у нас там мой коллега уже полгода лазит с интеловым 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;

И применяю везде, где надо.

вынесу его как функцию

Отлично. Где вы конкретно в коде вот эту строчку поместите и почему?

Я не понимаю вопроса. Будет у меня какой-нибудь UserExpressionHelper, в нем и напишу.

Так для справки, если не смотрели выступления Дяди Боба (SOLID это он) где он на пальцах все объяснял то я вам раскажу: Под изменениями он имел в виду переписывание кода а под причиной он имел в виду требования от бизнеса/ТЗ. По человечески это принцып звучит так — Надо писать код так чтобы если в друг поменяется ТЗ то переписывать код пришлось бы в как можно меньших местах. Для того чтобы соблюсти этот принцы надо ответственность за что-то концентрировать в какой-то библиотеке, классе, методе. Например библиотека логирования, интерфейс ILogger и класс его реализующий. В нем все ошибки через метод LoggError. Везде в своем коде для логгирования используете эту библиотеку (соблюдение принципа на уровне модуля) это interface (Соблюдение принципа на уровне класса/интерфейса) и для логгирования ошибок везде в своем коде используете метод LoggError (Соблюдение принципа на уровне метода). Теперь если прилетят требования что теперь все ошибки нужно сериализовать в XML и оправлять на сервер к партнерам вам нужно будет только поменять код в методе LogError. Просто вы такой задали вопрос что мне показалось что вы не понимаете суть этого принципа и зачем он вообще нужен.
мне показалось, что main «берет на себя слишком много», одновременно обьявляя сравнительно громозскую лямбду, и тут же непосредственно применяя эту лямбду к коллекции. Вам не кажется, что это однозначно нарушение SRP? Конечно, для решения задачки «такое» сгодится, т.к. поддерживать этот сниппет никому не придется. Далее, из комментария юзера под ником PsyHaSTe возникает вопрос, что делать, если лямбда выполняет достаточно простую операцию и вроде-бы можно ее и оставить. Но даже в этом случае, фильтр сам по себе — абсолютно статическая вещь по отношению к рантайму, почему бы не поступить с ним, как поступают, например, со строковыми константами, вынести т.е.?
  1. константы выносят обычно ради комментирования "что за константа". Например foo 1800 непонятно, а foo secondsInHour/2 уже лучше. Причем деление на два не выносится, потому что из контекста должно быть понятно, что к чему
  2. даже с константами их часто пишут инлайн.


    _logger.Log("Bad thing {badThing} happened at address {address}", badThing, address);

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


Например foo 1800 непонятно

«Обычно» понятно из контекста, что за константа:
timeoutInSeconds = 1800;
bufferSize = 1024;
Такие вещи, как:
foo = 1800
не видел уже довольно давно.

даже с константами их часто пишут инлайн

Не пишут значительно чаще.
Может конечно дело привычки, но код в первую очередь должен быть понятный и легко дабажить, если с первым в функциональном стиле всё более менее, то со вторым тут сложнее. Например сложный linq написанный зачастую в одну строчку не дебажиться нормально и надо на куски поделить чтоб понять в каком месте из набора функций всё отваливается.
На практике ни разу не сталкивался с подобной проблемой в C#. C# все же не является функциональным языком. Поэтому, некоторые частные задачи, требующие 1-10 строк кода легче записать в императивном стиле, а другие — в функциональном. Проблема с дебагом о которой вы говорите возникает если задачу, для которой по ее постановке и алгоритму просится императивный стиль запихнуть в функциональное выражение linq или др.
Например, если есть некий массив данных, по которому надо пройти, проверить много условий с привлечением множества данных и в этом же массиве что-то скорректировать по результатам, тогда это явно императивного стиля постановка задачи и нужен foreach и if-ы.
Если есть массив данных, по которому надо пройти с условиями и построить новый массив, то здесь linq сработает прекрасно и дебажить его не придется.
Для функционального стиля тут лишний вызов ToList. По хорошему без него надо писать.
Только в стандартный библиотеках 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) { ... }


Почему так? Две причины:

  1. этот код валидно работает со структурой такого вида:
    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);
    }

    а обычные проверки на нулл — нет.
  2. переменная `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, в реальном коде который не злоупотребляет булами код выглядит скорее как у меня по ссылке, нежели как в физзбаззе.

Но мы сейчас обсуждаем не абстрактный "реальный код", а код из физзбазза. В котором матч смотрится ужасно.

Нет, не предполагается. Задачка-то звучит максимально просто. «Если делится на 3, то fizz, если делится на 5, то buzz, если делится на 3 и на 5 — fizzbuzz». То что там и там 3 — это вам так повезло. Также предполагается, что ты не бросишься сломя голову писать, а начнешь уточнять. ТС этого не сделал, додумал сам условие и в итоге получил код, за который я бы поставил «плохо».
То же самое про оптимизаторов, которые начинают выводить не строчку «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()
            };

Можно, почему нет. Кстати, так ощутимо лучше.

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

Публикации

Истории