Сложность простоты


    Как я писал в предисловии предыдущей статьи, я нахожусь в поисках языка, в котором я мог бы писать поменьше, а безопасности иметь побольше. Моим основным языком программирования всегда был C#, поэтому я решил попробовать два языка, симметрично отличающиеся от него по шкале сложности, про которые до этого момента приходилось только слышать, а вот писать не довелось: Haskell и Go. Один язык стал известен высказыванием "Avoid success at all costs"*, другой же, по моему скромному мнению, является полной его противоположенностью. В итоге, хотелось понять, что же окажется лучше: умышленная простота или умышленная строгость?


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




    И сразу небольшая ремарка. Дело в том, что выражение (*) часто используют иронически, но это лишь потому, что люди его неверно парсят. Они читают это как "Avoid (success) (at all costs)", то есть "что бы ни произошло, если это ведет к успеху, мы должны это избежать", тогда как по-настоящему фраза читается как "Avoid (success at all costs)", то есть "если цена успеха слишком велика, то мы должны отступить на шаг и всё переосмыслить". Надеюсь, после этого объяснения она перестала быть смешной и обрела настоящий смысл: идеология языка требует правильно планировать свое приложение, и не вставлять adhoc костылей там, где они обойдутся слишком дорого. Идеология го, в свою очередь, скорее "код должен быть достаточно простым, чтобы в случае изменения требований его легко было выкинуть и написать новый".


    Методика сравнения


    Не мудрувствуя лукаво, я взял задачку, которую придумал товарищ 0xd34df00d и звучит она следующим образом:


    Допустим у нас есть дерево идентификаторов каких-либо сущностей, например, комментариев (в памяти, в любом виде). Выглядит оно так:


    |- 1
       |- 2
       |- 3
          |- 4
          |- 5

    Ещё у нас есть некое API которое по запросу /api/{id} возвращает JSON-представление этого комментария.


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


    По условиям задачи у нас нет API, которое сразу вернет итоговое дерево, только получение одного конкретного узла. Для простоты считаем, что никаких ддосов нет, что нам не нужно ограничивать параллельность, и т.п.


    В итоге вывод программы должен выглядеть примерно так:


    |- 1
       |- 2
       |- 3
          |- 4
          |- 5
    
    |- 1:Оригинальный комментарий
       |- 2:Ответ на комментарий 1
       |- 3:Ответ на комментарий 2
          |- 4:Ответ на ответ 1
          |- 5:Ответ на ответ 2

    В качестве тестового апи я использовал любезно предоставленный первой строчкой гугла сервис https://jsonplaceholder.typicode.com/todos/


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


    Отступление про Haskell



    Если вы знаете, зачем нужны точка-оператор, доллар-оператор и как работает do-нотация, то смело пропускайте раздел и преходите к следующему. Иначе очень рекомендую почитать, будет интересно. А ещё будут монады на C#



    Здесь что-то на эльфийском. Не могу прочитать


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


    Прежде чем начать статью, я хотел бы немного поговорить о структуре ML языков. Дело в том, что всем известно, что Lingua Franca низко- и среднеуровневых* языков это С. Если ты пишешь на джаве, а твой коллега на питоне, просто пошли ему сниппет на С, он поймет. Работает и в обратную сторону. Все знают си, и на чем бы они ни писали по работе, на нем они всегда договорятся.


    * под низкоуровневыми языками я имею ввиду языки С/С++/..., а под среднеуровневыми — C++/C#/Java/Kotlin/Swift/...


    Но менее известно, что в высокоуровневых языках это Haskell. В Scala/F#/Idris/Elm/Elixir/… тусовках если не знаешь, на чем пишет твой визави — пиши на хаскелле, не ошибешься. Однако программистов на этих языках не так много, и для более широкого охвата статьей я приведу небольшой разговорник, чтобы вариант на Haskell не казался китайской грамотой. Я буду приводить примеры на Rust/C#, они должны быть понятны любому человеку, знакомому с С. Термины Option/Maybe, Result/Either и Task/Promise/Future/IO означают одно и то же в разных языках и могут быть взаимозаменяемо использованы друг вместо друга.


    Итак, Если вы видите перед собой


    data Maybe a = Just a | Nothing  -- это комментарий

    То это означает


    // это тоже комментарий
    enum Maybe<T> {
       Just(T),
       Nothing
    }

    То есть просто энум, к одному из значений которых прицеплено дополнительное значение. Отличия в записи от Rust: генерик-аргументы в С-подобных языках принято выделять угловыми скобками и зачастую начинать с T. В хаскелле генерик-аргументы пишутся маленькими буквами через пробел. Одно это знание позволит вам расшифровывать тайные письмена хаскеллистов. Например, другой тип


    data Either a b = Left a | Right b

    мы теперь легко можем прочитать, и переписать знакомым нам образом


    enum Either<A,B> {
       Left(A),
       Right(B)
    }

    Довольно логично и последовательно. Стоит немного привыкнуть, и эта запись будет вам казаться вполне естественной (лично я переучился где-то за полчаса написания кода).


    Ну и конечно кроме тип-сумм есть и типы-произведения, это обычные структуры, которые пишутся так:


    data Comment = Comment {
          title :: String
        , id  :: Int
        } deriving (Show) -- просим компилятор автоматически генерировать функцию
                  -- преобразования в строку (аналог метода ToString() в C#/Java)

    и переводятся как:


    #[derive(Debug)]
    struct Comment {
        title: String,
        id: i32
    }

    Пока вроде все просто, идем дальше.


    Если же вы видите перед собой


    sqr :: Int -> Int
    sqr x = x*x
    
    main :: IO () -- IO это специальный тип, обозначающий взаимодействие с внешним миром, в частности вывод на консоль
    main = print (sqr 3)

    То это


    fn sqr(x: i32) -> i32 { x*x }
    fn main() {
       println!("{}", sqr(3));
    }

    Здесь мы объявляем две функции, одна — функция возведения в квадрат, а другая — вездесущий main.


    Одна особенность, которую мы сразу видим: в С-языках вызов функции обособляется скобками, в ML-подобных — пробелом. Но скобками все-равно приходится пользоваться из-за левой ассоциативности языка. Поэтому мы выделяем (sqr 3) в скобочки, чтобы сперва вычислилось это значение, а затем оно использовалось для вывода на экран. Без скобочек компилятор попробует сначала выполнить print sqr и конечно же выдаст ошибку компиляции, потому sqr имеет тип Fn(i32) -> i32 (Func<int, int> в терминах C#), для которого не определен метод show (местный ToString()).


    Другая особенность: объявление функции в хаскеле состоит из двух частей: первая (необязательная) — описание сигнатуры, и вторая — непосредственно тело функции. Из-за особенностей языка (в которые я сейчас не буду углубляться) все аргументы перечисляются стрелочкой ->, последнее значение справа это результат функции. Например, если вы видите функцию foo :: Int -> Double -> String -> Bool, то эта функция которая называется foo и принимающая три аргумента: один целочисленный, один с плавающей запятой и один строковый, и возвращащий булевское значение.


    Теперь попробуйте проверить себя, что за сигнатура у функции bar :: (Int -> Double) -> Int -> (String -> Bool)?


    Ответ

    Функция по имени bar принимает два аргумента: функцию Int -> Double и значение типа Int, и возвращает функцию String -> bool.


    Rust-сигнатура: fn bar(f: impl Fn(i32) -> f64, v: i32) -> impl Fn(String) -> bool


    C#-сигнатура: Func<string, bool> Bar(Func<int, double> f, int v)


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


    Теперь же, если вы видите


    sqr x = x*x     -- обратите внимание на опущенные сигнатуры, они будут выведены
    add x y = x + y -- Однако: FOR EXAMPLE PURPOSES ONLY!
    add5_long x = add 5 x
    add5 = add 5    -- как и в математике, иксы по обе части уравнения можно сократить, 
                    -- поэтому add5 это сокращенная запись варианта add5_long. 
                    -- Принцип схож с Method Groups в C#
                    -- Официальное название такого приема - каррирование
    
    main :: IO ()
    main = putStrLn (show (add 10 (add5 (sqr 3)))) 

    то это переводится как


    fn sqr(x: i32) -> i32 { x*x }
    fn add(x: i32, y: i32) -> i32 { x + y }
    fn add5(x: i32) -> i32 { add(5, x) }
    
    fn main() {
       println!("{}", ToString::to_string(add(10, add(5, sqr(3)))));                
    }

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


    main = putStrLn $ show $ add 10 $ add5 $ sqr 3 -- ура! нет скобочек

    С таким количество долларов в программах хаскеллистам была бы открыта дорога во все банки мира, но им это почему-то не понравилось. Поэтому они придумали писать точки. Оператор точка — это оператор композиции, и он определяется как f (g x) = (f . g) x. Например print (sqr 3) можно записать как (print . sqr) 3. Из функций "распечатай" и "возведи в квадрат" мы построили функцию "распечатай возведенный в квадрат аргумент", а потом передали ей значение 3. С его помощью пример выше будет выглядеть:


    main = putStrLn . show . add 10 . add5 $ sqr 3

    Стало намного чище, но заканчиваются ли на этом плюсы этого оператора? Как вы и догадались, ответ — нет, теперь благодаря этому мы можем вынести это все в отдельную функцию, придумать ей легкопроизносимое и очевидное имя и переиспользовать где-нибудь ещё:


    -- функция прибавляет к аргументу 5, затем прибавляет 10, затем преобразует в строчку, затем выводит на экран
    putStrLnShowAdd10Add5 = putStrLn . show . add 10 . add5
    -- аналогичная запись putStrLnShowAdd10Add5 x = putStrLn . show . add 10 . add5 x
    -- поэтому вычисление происходит справа налево (как, впрочем, и во всех языках)
    
    main :: IO ()
    main = putStrLnShowAdd10Add5 $ sqr 3

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


    Мы узнали про ML синтаксис практически всё, чтобы читать произвольный Haskell код, остался последний рывок и с разговорником покончено


    Последний рывок


    main :: IO ()
    main = 
      let maybe15 = do
          let just5 = Just 5    -- создаем объект типа Maybe с конструктором Just (см. первый пример) и значением 5
          let just10 = Just 10  -- то же самое с 10
          a <- just5            -- Пытаемся достать из него значение, если оно есть, то сохранить его в `a`.  если тут не будет значения то следующая строчка не выполнится!
          b <- just10           -- то же самое с `b`
          return $ a + b        -- если мы дошли до этой строчки, значит обе переменных содержали значения (были созданны через конструктор Just) и мы их сохранили в a и b.
      in 
        print maybe15

    Такая запись, область с выделением do-блока и использованием <- стрелочек, называется do-нотация, и она работает с любыми типами, являющимися монадой (не пугайтесь, это не страшно). На примере его использования с типом Maybe вы могли сразу узнать элвис-оператор (он же "Null condition operator"), позволяющий обрабатывать null-значения по цепочке, который возвращает null, если он не смог где-то получить значение. do-синтксис весьма-похож на него, но намного шире по возможностям.


    Подумайте, где вы могли такое видеть? Оператор, который позволяет вам "раскрыть" значение, лежащее в некоемом контейнере (в данном случае Maybe, но может быть и любой другой, например Result<T,Error>, или как его называют в ФП языках — Either), а если не получилось, то прервать выполнение? Предлагаю вам немного подумать, стрелочка <- может вам казаться странной, но на самом деле вы это наверняка писали тысячу раз в своем любимом языке.


    Ответ

    А ведь это ни что иное, как общий случай async/await (я использую синтаксис C# т.к. в Rust async-await ещё не стабилизирован):


    async ValueTask<int> Maybe15() {
        var just5 = Task.FromResult(5);
        var just10 = Task.FromResult(10);
        int a = await just5; // если тут будет ошибка то следующая строчка не выполнится!
        int b = await just10;
        return a + b;
    }
    
    Console.WriteLine(Maybe15().ToString()) // выведет ожидаемое 15

    Здесь я использую тип Task вместо Maybe, но даже по коду видно, как они похожи.
    В целом, можно воспринимать do-нотацию как расширение async/await (который работает только с асинхронными контейнерами навроде Task) на тип любых контейнеров, где do — это "начало async-блока", а <- — это "авейт" (раскрытие содержимого контейнера). Частные случаи включают в себя Future/Option/Result/ List, и многие другие.


    Магия C#

    На самом деле в C# есть полноценная do-нотация, а не только ограниченный async/await для Task. И имя ему, барабанная дробь, LINQ-синтаксис. Да, многие давно про него забыли, кто-то наоборот знает про этот маленький трюк, но для полноты картины рассказать о нем точно не помешает. Если мы напишем пару хитрых методов расширения для Nullable, то переписать код с Haskell в таком случае можно буквально один в один (не прибегая к аналогии с Task). Вспомним, как оно выглядело (немного упрощу):


    main :: IO ()
    main = 
      let maybe15 = do
          a <- Just 5
          b <- Just 10
          return $ a + b 
      in 
        print maybe15

    И теперь версия C#


    int? maybe15 = from a in new int?(5)
                   from b in new int?(10)
                   select a + b;
    
    Console.WriteLine(maybe15?.ToString() ?? "Nothing");

    Вы видите разницу? Я — нет, за исключением того что haskell умеет печатать Nothing при отсутствии значения, а в C# это приходится делать самостоятельно.


    Поиграться и посмотреть как же оно работает можно в заботливо подготовленном repl (для просмотра результатов программы прокрутите нижний див до конца). По ссылочке приложены также примеры работы с Result и Task. Как видите, работа с ними абсолютно идентична. Все контейнеры, с которыми можно работать подобным образом в ФП называются монадами (оказывается, это понятие не так уж страшно, правда?).


    Ну а тут можно посмотреть как то же самое выглядит на Haskell: https://repl.it/@Pzixel/ChiefLumpyDebugging


    Итак, вступление уже изрядно затянулось, предлагаю перейти непосредственно к коду


    Haskell



    С чем сталкивается каждый начинающий хаскеллист сразу после установки языка? Правильно, IDE ничего не подсказывает


    Примерно весь первый час после того как я решил начать с версии на хаскелле я настраивал окружение: устанавливал GHC (компилятор), Stack (сборщик и депенденси менеджер, похож на cargo или dotnet), IntelliJ-Haskell, и ждал установки всех зависимостей. Потом пришлось повозиться с идеей, но после очистки кешей и пары профилактических перезагрузок IDE все наладилось.


    Наконец все запущено, идея подсказывает имена и сигнатуры функций, генерирует сниппеты, в общем, все прекрасно, и мы готовы написать наш первый код:


    main :: IO ()
    main = putStrLn "Hello, World!"

    Ура, оно живое! Теперь начинаем с первого пункта, вывести дерево на экран. После непродолжительного гуглежа находим стандартный тип Data.Tree с прекрасным методом drawTree. Отлично, пишем, прям как написано в документации:


    import Data.Tree
    
    main :: IO ()
    main = do
        let tree = Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]
        putStrLn . drawTree $ tree -- в этот момент я пошел гуглить, что такое точка и доллар.
                                   -- результат моего расследования вы прочитали в предыдущей части

    И получаем нашу первую ошибку:


        • No instance for (Num String) arising from the literal ‘1’
        • In the first argument of ‘Node’, namely ‘1’
          In the expression:
            Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]

    Где-то секунд 30 я разглядывал её, потом подумал "при чем тут стринга?.. Хм… А, наверное он может вывести только дерево строк", гуглю "haskell map convert to string", и по первой ссылке нахожу решение использовать map show. Что ж проверяем: меняем последнюю строчку на putStrLn . drawTree . fmap show $ tree, компилируем, и радуемся нарисованному дереву


    Отлично, дерево мы рисовать научились, а как преобразовать его в дерево комменатриев?
    Гуглим, как объявить структуры, и пишем метод загрузки комментария по номеру. Раз я пока не знаю, как писать сетевое взаимодействие, мы напишем метод-заглушку который возвращает какой-то константный комментарий. Т.к. я уже имел какой-то опыт Rust я знал, что в современных языках все асинхронные операции по АПИ похожи на Option — опциональный тип, поэтому решил сделать возвращаемое значение метода-заглушки Maybe (местный Option), а потом, когда разберусь как делать HTTP запросы, заменю на нормальный асинк. А пока пусть возвращает вместо комментария число, преобразованное в строку.


    Дописываем объявление структуры и метод-заглушку:


    import Data.Tree
    
    data Comment = Comment {
          title :: String
        , id  :: Int
        } deriving (Show)
    
    getCommentById :: Int -> Maybe Comment
    getCommentById i = Just $ Comment (show i) i
    
    main :: IO ()
    main = do
        let tree = Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]
        putStrLn . drawTree . fmap show $ tree

    Все отлично, теперь нужно применить нашу функцию-заглушку для каждого узла. На этом моменте я загуглил "haskell map maybe list" (потому что на практике знаю, что мап списка он ничем не отличается от мапа дерева, а загуглить будет проще), и второй ссылкой нашел ответ "Просто используйте mapM". Пробуем:


    import Data.Tree
    import Data.Maybe
    
    data Comment = Comment {
          title :: String
        , id  :: Int
        } deriving (Show)
    
    getCommentById :: Int -> Maybe Comment
    getCommentById i = Just $ Comment (show i) i
    
    main :: IO ()
    main = do
        let tree = Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]
        putStrLn . drawTree . fmap show $ tree
        let commentsTree = mapM getCommentById tree
        putStrLn . drawTree . fmap show $ fromJust commentsTree

    Получаем:


    1
    |
    +- 2
    |
    `- 3
       |
       +- 4
       |
       `- 5
    Comment {title = "1", id = 1}
    |
    +- Comment {title = "2", id = 2}
    |
    `- Comment {title = "3", id = 3}
       |
       +- Comment {title = "4", id = 4}
       |
       `- Comment {title = "5", id = 5}

    Фух, вроде даже работает. Пришлось дополнительно добавить fromJust (аналогичен unwrap() в расте или Nullable.Value в C#, пытается развернуть значение, если там пусто, то бросает исключение), в остальном сделали все так, как написано по ссылке и получили вывод нашего дерева на экран.


    После этого я немного застопорился, потому что я не понял, как делать асинхронные запросы и парсить JSON'ы.
    К счастью, в чатике мне быстренько помогли и дали ссылки на wreq и местную либу для десериализации. Минут 15 я игрался с примерами после чего получил предварительно рабочий код:


    {-# LANGUAGE DeriveGeneric #-}
    
    import Data.Tree
    import Data.Maybe
    import Network.Wreq
    import GHC.Generics
    import Data.Aeson
    import Control.Lens
    
    data Comment = Comment {
          title :: String
        , id  :: Int
        } deriving (Generic, Show)
    
    instance FromJSON Comment -- `impl FromJson for Comment {}` в терминах Rust
    
    getCommentById :: Int -> IO Comment
    getCommentById i = do
      response <- get $ "https://jsonplaceholder.typicode.com/todos/" ++ show i
      let comment = decode (response ^. responseBody) :: Maybe Comment
      return $ fromJust comment
    
    main :: IO ()
    main = do
        let tree = Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]
        Prelude.putStrLn . drawTree . fmap show $ tree
        let commentsTree = mapM getCommentById tree
        Prelude.putStrLn . drawTree . fmap show $ fromJust commentsTree

    И… Сначала ждем 20 минут, пока скачаются и соберутся все зависимости (привет, сборка reqwest в Rust), а затем получаем нашу вторую ошибку:


        * Couldn't match expected type `Maybe (Tree a0)'
                      with actual type `IO (Tree Comment)'
        * In the first argument of `fromJust', namely `commentsTree'
          In the second argument of `($)', namely `fromJust commentsTree'
          In a stmt of a 'do' block:
            putStrLn . drawTree . fmap show $ fromJust commentsTree
       |
    28 |     Prelude.putStrLn . drawTree . fmap show $ fromJust commentsTree
       |                                                        ^^^^^^^^^^^^^

    Ну да, мы же использовали fromJust чтобы сделать преобразование Maybe Tree -> Tree, а теперь же у нас вместо заглушки настоящее IO происходит, которое и возвращает соответственно IO Tree вместо Maybe Tree. Как же достать значение? Как и прежде, обращаемся в гугл за этой информацией и получаем "используйте оператор <-" первой ссылкой. Пробуем:


    main :: IO ()
    main = do
        let tree = Node 1 [Node 2 [], Node 3 [Node 4 [], Node 5 []]]
        Prelude.putStrLn . drawTree . fmap show $ tree
        commentsTree <- mapM getCommentById tree
        Prelude.putStrLn . drawTree . fmap show $ commentsTree

    Ура, работает. Только медленно. Ах да, мы же забыли запараллелить.


    Следующие минут 20 я гуглил, как распараллелить обход дерева. Находил всякие странные Concurrent-пакеты, какие-то стратегии обхода, ещё что-то. Но ищущий да обрящет, и в конце концов я наткнулся на async. В итоге параллельная версия потребовала некоторых монументальных изменений, но в конце концов все-таки заработала:


    commentsTree <- mapConcurrently getCommentById tree

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


    Примечание: последние несколько ссылкок в песочнице не собираются т.к. они требуют библиотек, создающих HTTP соединений, а repl.it их не разрешает. Желающие могут скачать и скомпилировать пример локально


    На этом мой эксперимент с написанием на хаскелле завершается. Путем нехитрого гугла и интуиции от опыта работы с C# и Rust получилось меньше чем за час написать рабочую программу. Из них почти половину времени заняла просто установка 67 зависимостей веб-клиента. В принципе я был готов к этому, у reqwest в расте больше 100 зависимостей если мне не изменяет память, но все равно немного неприятно. Хорошо, что при последующей разработке все эти пакеты уже закэшированны локально и это был разовый оверхед на разворачивание окружения.


    Простота параллелизации меня очень приятно удивила. А также я внезапно обнаружил, что я совершенно не использую тот факт, что у меня дерево. Ради эксперимента я решил поменять дерево на массив, и вот какие изменения мне пришлось внести:


    main = do
        let tree = [1,2,3,4,5]
        print tree
        commentsTree <- mapConcurrently getCommentById tree
        print commentsTree

    выводит


    [1,2,3,4,5]
    [Comment {title = "delectus aut autem", id = 1},Comment {title = "quis ut nam facilis et officia qui", id = 2},Comment {title = "fugiat veniam minus", id = 3},Comment {title = "et porro tempora", id = 4},Comment {title = "laboriosam mollitia et enim quasi adipisci quia provident illum", id = 5}]

    То есть поменялась строчка с первоначальной инициализацией коллекции, и вывод её на экран. Если нам не нужно выводить результат на экран, то для замены дерева на массив не нужно менять вообще ничего, кроме собственно замены дерева на массив. Это, конечно, открывает большие просторы для гибкости решения в условиях меняющихся требований (то есть, любых реальных требований), и я, прямо скажу, не думал, что это настолько просто. Кроме того, компилятор на удивление почти никак себя не проявлял, за все время работы выдал только две ошибки со вполне очевидными описаниями.


    Ну, хаскель оставил приятные впечатления, давайте перейдем к следующей части, go. Его синтаксис больше похож на привычные мне языки, поэтому мне не придется тратить время на параллельное изучение синтаксиса (как видите, для go не понадобилось делать словарика) и я смогу сразу написать код. Я морально смирился, что мне придется писать более топорно (например, придется реализовать два разных типа для деревьев идентификаторов и деревьев комментариев), зато я смогу воспользоваться всей мощью главной рекламной фичи го — горутинами!


    Go



    Сейчас го покажет, как нужно писать асинхронные программмы


    Так как мы уже немного набили руку с предыдущим вариантом и знаем, что хотим получить в итоге, то просто открываем https://play.golang.org/ и пишем код.


    Сначала гуглим, как в го создаются структуры. Затем, как их инициализировать. Через минуту первая программа на go готова:


    package main
    
    type intTree struct {
        id int
        children []intTree
    }
    
    func main() {
        tree := intTree{ // это мне так gofmt отформатировал
            id: 1,
            children: []intTree {
                {
                    id: 2,
                    children: []intTree{
    
                    },
                },
                {
                    id: 3,
                    children: []intTree{
                        {
                            id: 4,
                        },
                        {
                            id: 5,
                        },
                    },
                },
            },
        }
    }

    Пытаемся скомпилировать — ошибка, tree declared and not used. Окей, в принципе я заранее знал, что го не разрешает иметь неиспользуемые переменные, переименовываем tree в _. Пробуем собрать, получаем ошибку no new variables on left side of :=. Ну, видимо нам даже проверить что мы не ошиблись в коде создания дерева не дадут, придется сразу дописывать форматирование и вывод на экран. Тратим ещё пару минут на то, чтобы узнать, как выводить форматирующую строку на экран и как сделать foreach цикл и дописываем необходимые функции:


    func showIntTree(tree intTree) {
        showIntTreeInternal(tree, "")
    }
    
    func showIntTreeInternal(tree intTree, indent string) {
        fmt.Printf("%v%v\n", indent, tree.id)
        for _, child := range tree.children {
            showIntTreeInternal(child, indent + "  ")
        }
    }

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


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


    type comment struct {
        id int
        title string
    }
    
    type commentTree struct {
        value comment
        children []commentTree
    }
    
    func loadComments(node intTree) commentTree {
        result := commentTree{}
        for _, c := range node.children {
            result.children = append(result.children, loadComments(c))
        }
        result.value = getCommentById(node.id)
        return result
    }
    
    func getCommentById(id int) comment { 
        return comment{id:id, title:"Hello"} // наша заглушка в случае go
    }

    ну и конечно же дописать пару строчек кода для вывода дерева комментариев:


    func showCommentsTree(tree commentTree) {
        showCommentsTreeInternal(tree, "")
    }
    
    func showCommentsTreeInternal(tree commentTree, indent string) {
        fmt.Printf("%v%v - %v\n", indent, tree.value.id, tree.value.title)
        for _, child := range tree.children {
            showCommentsTreeInternal(child, indent + "  ")
        }
    }

    С первой задачей мы почти справились, осталось только научиться получать реальные данные от веб-сервиса, и заменить заглушку на получение данных. Гуглим, как делать http запросы, гуглим, как десериализовывать JSON, и спустя ещё 5 минут дописываем следующее:


    func getCommentById(i int) comment {
        result := &comment{}
        err := getJson("https://jsonplaceholder.typicode.com/todos/"+strconv.Itoa(i), result)
        if err != nil {
            panic(err) // для игрушечной задачи сойдет
        }
        return *result
    }
    
    func getJson(url string, target interface{}) error {
        var myClient = &http.Client{Timeout: 10 * time.Second}
        r, err := myClient.Get(url)
        if err != nil {
            return err
        }
        defer r.Body.Close()
    
        return json.NewDecoder(r.Body).Decode(target)
    }

    Запускаем, получаем


    1
      2
      3
        4
        5
    0 - 
      0 - 
      0 - 
        0 - 
        0 - 

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


    Минут через 5 дебага и посматривания в документацию, стало понятно, что проблема в регистрозависимости десериализатора: распарсить {Title = "delectus aut autem", Id = 1} как cтруктуру id, title го не может. Заодно находим правила именования, что с маленькой буквы пишутся приватные члены, а с большой — публичные. В принципе, решений потенциальных два: первое — просто сделать поля публичными с большой буквы, второе — повесить специальные атрибуты чтобы указать имена, из которых нужно парсить.


    Ну так как у нас простая DTO, поэтому просто делаем поля публичными, запускаем, все работает.


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


    Тратим ещё минут 5, узнаем про вейтгруппы и go-нотацию. Пишем


    func loadComments(root intTree) commentTree {
        var wg sync.WaitGroup
        result := loadCommentsInner(&wg, root)
        wg.Wait()
        return result
    }
    
    func loadCommentsInner(wg *sync.WaitGroup, node intTree) commentTree {
        result := commentTree{}
        wg.Add(1)
        for _, c := range node.children {
            result.children = append(result.children, loadCommentsInner(wg, c))
        }
        go func() {
            result.value = getCommentById(node.id)
            wg.Done()
        }()
        return result
    }

    И снова получаем


    0 - 
      0 - 
      0 - 
        0 - 
        0 - 

    Эмм, ну а теперь-то почему? Начинаем разбираться, ставим брейкпоинт на начало функции, проходим её. Становится понятно, что мы выходим из функции не дожидаясь результата, поэтому когда wg.Wait() на верхнем уровне дожидается сигнала от всех горутин, у него уже на руках есть сформированное пустое дерево, которое он и возвращает.


    Окей, значит нам нужен какой-то способ вернуть значение после того, как функция вернулась. Опять обращаемся к поисковику, узнаем про каналы, учимся с ними работать, и ещё минут через 10 у нас готов следующий код:


    func loadComments(root intTree) commentTree {
        ch := make(chan commentTree, 1)   // создаем канал
        var wg sync.WaitGroup             // создаем вейт группу
        wg.Add(1)                         // у неё будет один потомок
        loadCommentsInner(&wg, ch, root)  // грузим дерево
        wg.Wait()                         // дожидаемся результата
        result := <- ch                   // получаем значение из канала
        return result
    }
    
    func loadCommentsInner(wg *sync.WaitGroup, channel chan commentTree, node intTree) {
        ch := make(chan commentTree, len(node.children))  // создаем канал по количеству детей
        var childWg sync.WaitGroup                        // создаем вейт группу для детей
        childWg.Add(len(node.children))
        for _, c := range node.children {
            go loadCommentsInner(&childWg, ch, c)         // рекурсивно грузим детей в горутинах (параллельно)
        }
        result := commentTree{
            value: getCommentById(node.id),               // синхронно грузим себя
        }
        if len(node.children) > 0 {                       // если у нас есть дети, которых надо дождаться, то ждем их
            childWg.Wait()
            for value := range ch {                       // все дети сигнализировали об окончании работы, забираем у них результаты
                result.children = append(result.children, value)
            }
        }
        channel <- result                                 // отдаем результат в канал наверх
        wg.Done()                                         // сигнализируем родителю об окончании работы
    }

    Запускаем и… тишина. Ничего не происходит. Честно говоря, в этот момент я ощутил некоторое замешательство. Я слышал, что в го есть детектор дедлоков, раз он молчит, значит мы не залочились. То есть какая-то работа выполняется. Но какая?


    Ещё минут 15 я расставлял ифчики, перестраивал код, добавлял/удалял вейтгруппы, тасовал каналы… Пока наконец не догадался заменить получение по HTTP на нашу изначальную заглушку:


    func getCommentById(id int) comment {
        return comment{Id: id, Title: "Hello"}
    }

    После чего го выдал:


    1
      2
      3
        4
        5
    fatal error: all Goroutines are asleep - deadlock!
    
    goroutine 1 [semacquire]:
    sync.runtime_Semacquire(0xc00006e228)
        C:/go/src/runtime/sema.go:56 +0x49
    sync.(*WaitGroup).Wait(0xc00006e220)
        C:/go/src/sync/waitgroup.go:130 +0x6b
    main.loadCommentsInner(0xc00006e210, 0xc0000ce120, 0x1, 0xc000095f10, 0x2, 0x2)
        C:/Users/pzixe/go/src/hello/hello.go:47 +0x187
    main.loadComments(0x1, 0xc000095f10, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
        C:/Users/pzixe/go/src/hello/hello.go:30 +0xec
    main.main()
        C:/Users/pzixe/go/src/hello/hello.go:94 +0x14d

    Ага, то есть все-таки дедлок происходит. Непонятно, почему ты раньше-то этого не сказал? Ведь получение данных по HTTP по идее не должно никак на тебя влиять, синхронная версия же работала как надо...


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


    1. Строить ноды рекурсивно в узлах это плохо. Нужно создать всё дерево заранее, а потом дать ссылки на ноды каждой горутине, чтобы она в это общее для всех горутин место по нужному адресу перезаписало пустую структуру comment на полученную из JSON
    2. Вызывать горутины рекурсивно тоже плохо. По опыту сишарпа я привык, что стартовать Task внутри других Task и аггрегация через WhenAny/WhenAll это совершенно нормальная операция. В го, судя по той информации, что мне сказали, это не так. Как я понял, там и планировщику плохо становится, и с производительностью наступает кирдык. То есть правильный сценарий использования — исключительно в роли веб-сервера а-ля
      for httpRequest := range webserer.listen() {
          go handle(httpRequest)
      } 
    3. На практике никто не пишет по функции на каждый чих и go-way будет написать одну функцию printTree:


      func printTree(tree interface{}) string {
          b, err := json.MarshalIndent(tree, "", "  ")
          if err != nil {
              panic(err)
          }
          return string(b)
      }

      Где interface {} — это аналог шарпового dynamic или тайпскриптового any, то есть локальное отключение всех проверок типов. Дальше с таким объектом надо работать либо через рефлексию, либо через даункаст к известному типу.


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



    После этого я ещё довольно долго сидел, и пытался самостоятельно разобраться с задачей: я мог бы починить дедлок, но смысл, если полученный код не будет идиоматичным? В конце концов один из людей в чате сжалился и поделился рабочим вариантом с параллельной загрузкой нод. Вот его вариант:


    func loadComments(root intTree) commentTree {
        result := commentTree{}
        var wg sync.WaitGroup
        loadCommentsInner(&result, root, &wg)
        wg.Wait()
        return result
    }
    
    func loadCommentsInner(resNode *commentTree, node intTree, wg *sync.WaitGroup) {
        wg.Add(1)
        for _, res := range node.children {
            resNode.children = append(resNode.children, &commentTree{})
            loadCommentsInner(resNode.children[len(resNode.children)-1], res, wg)
        }
        resNode.value = getCommentById(node.id)
        wg.Done()
    }

    Что тут происходит? Ну, тут учтено первое замечание из списка рекомендаций "go-way": мы изначально создаем пустое дерево, а потом начинаем заполнять его из разных горутин. У нас нет кучи вейтгруп на каждый узел дерева, есть одна единственная, куда каждая нода себя добавляет, и которую мы наверху ждем.


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


    Ответ

    Несмотря на то, что тут есть вейтгруппы и вот это все, код полностью синхронный.


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


    func loadComments(root intTree) commentTree {
        result := commentTree{}
        var wg sync.WaitGroup
        loadCommentsInner(&result, root, &wg)
        wg.Wait()
        return result
    }
    
    func loadCommentsInner(resNode *commentTree, node intTree, wg *sync.WaitGroup) {
        wg.Add(len(node.children))
        for _, res := range node.children {
            child := &commentTree{}
            resNode.children = append(resNode.children, child)
            res := res
            go func() {
                defer wg.Done()
                loadCommentsInner(child, res, wg)
            }()
        }
        resNode.value = getCommentById(node.id)
    }

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


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


    1
      2
      3
        4
        5
    START 1
    START 3
    START 5
    DONE 5
    START 2
    DONE 2
    START 4
    DONE 4

    Видно, что хотя 4 и 5 завершились, 3 не завершается. Стало понятно, что канал не закрывается, и ноды ожидают получения сообщений от детей, которые уже завершились и никогда в этот канал ничего не напишут. Оказывается, Rust меня настолько развратил, что я уже мысленно положился на концепцию владения, где канал был бы разрушен автоматически после того, как последний ребенок в него записал и дропнул свою ссылку.


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


    if len(node.children) > 0 {
        childWg.Wait()
        for i := 0; i < len(node.children); i++ { // выходим когда получили ожидаемое количество сообщений
            result.children = append(result.children, <-ch)
        }
    }
    channel <- result
    wg.Done()

    Что ж, пришло время подводить итоги? А вот и нет, мне в одном из чатов человек, хорошо шарящий в го сказал, что это всё ещё неидиоматичный код, и спустя полчасика скинул мне другой вариант. Полностью он доступен по ссылке, а тут я выделю интересные детали:


    1. func (n *IdTree) LoadComments(ctx context.Context) (*CommentTree, error) — вместо свободной функции используется функция на объекте tree
    2. g, ctx := errgroup.WithContext(ctx) — никаких ручных вейтгруп
    3. i, c := i, c // correct capturing by closure — судя по всему, замыкания работают по ссылке, а не значению. Нужно помнить об этом при написании лямбд в цикле
    4. g.Go(func() error — Никаких ручных горутин, всем рулит некий контекст

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


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


    Теперь давайте оценим, насколько легко изменить дерево на массив? Ну, тут код наверное проще будет выкинуть и заново переписать, потому что все функции знают, что работают с деревом. Оставить можно без изменений только getCommentById.


    С этими мыслями давайте двигаться дальше.


    C♯


    C# мой основной инструмент, которым я пользуюсь последние 6 лет, и конечно же я не мог обойти его стороной. У меня не будет таких подробных шагов как в прошлых пунктах, потому что я знал, что пишу, и минут за 8 с перерывами на общение в телеграме набросал такое решение:


    class Program
    {
        class Comment
        {
            public int Id { get; set; }
            public string Title { get; set; }
    
            public override string ToString() => $"{Id} - {Title}";
        }
    
        private static readonly HttpClient HttpClient = new HttpClient();
    
        private static Task<Comment> GetCommentById(int id) =>
            HttpClient.GetStringAsync($"https://jsonplaceholder.typicode.com/todos/{id}")
                .ContinueWith(n => JsonConvert.DeserializeObject<Comment>(n.Result));
    
        private static async Task<Tree<Comment>> GetCommentsTree(Tree<int> tree)
        {
            var children = Task.WhenAll(tree.Children.Select(GetCommentsTree));
            var value = await GetCommentById(tree.Value);
            var childrenResults = await children;
            return new Tree<Comment> { Value = value, Children = childrenResults };
        }
    
        private static async Task Main()
        {
            var tree = Tr(1, new[] { Tr(2), Tr(3, new[] { Tr(4), Tr(5) }) });
            PrintTree(tree);
            var comment_tree = await GetCommentsTree(tree);
            PrintTree(comment_tree);
        }
    
        class Tree<T>
        {
            public T Value { get; set; }
            public Tree<T>[] Children { get; set; }
        }
    
        private static void PrintTree<T>(Tree<T> tree, string intendantion = "")
        {
            Console.WriteLine(intendantion + tree.Value);
            foreach (var child in tree.Children)
            {
                PrintTree(child, intendantion + "  ");
            }
        }
    
        static Tree<T> Tr<T>(T value, Tree<T>[] children = null) => new Tree<T> { Value = value, Children = children ?? Array.Empty<Tree<T>>() };
    }

    Тут сразу я написал параллельную версию, как будет выглядеть синхронная? Ну, точно так же, только вместо Task.WhenAll нужно будет написать foreach и ждать каждого ребенка отдельно. То есть в плане параллелизации гибкость высокая.
    Что с заменой дерева на список? Ну, тут как и в случае с го придется выкидывать весь код и писать заново. Оставить можно тоже как и в его случае только GetCommentById. От этого можно абстрагироваться, реализовав AsyncEnumerable перечисление, тогда мы отвяжемся от конкретного представления нашей структуры данных, но это уже получится неоправданное для такой простой задачи усложнение.


    Выводы



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


    Haskell go C#
    Количество строк общего кода* 17 76 28
    Общего времени на разработку -
    Чистого времени на кодирование однопоточного варианта 30м 10м -
    Чистого времени на кодирование многопоточного варианта 15м 50м -
    Простота замены структуры данных
    Простота замены однопоточного варианта на многопоточный
    Простота условного выполнения операций отображения** ✓✗
    Время чистой сборки ~30м
    Время инкрементальной компиляции 5c 1.9c*** 1.6c

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


    ** Допустим, мы хотим выкидывать всех детей у которых сумма айдишек равна 42. В сишарпе, полагаю, это будет выглядеть примерно так:


    async Task<Tree<Comment>> GetCommentsTree(Tree<int> tree)
    {
        if (tree.Children.Sum(c => c.Value) == 42)
        {
            return new Tree<Comment> { Value = await GetCommentById(tree.Value), Children = Array.Empty<Tree<int>> };
        }
        ... дальше то же самое что и в прошлом варианте

    В го это будет решаться схожим способом.


    А вот в хаскелле скорее всего придется предварительно пофильтровать дерево, выкинув ненужные ноды, и только потом передавать в mapParallel. Звучит не особо сложно, но не так просто, как в случае go или C#.


    *** С го произошла странная фигня, что первые два билда собирались 7 секунд, последующие два 1.9 секунд, а дальше упало до 200мс. Я честно не очень понял, какие значения сюда писать и взял те что были в середине.




    Давайте вернемся к изначальным вопросам, поднятым в заголовке:


    Насколько идиоматичным получается "новичковый" код

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


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


    Какая кривая обучения для разработчика с опытом

    Haskell: если нет никакого опыта других языков, кривая довольно крутая, иначе почти со всеми вещами можно найти общее. По моему рассказу можно видеть, как я то и дело говорю "это похоже на сишарп", "это я знаю из Rust", и так далее. То есть либо нужна интуиция и опыт разработчика, либо нужно читать специализированные книжки по ФП и гуглить чуть менее успешно. С другой стороны, компилятор как и в расте очень помогает. У программы есть два состояния: не компилирующаяся и правильно работающая. Возможно, этот закон ломается для больших и сложных программ, но в моем примере получилось именно так.


    Go: кривая обучения начинается как довольно пологая. Первоначальный вариант мы там минут за 10 вроде написали. Но вот потом начинаются чудеса. Программа компилируется, но в рантайме просиходит совсем не то, что мы хотели. К кому обращаться с проблемой, непонятно, гугл выдает однотипные истории про то, как чудесно передавать в канал значения и как оттуда читать, а почему у нас все развалилось — неизвестно. Можно спросить в чатиках, но про них ниже. Мне не удалось найти каких-то библиотек, которые могли бы помочь с этой сложностью, насколько я понял, предлагается каждый раз решать их самостоятельно. Поправьте меня, пожалуйста, в комментариях, если я ошибаюсь. Таким образом после пологого старта я лично наблюдаю резкий рывок вверх, который превосходит сложность большинства остальных языков. Где кривая выходит на плато, не берусь судить, в рамках задачи мне его достигнуть не удалось.


    Cколько в итоге придется заплатить за ублажание хаскеллевского компилятора

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


    Cколько времени сэкономит знаменитое удобство горутин

    Удобство имеется, но я не вижу каких-то принципиальных отличий от любого другого языка, включая раст. В том же сишарпе я могу написать Task.Run(() => DoStuffAsyncInAnotherThread()) и оно запустит гринтред, где будет что-то там вычислять. Чуть больше букв, чем go DoAsyncStuff(), но зато у них нет ограничений вроде того, что плохо вызывать горутины в контексте выполнения других горутин (у нас из-за рекурсии так получалось), их легко композировать с помощью Task.WhenAny/Task.WhenAll (в го придется руками через каналы это изображать), ну и так далее.


    С другой стороны встроенный детектор дедлоков мне понравился. Без него я бы наверное ещё полчаса просидел, пытаясь понять, почему на экране пусто. Очень удобный инструмент. Хотя, как мы помним по варианту кода с черным экраном, и он работает не всегда.


    Если я где-то ошибаюсь, опять же, пожалуйста, поправьте меня в комментариях.


    Субъективная часть


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


    В случае хаскелля у меня было понимание, что делать дальше в каждый момент времени. Состояние программы разбивается на две части: либо она компилируется, тогда ничего делать не надо, либо нет, тогда нужно исправить ошибку. Например, как в наших случаях, когда для вывода на экран у нас должно быть дерево строк, а у нас на руках дерево чисел, и тогда вопрос звучит "как преобразовать одно в другое?", или когда мы замениил Maybe на IO и гуглили "На что заменить fromJust чтобы вытаскивать не из Maybe, а из IO?".


    В случае го изначально все было элементарно, т.к. он сам состоит из элементарных блоков, но то что код скомпилировался ничего ещё не означает. Мне не удалось нагуглить библиотек которые взяли бы на себя сложность с ожиданями, блокировками, распараллеливанием и всем остальным, из-за чего пришлось писать всё это самому. И ловить дедлоки. А когда у тебя дедлок, ты не можешь просто спросить гугл "что мне сделать чтобы его починить", потому что каждый случай индивидуален. Нужно либо идти в чатик и просить разобрать твой код, либо сидеть расставлять принты. Любопытный факт, что брейкпоинт на функции загрузки комментариев у меня срабатывал только один раз за работу приложения, хотя очевидно туда поток выполнения заходит для каждого узла дерева. С чем это связано не берусь судить, но если бы это работало иначе, скорее всего я бы сразу догадался в чем проблема.




    Программа на хаскелле требует больше знаний, более абстрактного подхода к проектированию задачи, но оно вознаграждает за это тем, что скорее всего за тебя уже кто-то написал библиотеку, которая снимает головную боль с тебя. Нужен многопоток? Бери вон то. Нужен JSON? Бери вот это. Скомпонуй через точку или доллар и все будет хорошо. Любопытная особенность в том, что в хаскелле я не понимаю отдельных моментов (например, я до сих пор не знаю что делает оператор ^.), но зато я понимаю, как оно работает на верхнем уровне (каким-то образом достает из HTTP ответа тело), поэтому я могу его использовать.


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




    Наконец, коммьюнити.


    В хаскель мне вежливо объяснили как настроить IDE/окружение/etc, ну и целом просто помогали/отвечали на все вопросы.


    А вот в го чате произошла совершенно изумительная история.


    Во-первых у меня сложилось ощущение, что непосредственно го-разработчиков в чате едва ли треть, а остальное — замаскированные тролли из других языков. Совершенно нормальная ситуация, когда вам в чате го посоветуют не писать на го, а взять шарпы/питон/… Во флудилках других языков это встречается повсеместно, но в основном чате коммьюнити...


    Во-вторых когда я зашел и попросил накидать пример, как мне написать такую программу на go 3 человека посветовали мне не заниматься ерундой, а два других посоветовали нанять для этого фрилансера. Я-то наивно полагал, что раз я за 8 минут на своем языке написал, то с горутинами и всем остальным на го это займет минут 5, и кого-нибудь из многотысячного чата не затруднит потратить их если попросить. В итоге все обошлось парой сердобольных людей, которые по некоторым моим вопросам все же помогали, включая людей, попытавшихся решить проблему с дедлоками. Им большая благодарность.


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


    — в go нет ни стека, ни сортеда листа, все это заменяется как слайс

    — удобно *sarcasm*

    Sorry, this group is no longer accessible

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


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


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


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


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


    Заключение


    Чем можно все это дело подытожить? Для своего бекграунда и задач я не нашел, что мне может предоставить го. Дедлок чекер из коробки очень крут. Компилится все и правда за миллисекунды, никаких промежуточных артефактов, после сишарпа с миллионом obj/bin файликов это кажется очень странным. Но на этом такое ощущение, что преимущества заканчиваются. Мне так показалось, что плюсами Go восхищаются по большей части люди со скриптовых языков, и для которых это очень серьезные преимущества, но для энтерпрайз-разработки это всё довольно слабо же. Люди в чате языка правда не знают, что есть способы автоматической генерации запросов с тайпчеком, или автоматической генерацией схемы и миграцией из моделей кода. В качестве супер-крутого решения в чате предлагали использовать gorm, но как разработчику знакомому с EF/Linq2Db/Hibernate/Dapper мне просто больно смотреть на его API.


    Я ни в коем случае не хочу никого задеть, просто мне кажется, что люди вообще не подозревают что такие штуки уже существуют, и что ими можно пользоваться. Ведь всё это плоды "сложности" языков, которая создается не для того, чтобы усложнить всем жизнь, а наоборот, упростить. Чтобы не надо было писать SQL руками, а он генерировался. Чтобы выражать программу не императивно, а декларативно. Чтобы параллелизация заключалась в изменении одной строчки, а не переосмыслении всей программы. Да, мой пример был простой, и в сложном случае придется думать и в том, и в другом случае, но в "сложном" языке всегда есть вероятность, что кто-то решил проблему за вас. С другой стороны вы в свою очередь всегда можете написать крутое решение, которым будут пользоваться миллионы, и которое может стать настолько крутым, что заменит стандартное (из жизни раста примитивы синхронизации, а ещё хэшмап). В go вы никогда не сможете соревноваться со стандартными Map, потому что у вас нет возможности конкурировать с разработчиками стандартной библиотеки: то что можно делать им, простым смертным — не получится.


    И хотя люди любят вставлять цитаты Эйнштейна по делу и нет, мне кажется сейчас подходящий случай для одной из них:



    Го — очень простой язык, но почему же на нем так сложно писать?..

    Only registered users can participate in poll. Log in, please.

    Какой язык показал себя лучше?

    Был ли вам полезен мини-справочник по хаскеллю?

    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 249

      +3
      Если говорить о функциональных языках, то, конечно, грех не вспомнить про Scala. В скале, на мой взгляд, в некотором смысле проще ориентироваться, чем в хаскеле — там удобство «функциональщины» сочетается с знакомым для «пришедших из мира ООП» синтаксисом.
        +3

        Знакомые скаллисты говорят, что с помощью cats получается почти 1в1 как в хаскель варианте. Теперь понятно, почему в Тинькове именно скалу используют, однако.

          +3
          Собственно, вот пример решения задачи на скале

          Выглядит чуть грязнее, чем на хаскеле, но суть похожа
            +1

            Любопытно, около 10 строк без учета вывода дерева на экран. С другой стороны код выглядит достаточно сложным, новичок без подготовки за час-два такое не напишет.

          +11

          Хорошая статья. Я считаю, сложность программирования на Хаскеле сильно преувеличена, как и простота программирования на Го. Из-за императивной сущности языка, отсутствия иммутабельности и хорошей типизации граблей там предостаточно. Так, чтобы эффективно использовать Го, необходимо изучить мануал Effective Go, и понимать модель вычислений в Go, например
          Understanding real-world concurrency bugs in Go.

            +4

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

            +21
            На самом деле, ситуация страшная. Го, вроде как, позиционируется как простой и быстрый в освоении язык, но, когда человек с опытом двух огромных (Rust, C#) языков не может справиться с простой задачей на 20 минут и сталкивается с проблемами чуть ли не с первых строк, к чему придет новичок, для кого Го станет первым языком? Какое качество продукта в результате выйдет?
            Да и зачем тогда нужен Го, если со своим назначением (простота и легкость обучения) он справиться не может, а другие языки решают те же задачи лучше и с меньшим количеством проблем?
              +5
              Слишком много обобщений на слишком маленькой базе для экстраполяции.
                +5
                Простота тяжела, лёгкость сложна. Всё это было у Рича Хикки (Simple made easy).
                0

                В изначальном варианте в Go подозрительно выглядит строка


                resNode.children = append(resNode.children, &commentTree{})

                Дело в том что в Go slice внутри это обычный массив с capacity. Соответственно если capacity превышено то создается новый массив с увеличенным capacity, в него переписываются старые значения и добавляются новые. Если это делается в одном потоке то все норм, но в многопоточном варианте будут проблемы. Все таки в go идеоматическим вариантом было бы передача значений по каналам и аккумулирование детей в родительском потоке отвечающем за текущий набор детей.
                Здесь самый на мой взгляд интересный вопрос в плане производительности. Скажем если сгенерить большое дерево (ну скажем пусть будет 1 млн узлов) и выбрать из него процентов 5 данных что получится по скорости и потреблению памяти? Причем если в плане haskel в целом замеров в сети много то вот сравнение go vs C# было бы интересным ИМХО

                  +1
                  Скажем если сгенерить большое дерево (ну скажем пусть будет 1 млн узлов) и выбрать из него процентов 5 данных что получится по скорости и потреблению памяти?

                  В случае хаскеля, кстати, вполне может оказаться O(1) по памяти (но, возможно, придётся пожертвовать конкурентностью запросов, совмещение ленивости и параллельной обработки потребует нетривиальной акробатики).

                  0

                  Вообще говоря, правильное решение этой задачи — хранить дерево в нормализованной форме (Dictionary( NodeId , NodeData )). Это мало того, что упрощает задачу "получить список идентификаторов всех узлов", так ещё и не вызывает экспоненциального роста потребления всех ресурсов, когда условия меняются так, что в дереве вдруг появляются циклы.

                    +1

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


                    В случае с Dictionary нам придется по нему руками (потенциально долго) ходить и удалять все NodeId того поддерева, которое мы хотим удалить.

                      0

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


                      Всё как всегда: без внятного ТЗ — результат ХЗ :)

                        +2

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


                        Что касается вопроса с "ТЗ", то у меня опыт такой, что иногда бывает задача её не доехала до мастера, а тестируется на регрессе в девелопе, а тебе уже приходят задачи на её модификацию и доработки.

                          +1

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

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

                        Ага, конечно, а кто удалит остальные ссылки на удалённые узлы? А delete на сервер кто по каждому узлу пошлёт?

                          0

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


                          Можно конечно вместо этого завести еще одно булево поле display и выводить только те у кого true, но это звучит костыльно.

                            0

                            А если хотим?


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

                              +3

                              Тогда придется написать код для осуществления удаления.

                      0
                      Почитал бы про сравнения с другими ЯПами. Мне вот наиболее простыми и лаконичными кажутся F# и TypeScript, но это из тех, у которых для меня минимально достаточно возможностей
                        –1
                        >> И в го получается диаметрально противоположенная с хаскеллем ситуация: я точно знаю что происходит на каждой строчке программы, но не понимаю почему она дедлочится.

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

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

                        Не наличие библиотеки против непонимания работы го, а именно го виноват во всём.

                        Автор, может вы передумаете?

                        И наконец, на С# код написан много быстрее, чем на хаскеле. Из минусов — некоторая кажущаяся объёмность переделки кода под новую структуру. Но разве мы постоянно меняем структуры в программе? Вроде нет. Тогда каков вес этого минуса? И каков вес плюса, позволившего очень быстро написать решение на привычном языке?

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

                          Не понимаю, я же это прямо написал в самой статье, в чем собственно и посыл. Есть много простого кода, но где-то забралась банальная опечатка или неправильное понимание примитива (в моем случае — неправильное понимание записи for ch in range channel).


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

                          Так я только рад буду, если вы покажете какую-нибудь библиотеку на го, которая позволит сделать все то же самое. Если вы мне дадите готовый код, я его в Upd. к посту добавлю.


                          И наконец, на С# код написан много быстрее, чем на хаскеле. Из минусов — некоторая кажущаяся объёмность переделки кода под новую структуру. Но разве мы постоянно меняем структуры в программе? Вроде нет. Тогда каков вес этого минуса? И каков вес плюса, позволившего очень быстро написать решение на привычном языке?

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


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

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

                            –1
                            >> Не понимаю, я же это прямо написал в самой статье, в чем собственно и посыл

                            Я же специально выделил часть вашего текста:
                            я точно знаю что происходит на каждой строчке программы


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

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

                            >> Моя грубая оценка: опытный го-разработчик написал бы минут за 5, а опытный хаскеллист минуты за три.

                            Пусть даже так (хотя не факт), но что это меняет? В любой серьёзной разработке собственно написание кода — это малая часть работы. И разница 3-5 минут вообще ничего не решает. А решает, например, лёгкость освоения языка, ибо нужно много программистов, а где их взять? И вот в случае хаскеля простота обучения, скажем прямо, так себе. А в случае го — всё доступно. Именно поэтому гуглы и придумали го, ибо им нужны миллионы индусов, которые задёшево и быстро освоят новый язык. Представьте себе, сколько времени займёт освоение хаскеля обычным индусом (из тех самых миллионов). Представили? Вот поэтому го идёт в массы, а хаскель тихо курит бамбук в академической среде.

                            ЗЫ. Это всё не спора ради. Просто замечания по смыслу текста. Надеюсь на некоторое дополнение к статье с осмыслением данных замечаний. Хотя с другой стороны — количество просмотров упадёт буквально через пару дней, так что может и не стоит заморачиваться с дописанием.
                              +5
                              Я же специально выделил часть вашего текста:

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


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

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


                              Пусть даже так (хотя не факт), но что это меняет? В любой серьёзной разработке собственно написание кода — это малая часть работы. И разница 3-5 минут вообще ничего не решает.

                              Ну во-первых 3-5 минут это почти в 2 раза. Во-вторых, как правильно замечено, чтение кода занимает б0льшую часть времени. Поэтому язык, где по сигнатуре можно понять всё, что происходит внутри (например, есть вывод на экран/запись в БД/… или нет) очень экономит это самое время. Ну и в-третьих мне кажется, что 15 строк прочитать проще, чем 60. Это еще раньше подмечено было, раздел "Не очень выразительный" (заголовок желтоват, но тот поинт актуален).


                              И вот в случае хаскеля простота обучения, скажем прямо, так себе. А в случае го — всё доступно. Именно поэтому гуглы и придумали го, ибо им нужны миллионы индусов, которые задёшево и быстро освоят новый язык. Представьте себе, сколько времени займёт освоение хаскеля обычным индусом (из тех самых миллионов). Представили? Вот поэтому го идёт в массы, а хаскель тихо курит бамбук в академической среде.

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


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


                              ЗЫ. Это всё не спора ради. Просто замечания по смыслу текста. Надеюсь на некоторое дополнение к статье с осмыслением данных замечаний. Хотя с другой стороны — количество просмотров упадёт буквально через пару дней, так что может и не стоит заморачиваться с дописанием.

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

                                +2
                                Для человека с хотя бы годом опыта работы в любом несистемном языке не будет никаких проблем с изучением хаскелля, мне казалось я достаточно по шагам описал возможный процесс.
                                Надеюсь, никто камнями не закидает, но… Вроде знаю много языков, разбираясь в них на уровне от «в общих чертах» и выше, но от хаскелля я немного в ужасе (хотя статью про монады и хасскель переваривал когда-то на Хабре). Читал Ваше введение, читал… Вроде по-отдельности всё понятно. Но вдруг main очередного примера складывается в одну длинную строчку с кучей знаков препинания… И смысл улетел, смысл кода не вижу.
                                Мне просто кажется немного невероятным, когда эта тема менее чем за час укладывается в голове настолько, что строка кода вроде:
                                main = putStrLn . show . add 10 . add5 $ sqr 3
                                Начинает без проблем в голове складываться в AST. Простите, но я с цитатой выше и с:
                                -- ура! нет скобочек
                                В таком случае не согласен.
                                  +4

                                  Ну давайте разберемся. Точечки нам по сути просто экономят скобки. Читаем запись практически дословно:


                                  Выводим на экран. Что?
                                  Преобразованное в строку. Что?
                                  Добавление 10 к чему?
                                  Добавление5 к чему?
                                  К квадрату трёх.


                                  Получаем "выведи на экран преобразованное в строку добавление 10 к добавлению5 к квадрату трёх". Ничем не хуже WriteLine(ToString(Add(10, Add5(Sqr(3)))))). А еще не надо считать сколько скобочек надо закрывать чтобы выражение скомпилировалось. Поверьте, когда я в сишарпе работал с AST я мечтал чтобы в нем была такая возможность...


                                  В таком случае не согласен.

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

                                    +1
                                    Поверьте, когда я в сишарпе работал с AST я мечтал чтобы в нем была такая возможность...

                                    Так всё равно же не помогло бы! Там-то сложные выражения частенько идут в середине, а "скобочно оптимизируется" только последний аргумент же.

                                      +3

                                      Ну даже если в середине, можно себе упростить жизнь. Например так:


                                      main :: IO ()
                                      main = do
                                          putStrLn . show $ (*) ((+) 10 5) $ (+) 2 3

                                      Естественно для арифметики и подходящих операторов писать нормально:


                                      main :: IO ()
                                      main = do
                                          putStrLn . show $ (10 + 5) * (2 + 3)

                                      просто для иллюстрации возможности, вместо +/* могут быть нормальные функции.

                                      0

                                      Точки — это просто, попробуйте уследить за типами тут:


                                      newtype Parser a = Parser { unParser :: String -> [(String, a)] }
                                      instance Functor Parser where
                                        fmap f = Parser . (fmap . fmap . fmap) f . unParser
                                        +4

                                        Я щас что-нибудь однострочное на акка стримах скину тоже офигеете парсить что написано :)


                                        Ответ просто — надо писать для людей, а не чтобы компилятор отстал.

                                          +1

                                          ну, так в сях тоже можно #define TRUE FALSE и угадывай там, что же на самом деле было. Наговнякать можно на любом языке.

                                            +2

                                            За типами пусть компилятор следит, он это хорошо умеет.


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


                                            Первый fmap работает для кортежа (String, a), второй — для списка [(String, a)], третий — для функции String -> [(String, a)], через их все и пробрасывается f. Вызовы Parser и unParser — это просто распаковка и упаковка именованного типа.


                                            Хотя я бы предпочёл всё же читать что-то более понятное...

                                              +3

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


                                              У вас был p :: Parser a и f :: a -> b. Как вы из этих двух ингридиентов можете получить p' :: Parser b? Взять то, что выдал p, и ко всем a применить f. Всё. У вас нет никаких других способов.

                                                0

                                                Если рассматривать всяческие дурацкие способы, то можно взять пустую строку, распарсить её как a, применить к результату f, после чего возвращать результат как константу независимо от переданной строки. Или можно просто вернуть undefined, у нас же нетотальный язык.


                                                Так что в общих чертах понимать что происходит всё равно надо.

                                                  +4
                                                  Если рассматривать всяческие дурацкие способы, то можно взять пустую строку, распарсить её как a

                                                  Как? Вы же ничего не знаете об a. У вас даже нет инстанса Read какого-нибудь, или Default, или Monoid. Вам просто неоткуда взять a, кроме как из парсера-аргумента к fmap.


                                                  Или можно просто вернуть undefined, у нас же нетотальный язык.

                                                  Ну чего вы, нормально ведь общались.


                                                  Ну, да, можно undefined, error или fmap f = fmap f. Тут, естественно, надо делать некоторые дополнительные предположения:


                                                  1. Этой всей ерунды нет (и это относительно легко проверить, для того, чтобы увидеть отсутствие всего этого, выводить типы и семантику каждого субтерма здесь необязательно), или же мы живём в 2023-м году, где в хаскель завезли выборочную проверку тотальности.
                                                  2. Вы также готовы смириться, что каждый из трёх упомянутых инстансов fmap нетотальный, равно как и завёрнутая в исходный Parser функция.
                                                    0
                                                    Вам просто неоткуда взять a, кроме как из парсера-аргумента к fmap.

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


                                                    instance Functor Parser where
                                                        fmap f x = Parser $ const $ f $ snd $ head $ unParser x ""
                                                      0

                                                      А, понял, о чём вы. Да, это пока проблема (но, с другой стороны, такой код надо писать специально).


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


                                                      fmapId :: pi p -> pi str -> runParser str (fmap id p) = runParser str p

                                                      и аналогично для композиции.

                                                        0

                                                        Вряд ли даже с завтипами и тотальностью вы можете запретить воткнуть композицию с const "" перед unParser если только специально не будете "ловить" именно этот случай.


                                                        Проще просто хоть немного понимать что в коде происходит, а не только на типы смотреть.

                                                          0

                                                          Я что-то не уверен, что для композиции с const "" будет выполняться fmap id p = p.

                                                            0

                                                            А завтипы разве как-то позволяют вывести это свойство для стандартного Functor?


                                                            Я вот смотрю на файл Functor.idr и не вижу никаких хитрых проверок. Да там, в отличии от Хаскеля, даже в комментарии подобная аксиома не упомянута.

                                                              0

                                                              Я думаю вы немного не туда смотрите :) https://github.com/idris-lang/Idris-dev/blob/master/libs/contrib/Interfaces/Verified.idr#L21

                                                                0

                                                                Ну это всё-таки пока ещё нестандартный модуль же...

                                                                  +1

                                                                  Как это нестандартный?


                                                                  Да и в любом случае какая разница так-то, стандартный или нет.

                                                                +1

                                                                Завтипы его позволяют выразить (и доказать).


                                                                А ссылку вам там PsyHaSTe дал. В идрисе принято разделять реализацию тайпкласса и ее верификацию (поэтому есть всякие VerifiedFunctor, VerifiedSemigroup и так далее).

                                                +2

                                                А зачем, это в этом случае, если оно тайпчекается? В более мощном языке я бы вообще сделал бы obvious proof search, и плевать, какой там терм, пока он удовлетворяет типу.


                                                Хотя в хаскеле можно {-# LANGUAGE DeriveFunctor #-} и потом


                                                newtype Parser a = Parser { unParser :: String -> [(String, a)] }
                                                  deriving (Functor)

                                                но то такое.

                                                0
                                                Начну с того, что статью я читал вчера, ближе ко сну, а в комментарии полез уже сегодня. И сегодня мне это кажется чуть более понятным. Но всё же…
                                                Думаю, меня в первую очередь смутило, что изначально точка представлялась как оператор объединения методов, а тут внезапно «10 . add5» – и всё, смысл ускользает (чтобы понять точнее, нужно уловить, что слева – вызов бинарного метода с одним аргументом – получаем каррирование вместо композиции). После чего дальше примеры выглядят ещё более жутко – вроде как общий смысл виден, но больше похоже на магию всё равно.
                                                Читаем запись практически дословно
                                                Читаем запись практически дословно
                                                Для этого нужно понимать каждую функцию в строке (количество, плюс, возможно – тип аргументов). А в умеренном количестве скобок ничего страшного нет, плюс:
                                                • Можно добавить пару пробелов, выделяя часть строки логически (да, в хасскеле – тоже можно);
                                                • IDE может подсветичивать парные скобки, помогая уложить в голове выражение (при такой записи в хасскеле это невозможно);
                                                • В некоторых случаях ничего не мешает вместо одной строки написать две, но более понятно;
                                                • Можно отдельно разобрать один из аргументов метода, даже если это очень сложное выражение – а вот тут в хасскеле тоже проблема, ибо если взять середину длинного выражения – будет непросто понятно, где кончается данный аргумент. Представьте себе вместо аргумента «10» выражение длинной с исходное выражение...
                                                • А ещё это вопрос вкуса, привычек, и требований к оформлению к кода (при работе в команде) – но никто не мешает закрывающие скобки (о, ужас!) выносить на отдельную строку, чтобы было видно, сколько и откуда их «вылезло».

                                                И раз уж изначально в статье отталкиваетесь от C#, то я добавлю, что в C# есть очень большая куча синтаксического сахара, вроде индексаторов, свойств, методов расширений – судя по Вашей ссылке, уверен, не мне Вам про это всё рассказывать. Когда в C++/Qt приходится писать (других вариантов, по сути, нет):
                                                QJsonValue v = ...; // некоторое значение
                                                if(v.isArray()) {
                                                  for(QJsonValue v2 : v.asArray())
                                                    doSomething(v2);
                                                }

                                                C# позволяет в подобных местах использовать свойства (v.isArray, v.asArray), избегая лишних скобок там, где они не нужны. Не хватает только возможности делать свойства расширений – при желании можно было бы писать так:
                                                Console.WriteLine( Add(10, Add5(3 . Sqr)).AsString )

                                                И немного offtopic по вашему коду
                                                Я бы для упрощения использовал ту же комбинацию в стиле хасскелевой точки:
                                                var ArgumentIdentifier = (id) => Argument(IdentifierName(id));
                                                // Тогда получается не так страшно местами
                                                something.AddArgumentListArguments(
                                                  ArgumentIdentifier(web3Identifier),
                                                  ArgumentIdentifier(abiIdentifier),
                                                  ArgumentIdentifier(binIdentifier),
                                                  ArgumentIdentifier(gasIdentifier),
                                                  ...
                                                );
                                                
                                                Опять же, если есть такая возможность, то через те же методы расширения, я бы попробовал сделать:
                                                SingletonList(
                                                  AttributeList(
                                                    SingletonSeparatedList(
                                                      Attribute(
                                                        IdentifierName("FunctionOutputAttribute")))))
                                                // =>
                                                AttributeIdentifier("FunctionOutputAttribute")
                                                  .AsSingletonSeparatedList()
                                                    .AsAttributeList()
                                                      .AsAttributeList()
                                                
                                                И нет больше пяти ")" подряд. Честно говоря, я не стал разбираться, что такое SingletonList, SingletonSeparatedList и т.д. Если это не статичные методы, тогда, скорее всего, в каждый .As*() метод нужно будет ещё параметром this передавать. Это может быть меньшей проблемой.

                                                Nim
                                                PS. Кстати, как-то пытался ради забаыв попробовать реализовать ECS на связке C++/Qt/Nim, и вот у последнего вроде мощная работа с AST из коробки, причём выглядело оно довольно удобно реализованным, как ни странно.
                                                Бросил, когда не вышло сделать обёртку для generic-класса, типа «QSharedPointer[ClassName]»; ни на пакете из репозитория убунты, ни на последней на тот момент версии, скачанной и собранной отдельно.

                                                Ну пишите скобки, если хотите, никто же не заставляет так писать.
                                                Это работает до первой встречи с чужим кодом; если самому не понять, как это работает – то потом нельзя будет быть уверенным, что при возникновении проблем ответ на свой вопрос по хасскелю на том же stackoverflow удастся разобрать и понять :) Не всё же бездумно копипастить.
                                                  +2

                                                  Как-то я пропустил ваш комментарий, вчера температура 39 была, поэтому видимо не заметил.


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

                                                  Честно говорю, вопрос привычки же :) Если вам нужно, можете выносить в локальные переменные:


                                                  main = do
                                                      let print = putStrLn . show
                                                      let printAdd10 = print . add 10
                                                      let pritnAdd10Add5 = printAdd10 . add 5
                                                      pritnAdd10Add5 $ sqr 3

                                                  Новички в сишарпе тоже LINQ считают магией и выносят каждый шаг обработки в локальные переменные, но более знакомые с технологией не стесняются по 5-10 вызовов в чейне сделать. Просто потому что так обычно удобнее.


                                                  Для этого нужно понимать каждую функцию в строке (количество, плюс, возможно – тип аргументов). А в умеренном количестве скобок ничего страшного нет

                                                  Так просто не пишут, потому что так удобнее. Как например флюент апи в сишарпе никто не выносит в переменные. Знай просто читай как оно написано и все.


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

                                                  Так а зачем вам скобки? допустим у нас есть функция add 10 . add 5 $ sqr 3. Смотрим на сигнатуру add, видим Int -> Int -> Int, смотрим, что передали в неё Int, значит осталось Int -> Int. Ок, теперь смотрим что справа от точки. add 5 $ sqr 3, тут все вроде понятно. Ну и вспоминая Int -> Int и то, что справа у нас получился Int, получаем результат выражения Int.


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


                                                  Не хватает только возможности делать свойства расширений – при желании можно было бы писать так:

                                                  Ну скоро добавят extension everything, но сахара имхо в сишарпе давно хватает. А вот полезных фич добавляется не так много, к сожалению.


                                                  И немного offtopic по вашему коду

                                                  Вы совершенно правы, но тут вся суть в том, что я этот код весь написал за ~2 рабочих дня, и у меня просто не было времени рефакторить. Я по сути написал кодогенератор на рослине сам для себя, забрав 2 дня из текущей задачи на работе. Потом конечно он мне эти 2 дня легко сэкономил и я выправился, но сидеть и улучшать код времени не было. Работает, и хорошо :) Но вообще конечно надо бы все эти лесенки убрать.


                                                  Но даже с вашими советами количество )))))))) будет большим, если только не дробить всё очень мелко.

                                                    0
                                                    У меня только вроде начало в голове складываться… Я разглядел, что при записи так, как в этих примерах, всё сводится к польской нотации. Получается, что «парсить в голове» нужно именно в таком режиме, читая слева направа «со стеком». Осталось вроде только разобраться в ролях точки и доллара.
                                                    С наскока не вышло. Открыл онлайн компилятор хаскелля:
                                                    main = do
                                                        putStrLn . show . add 10 . add5 $ sqr 3
                                                        putStrLn . show $ add 10 . add5 $ sqr 3
                                                        putStrLn . show $ add 10 $ add5 $ sqr 3
                                                        putStrLn . show $ add 10 . add5 . sqr 3
                                                        putStrLn . show $ add 10 ( add5 . sqr 3 )
                                                    Первые три строки выдают 24 каждая (3² = 9; + 5 = 14; + 10 = 24) и это расслябляет, складывается ощущение, что они взаимозаменяемы. То есть вроде особой разницы. А вот третья и четвёртая не собирается (т.е. работает только если их закомментировать), причём падает по несколько ошибок, независимо от того, оставляем только одну из них, или обе. Первая ошибка всегда показывает на третью строку: add5 = add 5
                                                    Вроде последний пример выглядит проще для новичка:
                                                    putStrLn . show $ add 10 ( add5 . sqr 3 )
                                                    В скобках точка приводит к композиции двух Int -> Int функций, затем им передаётся Int 3. Оно проходит sqr (= 9), затем add5 (= 14)…
                                                    Как я понял, доллар – это такая открывающаяся скобка «отсюда и в ту сторону». Смотрим от него справо: add 10 14 = 24
                                                    Справа от доллара у нас композиция «превратить в строку, затем вывести». То есть вроде не должно быть проблем, но не собирается.

                                                    [Пока дописывал] А нет, разобрался вроде. Почитал про приоритеты и ассоциативность, и оказалось, что вызов функции имеет более высокий приоритет, затем точка, математические и логические операторы, а доллар ­– самый последний. Тогда в последней строке получается, что:
                                                    ( add5 . sqr 3 )

                                                    Означает, что мы первым делам вызываем sqr 3, после чего точка «ломается». Тогда или пишем add5 $ sqr 3, чтобы отработал sqr 3 с последующим попаданием результата в add5, или берём композицию в скобки, (add5. sqr) 3…
                                                      +2

                                                      смотрите, точка между функциями означает композицию. f . g === x => f(g(x)). Теперь сморим на эту запись:


                                                      add5 . sqr 3

                                                      sqr 3 это не функция, а значение. Нельзя скомпозировать функцию и значение. Поэтому оно и не собирается, у вас по стрелкам не сходится. Композиция работает так: (a -> b) . (b -> c) => a -> c. А у вас слева add5 :: Int -> Int, а справа sqr3 :: () -> Int. Поэтому и не компилируется :)


                                                      Поэтому если вы напишете так:


                                                      putStrLn . show $ add 10 (( add5 . sqr) 3 )

                                                      То стрелки сойдутся и все заработает




                                                      Впрочем, вижу вы это и дописали, а я проглядел.

                                                        0

                                                        Уточнение: sqr 3 имеет тип Int, а не () -> Int.


                                                        Тип () -> Int могла бы иметь конструкция const $ sqr 3

                                                          0

                                                          Они изоморфны, потому что стрелки между категориями всегда отображаются на значения множества 1-1 :)



                                                          Собственно вы и показали способ это сделать. В обратную сторону так же тривиально :)


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

                                                          +1

                                                          Тольуко у композиции функций немного другая сигнатура:


                                                          GHCi>:t (.)
                                                          (.) :: (b -> c) -> (a -> b) -> a -> c
                                                            +1

                                                            Посыпаю голову пеплом, вы совершенно правы. Забыл, что у неё аргументы инвертированы.

                                                  +3
                                                  AST здесь нет, знаков препинания минимум, чистый смысл. Большинство людей, с которыми я общался, видит в этом смысл легко. Попробуйте ещё разок, обязательно получится, я уверен.
                                                    0
                                                    Как это нет AST? Когда компилятор хасскеля парсит код, он же разбирает и приводит его к какому-то подобию синтаксического дерева?
                                                    Мне просто кажется, что если я хочу понять часть сложного выражения на хасскеле – нужно подробно разобрать всю строку, каждый знак препинания и каждый идентификатор. Хоть с константами вроде проблем нет.
                                                    И если идёт код вроде того, что дал выше PsyHaSTe – можно взять любую часть кода или длинного выражения, и даже вручную (а лучше – с подсветкой парных скобок в IDE) понять, что там вызывается, с какими аргументами. Опять же, если взять json/xml/css документ, даже после минификации – можно из середины взять элемент, и разобраться со всем его содержимым, не разбирая документ (или строку в несколько десятков килобайт) целиком. Просто по парным закрывающим/открывающим тегам/скобкам. Может, это дело привычки, но хасскель в этом плане кажется сложнее.
                                                    Не претендую быть истиной в последней инстанции. )
                                                      +3
                                                      Когда компилятор хасскеля парсит код, он же разбирает и приводит его к какому-то подобию синтаксического дерева?

                                                      Там с этим все сложно по причине, поразительно похожей на проблемы парсинга C++. Но, впрочем, это я придираюсь, ваш оппонент скорее имел в виду то, что разница между AST и CST минимальна.


                                                      И если идёт код вроде того, что дал выше PsyHaSTe – можно взять любую часть кода или длинного выражения, и даже вручную (а лучше – с подсветкой парных скобок в IDE) понять, что там вызывается, с какими аргументами.

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

                                                        +2
                                                        Дерево синтаксиса есть внутри компилятора, но код деревом синтаксиса (тем более, абстрактного), не является.
                                                          +4
                                                          Всё отлично читается и по кусочкам. Я думаю, вы привыкли к Алгол-С-подобному синтаксису, а к ISWYM-подобному ещё не привыкли. Если поработаете с Хаскелем некоторое время, научитесь и ему.
                                                      –8
                                                      >> Ну так все правильно, каждая строчка понятна, а при их композиции произошла фигня. Потому что строчек много, и комбинаторный взрыв происходит очень неприятный.

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

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

                                                      Вообще, когда я не понимаю, что происходит при выполнении программы, меня это напрягает. Я оказываюсь в ситуации, когда должен полагаться на волю случая — а вдруг там внутри всё само как-то правильно сложится? И да, хаскель содержит встроенные средства контроля разных косяков с типами, а так же сама структура вызовов кое-что подправляет за программиста, но ведь это всё — абсолютно неявно, неочевидно и непонятно, ровно до тех пор, пока вы не вызубрите все приоритеты и все используемые функции. А что бы вызубрить все функции стандартных библиотек, надо потратить немало времени. Без зазубривания же вы не сможет понять чужой код. Точнее — по вашему, как вы выше сообразили без понимания происходящего внутри, представить себе нечто вполне возможно, и даже хаскель поможет вам своими приятными качествами, но тем не менее — вы по прежнему не понимаете, что происходит внутри, а значит по прежнему не понимаете, где находится проблема, если после запуска программа выдаст не то, что вы ожидали. И как только такая неожиданность случится — всё, полностью и дословно повторится история с го, когда по частям вам кажется, что всё понятно, а в целом — ничего не работает. А всё из-за чего? Из-за отсутствия понимания происходящего внутри. А для получения такого понимания вам нужно потратить много времени на зазубривание приоритетов и хотя бы функций из стандартной библиотеки (а их там несколько десятков, плюс десяток занимательных типов, и это без монад и прочего IO). Вот в этом и проблема хаскеля — он не предназначен для тех, кому нужен простой и быстрый результат. А это как раз все те индусы. И как бы вы не возражали, но «индусов» на земле на порядки больше, чем тех, кто готов потратить время на спокойное изучение хаскеля, на зазубривание приоритетов и изучения всех библиотечных типов и функций. Это аналогично высшему образованию — нужно пройти высшую математику, и лишь потом станет понятно, почему теория автоматического управления целевым объектом действительно даёт правильный результат. Но вспомним — сколько людей так и остаются без высшего образования? Вот такой же процент не будет готов и к изучению хаскеля. А вот го они осилят легко. Потому что там сразу ясно, что происходит. Ну а комбинаторная сложность комплексных явлений, будь то текст программы или что угодно ещё, всегда высокая. И в хаскеле с ней бороться невозможно без понимания всех функций, типов, приоритетов. Хотя да, можно полагаться на удачу — запустил и оно как-то само всё сделало. Но это не наш метод. Это скорее опять к индусам, которые понадёргают из примерчиков составляющих и получают нечто, вроде даже работающее, но все проблемы, кроме самых очевидных, индусы никогда не вылавливают, и не важно, на хаскеле они это делают или на го. Но го они хотя бы способны понять. А вот хаскель — практически никогда не поймут до уровня, который позволит им разобраться в сложных проблемах.

                                                      >> Мне кажется, что такой библиотеки просто в принципе нет, по крайней мере я не представляю, как её создать с теми возможностями, что дает Go.

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

                                                      И о совести. Почему-то сторонники хаскеля всегда наиболее воинственно отстаивают преимущества своего любимого чуда. И при этом игнорируют любые указания на недостатки. Вот почему бы это?

                                                      >> язык, где по сигнатуре можно понять всё, что происходит внутри (например, есть вывод на экран/запись в БД/… или нет) очень экономит это самое время

                                                      Нельзя ничего понять по сигнатуре, если не знаешь алгоритма внутри вызываемой функции. Можно строить предположения, можно догадываться, можно гадать на кофейной гуще, но полноценно понять — нельзя. Назовём функцию вычисления квадрата cube, вы по её сигнатуре поймёте, что с ней что-то не так?

                                                      >> мне кажется, что 15 строк прочитать проще, чем 60

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

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

                                                      Вопрос не в возможности изучения, а в скорости. Индус будет изучать лет 5 (может утрирую, но не сильно). А го изучит параллельно с работой, даже не заметит. Есть разница?

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

                                                      Проблема не в самооценке, а в объективно существующей потребности. Потребность простая — надо много и быстро. А индусы на хаскеле — не способны ни много, ни быстро. И как бы вы не решали, кем хотите быть, проблема от этого никуда не денется.

                                                      И да, разработчики, которые ценят своё время, как раз очень озабочены затратами этого самого времени на зазубривание хаскеля хотя бы на уровне стандартного синтаксиса и Prelude. То есть если времени девать некуда — ну тогда ОК, можно заниматься хаскелем. А если есть актуальные задачи?

                                                      Вообще, пришла в голову простая мысль — сторонники функционального подхода реально не знают, что такое требования жизни. То есть в академиях и прочих неспешно грызущих гранит науки заведениях действительно можно годами ваять на хаскеле примитивный софт, потом его вылизывать, совершенствовать и т.д. И в конце получить нечто, от чего можно выпячивать губу, мол вон что мы сотворили! Но как коснёшься реального применения (то есть в реальной жизни), то сразу вылазят косяки хоть и достаточно общего подхода, но далеко не универсального. Возьмите стандартный (или просто распространённый, я тут не возьмусь вешать ярлыки) ORM на хаскеле — концепт изначально ориентирован на манипуляцию SQL, а в реальной жизни акцент смещается на манипуляцию с результатами работы SQL. И это сильно не одно и то же. Хотя да, подход в хаскельном варианте солидный, обобщённый и т.д. Но пользоваться — неудобно. Может для мелких академических задач это нормально, но для реальной жизни — ну не то, просто неудобно.

                                                      В целом я бы повозился с тем же хаскелем побольше, но вот реально — а что толку? Куда я его прикручу с его неудобными ORM-ами и прочим? Народ давно попробовал и сделал вывод — для enterprise разработки оно реально неудобно. Куча гемороя с поддержкой состояния, малое количество библиотек и неудобство существующих (хотя хаскелисты здесь будут долго кидаться гнилыми помидорами).
                                                        –1

                                                        TL;DR Вообще проблема с тем, что это работает автомагически — она характерна для всей индустрии. Историю с left-pad помните, надеюсь? Вот и везде так — берут готовые библиотеки, не вдаваясь в суть того, что у них под капотом.

                                                          +6
                                                          Во первых, это подтверждает очевидную истину — не зная в деталях объект приложения усилий, можно нагородить ерунду. Поэтому здесь ничего удивительного нет, кроме одного момента — почему вы удивляетесь, что не можете понять смысл целого, когда не знаете деталей?

                                                          Вопрос в емерджентности: проблема возникает не принадлежит какой-то конкретной строчке кода, а возникает лишь из их совокупности.


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

                                                          О каких приоритетах идет речь? Выполнение идет всегда справа налево, как и во всех остальных языках — f(g(h(x))), сначала h, потом g, потом f. Знать все библиотечные функции не обязательно, достаточно уметь сформулировать вопрос в гугл. А то и в хугл.


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

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


                                                          Без зазубривания же вы не сможет понять чужой код.

                                                          Я не настоящий хаскеллист, но судя по тому что я слышал от людей чтобы понять чужой код достаточно посмотреть какие типы у функции + название. Учить их не надо, чтобы понимать. Точно так же как если вы приходите в новый проект на C#/Java/Go и там есть функция SendEmail с параметрами урла и текстом уведомления, вам не надо "учить" эту функцию. Ну ок, она есть.


                                                          А вот хугл это очень крутая штука, я тут потыкал недавно. Например, у вас есть массив, и вы хотите его отсортировать. Достаточно просто сделать поиск по сигнатуре (a -> Bool) -> [a] -> [a], и почти наверняка найдете пару функций сортировки. Выбирайте любую. То есть у вас ситуация, что у вас есть некоторые типы на входе, а функция возвращает какой-то другой. Вы не знаете, что сделать, чтобы все отработало. Вбиваете сигнатуры в хугл и он вам подсказывает, какие есть библиотеки и с какими функциями. Из-за того, что сигнатура на 100% описывает что происходит внутри это и становится возможно. С императивными функциями () -> () так не выйдет, увы.


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

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


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


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

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


                                                          Хотя да, можно полагаться на удачу — запустил и оно как-то само всё сделало. Но это не наш метод.

                                                          Поймите, что вся суть в том, чтобы компилятор ловил ошибки. Собралось, значит все правильно и отработает ожидаемым образом. Именно за эту безопасность приходится платить всеми этими "сложностями". Но сложность привносимая инструментом должна быть ниже чем побежденной проблемы. Экскаватор сложнее лопаты, и если вы хотите перекопать грядку то он излишен. Но глупо спорить, что он хуже лопаты в задачах выкапывания колодцев. Я по работе в основном занимаюсь именно колодцами, а не простыми грядками. Поэтому я и рад такому инструменту.


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

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


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

                                                          Спросите у сишарпистов, что понятнее, императивный код на циклах или декларативный на LINQ.


                                                          Вопрос не в возможности изучения, а в скорости. Индус будет изучать лет 5 (может утрирую, но не сильно). А го изучит параллельно с работой, даже не заметит. Есть разница?

                                                          Мне кажется вы очень переоцениваете сложность. Я думаю, что разница в несколько раз. Если для го приличный код можно выдавать через месяц, то на хаскелле через 2-3. На примере того же Rust я слышал именно такую статистику. Вопрос в том, что у вас нет ограничения на потолок. Условно говоря, профессиональный гошник напишет программу за день, а начинающий — за 2 недели. При это начинающий хаскеллист будет писать 2 месяца, а профессиональный — за час. Я стремлюсь именно к последней цифре.


                                                          Проблема не в самооценке, а в объективно существующей потребности. Потребность простая — надо много и быстро. А индусы на хаскеле — не способны ни много, ни быстро. И как бы вы не решали, кем хотите быть, проблема от этого никуда не денется.

                                                          Ну мне жаль индусов, я выбираю язык для себя. Я не буду выбирать плохой инструмент только потому, что он модный. 95% веб-сайтов это вордпресс сайты-визитки, но при этом люди как-то продлжают заниматься бекендом на этих ваших джавах и нодах.


                                                          И да, разработчики, которые ценят своё время, как раз очень озабочены затратами этого самого времени на зазубривание хаскеля хотя бы на уровне стандартного синтаксиса и Prelude. То есть если времени девать некуда — ну тогда ОК, можно заниматься хаскелем. А если есть актуальные задачи?

                                                          Времени вообще никогда не хватает
                                                          image
                                                          Вопрос только в желании вечно оставаться на подхвате, и писать руками то, что могла бы сгенерировать машина.


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

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


                                                          В целом я бы повозился с тем же хаскелем побольше, но вот реально — а что толку? Куда я его прикручу с его неудобными ORM-ами и прочим? Народ давно попробовал и сделал вывод — для enterprise разработки оно реально неудобно.

                                                          Могу спросить в хаскельном чате что у них с орм, но по словам одного моего знакомого по крайней мере с постгресом никаких проблем нет.


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


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

                                                            +3
                                                            О каких приоритетах идет речь?

                                                            Судя по прошлым обсуждениям, человек опасается, что если у него там написано foo <$> bar <*> baz, то компилятор это как-нибудь не так распарсит. Хотя легко видеть, что тайпчекается только один из способов парсинга, и это достаточно универсальный принцип.


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

                                                            А то и в репл (ну, если вы не знаете, что делает написанная в коде перед вами функция или оператор). В последних версиях ghc посмотрели на идрис и завезли :doc, кстати. Если у вас ghc >= 8.6, попробуйте :doc (<$>), прикольная штука.


                                                            Могу спросить в хаскельном чате что у них с орм, но по словам одного моего знакомого по крайней мере с постгресом никаких проблем нет.

                                                            Мне лично beam норм зашёл. Есть довольно прикольный opaleye на стрелочках, но вот как раз конкретно он весьма академичен в плохом смысле.

                                                              +2
                                                              Зкакон необходимого разнообразия в формулировке Бира звучал примерно так: «Сложность системы управления должна сответствовать сложности объекта управления» в нашем случае будет что-то вроде «Сложный инструмент для сложных задач».
                                                                0

                                                                Как же быть с задачами, которые простые только в самом начале (то есть чуть менее, чем ~90% всех задач)?

                                                                  0
                                                                  ну это вопрос про менеджмент, унивесального ответа нет, где-то можно забить и писать на чём пишется, где-то нужно сразу писать на нормальном, где-то допустимо переписать при усложнении.
                                                                    0

                                                                    Это получается, что помянутый закон на практике не так чтобы часто применим?

                                                                      0
                                                                      Применим и применяется это разщные вещи.
                                                                –5
                                                                >> О каких приоритетах идет речь?

                                                                Вот вы в своём коде использовали значок $, а почему? Знаете?

                                                                >> Знать все библиотечные функции не обязательно, достаточно уметь сформулировать вопрос в гугл. А то и в хугл.

                                                                Ну так вы слона не продадите. Я тоже могу сказать, что знаю любой язык, но со словарём, ведь делов-то — вбил в гугл-транслейт текст и почти всё понял!

                                                                Эффективно использовать инструмент, постоянно ползая по интернету — невозможно.

                                                                >> чтобы знать что что-то работает нужно иметь возможность судить об этом по интерфейсу, а не по реализации

                                                                Во первых, интерфейсы есть почти во всех императивных языках. Чем в этом плане лучше хаскель? Во вторых, реализация хоть сколько-нибудь сложного алгоритма всегда предполагает знание её пользователем набора ограничений, вне которых алгоритм не работает (или работает криво). Если вы ещё не изучили, что такое возведение в квадрат — какой смысл говорить о функции, которая возводит в квадрат? Даже если весь этот текст содержится в её названии.

                                                                >> >> А вот хугл это очень крутая штука, я тут потыкал недавно
                                                                >> Достаточно просто сделать поиск по сигнатуре (a -> Bool) -> [a] -> [a]
                                                                >> С императивными функциями () -> () так не выйдет, увы.

                                                                Почему не выйдет с императивом? Например: (int[]) -> (int[]). Но интересно, а если функция просто переставляет пару значений в массиве, то как вы её отличите от сортировки?

                                                                >> Если человек потратил время чтобы изучить его возможности он напишет так же быстро, как и на го

                                                                Но он так же потратит меньше времени на изучение го, значит суммарные затраты времени меньше.

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

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

                                                                Я же говорил — будь у меня куча лишнего времени, я бы обязательно много чего поизучал бы. Но даже когда вдруг время появляется, то оказывается, то «много чего» за раз не получается, приходится расставлять приоритеты, а потом находить способ не затягивать с первым выбранным предметом, ибо всё на свете можно копать до бесконечности, но тогда ведь на остальные темы времени никогда не будет. Поэтому компромисс обязателен. Как минимум для тех, кто хочет получить от жизни больше. Но да, можно отказаться от всего остального и заняться самосовершенствованием в хаскеле. Только мне это не кажется полезным.

                                                                >> на протяжении всего этого года нам могли бы объяснить принцип, и мы бы для любых фигур могли бы считать, а не только для «одобренных минобром»

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

                                                                >> Поймите, что вся суть в том, чтобы компилятор ловил ошибки.

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

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

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

                                                                >> Спросите у сишарпистов, что понятнее, императивный код на циклах или декларативный на LINQ.

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

                                                                >> Если для го приличный код можно выдавать через месяц, то на хаскелле через 2-3. На примере того же Rust я слышал именно такую статистику. Вопрос в том, что у вас нет ограничения на потолок.

                                                                Ограничивает не язык, а умение придумать правильный алгоритм. Ну а инструмент — он и в африке инструмент. То есть он должен быть удобным, это да, но превозносить удобства до небес, даже заявляя, что «с этим инструментом можно сделать то, чего ни один другой (императивный) инструмент не может» — это неправильно.

                                                                >> Я не буду выбирать плохой инструмент только потому, что он модный

                                                                Хаскель как раз — модный. То есть продуктивность в реальной жизни он не обеспечивает, но создаёт иллюзию «последнего писка технологичности».

                                                                >> Времени вообще никогда не хватает

                                                                Картинка весёлая :)

                                                                Но про компромисс и при её создании забыли.

                                                                >> Мы недавно переписали сервис с джавы на хаскель, и выиграли в 5 раз по памяти и в 10 по производительности

                                                                Значит писали сервис индусы. Если переписать сервис с хаскеля на Java, но не по индуйски, то вы ещё больше сэкономите.

                                                                >> Что касается экосистемы, то всегда есть Scala

                                                                Да, есть. Но всё же пока востребован императив, поэтому я на «тёмной стороне» силы. Вот победит функциональщина, докажет продуктивность в массовой разработке — я к вам обязательно присоединюсь :)

                                                                А пока — пусть фанаты функций готовят дорогу для будущего счастья, может даже когда-то у них получится. Я же поигрался и не увидел серебряных пуль и прочего вундерваффе.
                                                                  +4
                                                                  Есть, например, SQL (более распространённый аналог LINQ). И никто в здравом уме не пишет в императивных языках всё то, что можно сделать на SQL.

                                                                  Зато почему-то продолжают писать всё то, что можно сделать на LINQ. Видимо, или язык плохой достался, или всё же не аналог.

                                                                    –4
                                                                    Во первых — привычка. Во вторых — LINQ не идеал.
                                                                      +3

                                                                      Привычка чего? Циклы появились раньше LINQ. Как раз наоборот, у людей была привычка писать циклы, и они начали писать запросы. Как так-то?

                                                                    +4
                                                                    Вот вы в своём коде использовали значок $, а почему? Знаете?

                                                                    Чтобы не писать скобочки. Вот вы в своем языке знаете, у чего больше приоритет: у плюсика, скобочек или знака умножения?


                                                                    Эффективно использовать инструмент, постоянно ползая по интернету — невозможно.

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


                                                                    Во первых, интерфейсы есть почти во всех императивных языках. Чем в этом плане лучше хаскель?

                                                                    То что вы не разделяете интерфейс как элемент ООП и интерфейс как набор публичных АПИ о многом говорит.


                                                                    Почему не выйдет с императивом? Например: (int[]) -> (int[]). Но интересно, а если функция просто переставляет пару значений в массиве, то как вы её отличите от сортировки?

                                                                    Потому что я могу написать такую функцию на сишарпе:


                                                                    int[] Foo(int[] a) 
                                                                    {
                                                                       Console.WriteLine("Hello!");
                                                                       ElasticSearch.Push(new LogMessage("Processing a"));
                                                                       return a.Shuffle();
                                                                    }

                                                                    А вот в хаскелле не получится (да, это плюс).


                                                                    Потому что в хаскелле если я увижу сигнатуру foo :: [a] -> a я точно знаю, что результатом выполнения будет либо какой-то из элементов списка.


                                                                    теперь посмотрим на шарп:


                                                                    T Foo(T[] a) 
                                                                    {
                                                                       return (T) Activator.CreateInstance(T); // упс
                                                                    }

                                                                    Упс.


                                                                    Но он так же потратит меньше времени на изучение го, значит суммарные затраты времени меньше.

                                                                    Если вы планируете разработкой заниматься хотя бы еще 10 лет, то нет, не меньше.


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

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


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

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


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


                                                                    Ну здесь же вы опять в сторону индусов уходите, хотя их образ действий критикуете.

                                                                    Да, я критикую образ индусов. Делать втупую то, что может сделать машина — пустая трата времени. Вся суть программирования в автоматизации. Если бы всем было в кайф все руками делать (например, вместо экселя на калькуляторе считать или вместо автоматического пробрасывания через ExceptT писать if err != nil) то эти инструменты никогда не появились бы.


                                                                    И если вы предпочитаете получить работающий способ здесь и сейчас, то вы идёте строго по пути индусов.

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


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

                                                                    Что если я вам скажу, что компилятор при желании может проверить все ошибки в вашей программе, включая логические?


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

                                                                    Ну если вы хотите на го написать интерпретатор хаскелля то милости просим, теоретически это конечно возможно. Есть желающие?


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

                                                                    Вообще не понял этот момент. Вообще-то мы говорили Linq2Objects, там вообще никаким SQL не пахнет. Вы же сказали про императивные циклы, вот я привел пример того, как языке вместо них пишут запросы. Вы, это, сжудения не подменяйте.


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

                                                                    Вот я придумал сделать сортировку, которая не зависит от типа объектов (только чтобы он сравниваться умел), а язык, собака, мне не дает его написать. Я конечно понимаю, что путь индуса для трех разных типов в программе где эта функция используется накопипастить 3 раза (чай не 10 же), но все же?


                                                                    Хаскель как раз — модный. То есть продуктивность в реальной жизни он не обеспечивает, но создаёт иллюзию «последнего писка технологичности».

                                                                    Это про хаскель из каждого утюга вещают и запросы в гугл подменяют? Вот не знал.


                                                                    Но про компромисс и при её создании забыли.

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


                                                                    Значит писали сервис индусы. Если переписать сервис с хаскеля на Java, но не по индуйски, то вы ещё больше сэкономите.

                                                                    Ненастоящий шотландец


                                                                    Да, есть. Но всё же пока востребован императив, поэтому я на «тёмной стороне» силы. Вот победит функциональщина, докажет продуктивность в массовой разработке — я к вам обязательно присоединюсь :)

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


                                                                    image


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


                                                                    Если посмотреть на график, то вещи вроде паттерн матчинга, лямбд, query-language в языках и асинк-авейт это Early majority, а то что я тут рассказываю — Early adopters. А у человека с опытом всегда преимущество.

                                                                      –7
                                                                      >> Вот вы в своем языке знаете, у чего больше приоритет: у плюсика, скобочек или знака умножения?

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

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

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

                                                                      >> То что вы не разделяете интерфейс как элемент ООП и интерфейс как набор публичных АПИ о многом говорит.

                                                                      Не знаю, на основании чего сделан такой вывод. И тем более, не знаю, о чём это говорит.

                                                                      >> Потому что я могу написать такую функцию на сишарпе

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

                                                                      >> Это какой-то призыв «спервадобейся» и «ктотытакойчтобымнеговорить»

                                                                      Нет, это призыв посмотреть правде в глаза. А правда простая — индусы реально конкурентоспособны. Вы будете спорить? Ну тогда съездите в Индию, посчитайте их там по головам — будет очень много новых открытий.

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

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

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

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

                                                                      >> Что если я вам скажу, что компилятор при желании может проверить все ошибки в вашей программе, включая логические?

                                                                      Я вам не поверю.

                                                                      >> Ну если вы хотите на го написать интерпретатор хаскелля то милости просим, теоретически это конечно возможно.

                                                                      Нет, там проблема решается гораздо проще.

                                                                      >> Вообще-то мы говорили Linq2Objects, там вообще никаким SQL не пахнет. Вы же сказали про императивные циклы, вот я привел пример того, как языке вместо них пишут запросы. Вы, это, сжудения не подменяйте.

                                                                      Поясняю — LINQ, это расширение декларативных запросов из области реляционных баз на область любых множеств, поэтому SQL — основа того, что получилось в LINQ. Ну а императивные циклы есть способ обработки всё тех же множеств, поэтому если речь идёт о БД, там эту часто повторяющуюся потребность выделили и создали на этой основе язык SQL, после чего (лет 50 спустя) некто наконец решился создать аналог с чуть более широкой областью применения (LINQ). Поэтому я вам и ответил про SQL.

                                                                      >> Вот я придумал сделать сортировку, которая не зависит от типа объектов (только чтобы он сравниваться умел), а язык, собака, мне не дает его написать

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

                                                                      >> Это про хаскель из каждого утюга вещают и запросы в гугл подменяют?

                                                                      А про го разве вещают? Нет, там всё проще — вешают объявление, з/п программиста на го — 200к$, и всё, далее остаётся лишь говорить о чьей-то неконкурентоспособности.

                                                                      >> Так ваш компромисс это исключительно «хуяк хуяк и впродакшн». Такой себе компромисс, должен сказать.

                                                                      Я не спорю, любую мысль можно извратить, но компромисс — это не крайность, а потому вы меня просто неправильно поняли, либо понять не захотели.

                                                                      >> Ненастоящий шотландец

                                                                      Опять нет. Я вам снова повторю — если кто-то сделал что-то в 5 раз быстрее, значит тот, кто делал до него — клинический дебил, либо ему было плевать на результат (а значит на результат было плевать и всем его начальникам по цепочке до самого верха).

                                                                      >> вещи вроде паттерн матчинга, лямбд, query-language в языках и асинк-авейт это Early majority, а то что я тут рассказываю — Early adopters. А у человека с опытом всегда преимущество

                                                                      Точно. У человека с опытом есть большое преимущество — он может расслабиться и лениво поглядывать на безумства Early adopters, ибо когда они наконец нарезвятся вдоволь, можно будет спокойно спуститься с небес и… отыметь всё стадо получить все ништяки.

                                                                      Но проблема в том, что пока винигрет в сообществе функционалов не устаканился, а это означает, что данная технология ещё молода и не даёт должного эффекта в реальной жизни. Поэтому заниматься активно этой пляской с бубнами — тратить время на развитие недоразвитого организма. Только вот конкретно мне пока не хочется тратить много времени на этого детёныша, просто потому, что для меня есть более интересные темы, а в данной — да, похоже есть кое какие перспективы, но пока они где-то за облаками.
                                                                      +4
                                                                      Вот вы в своём коде использовали значок $, а почему? Знаете?

                                                                      Забавно, я вот прям только что написал у себя в коде


                                                                      insertValue :: ASetter' StatsAggregator [a] -> a -> StatsAggregator -> StatsAggregator
                                                                      insertValue setter val obj = obj & setter %~ (val :)

                                                                      вообще не думая о приоритетах &, %~ и :, и оно просто работает. Как так получается без заучивания всех приоритетов всех операторов, ну или хотя бы всех операторов из шпаргалки по линзочкам?


                                                                      Во первых, интерфейсы есть почти во всех императивных языках. Чем в этом плане лучше хаскель?

                                                                      Они там могут быть выразительнее.


                                                                      Почему не выйдет с императивом? Например: (int[]) -> (int[]). Но интересно, а если функция просто переставляет пару значений в массиве, то как вы её отличите от сортировки?

                                                                      В хаскеле пока никак. Но никто в здравом уме не будет писать функцию типа


                                                                      stupid (x0 : x1 : xs) = x1 : x0 : xs
                                                                      stupid xs = xs

                                                                      и навешивать на неё сигнатуру stupid :: Ord a => [a] -> [a]. Там не нужна Ord a.


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


                                                                      Но он так же потратит меньше времени на изучение го, значит суммарные затраты времени меньше.

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


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

                                                                      Говорят, практика — критерий истины, и она что-то с вами тут не согласна.


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

                                                                      И что же вас ограничивает в системе типов хаскеля?


                                                                      (Меня лично много что, но не в ту сторону, в которую вы думаете.)


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

                                                                      У меня начинает складываться впечатление, что этот тезис у вас не вполне обоснован опытом.

                                                                        0
                                                                        Другое дело, что функцию, сортирующую список, вы уже не отличите от функции, возвращающей уникальные элементы в нём (опять же, в сегодняшнем хаскеле), но это именно что другое дело.

                                                                        Строго говоря, пример не очень удачный — по идее, для уникальных элементов нужно Eq a, а не Ord a?

                                                                          +3

                                                                          Имея Ord a можно получить уникальные элементы за O(N log N), в то время как Eq a даёт только O(N2)

                                                                    +6

                                                                    Ну вот опять, общие слова, пара мифов и никакой конкретики.


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

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


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

                                                                    Только если компилятор вас не ограждает.


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

                                                                    Для того, чтобы стало понятно, ничего вызубривать не обязательно.


                                                                    а значит по прежнему не понимаете, где находится проблема, если после запуска программа выдаст не то, что вы ожидали

                                                                    Приятное свойство в том, что то непонимание, о котором идёт речь, не даст программе скомпилироваться, если там что-то не так.


                                                                    Да, даже приоритеты операций. Они там по-умному сделаны, если вы ассоциативностью напутали, то у вас программа просто не скомпилируется.


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

                                                                    И вся эта интроспекция, рефлексия и просто тип Object будут проверяться в компилтайме?


                                                                    И о совести. Почему-то сторонники хаскеля всегда наиболее воинственно отстаивают преимущества своего любимого чуда. И при этом игнорируют любые указания на недостатки. Вот почему бы это?

                                                                    Могу привести в пример вагон недостатков, от качеств производительности до типо-теоретических.


                                                                    Только это будут недостатки не в сравнении с Go.


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

                                                                    Людей, которые на хаскеле пишут в продакшен, в вашем мире не существует? Ну ок.


                                                                    В целом я бы повозился с тем же хаскелем побольше, но вот реально — а что толку? Куда я его прикручу с его неудобными ORM-ами и прочим?

                                                                    А какие ORM вы пробовали? Чего вам там не хватило?


                                                                    Куча гемороя с поддержкой состояния

                                                                    А в чём геморрой-то?


                                                                    малое количество библиотек

                                                                    Да, кое-где мало. Но это в любом языке так — на Java мало библиотек для машинного обучения, на C++ мало библиотек для того же ORM (плюсы к тырпрайзу не готовы), на питоне мало библиотек для разработки компиляторов.

                                                                      +5
                                                                      >> Если вы в курсе, что такое дженерики, то всё вы легко поймёте.
                                                                      так как их и нет в го. Именно поэтому это и минус именного го как языка, который предотвращает появления «обобщенных» (generic) решений. Можно, конечно, использовать везде interface{} (не забываем, что это не го-вей go-proverbs.github.io), вот только это увеличивает вероятность багов и более того распространяет эту заразу в клиентский код.

                                                                      >>>> язык, где по сигнатуре можно понять всё, что происходит внутри (например, есть вывод на экран/запись в БД/… или нет) очень экономит это самое время
                                                                      >> Нельзя ничего понять по сигнатуре, если не знаешь алгоритма внутри вызываемой функции.
                                                                      Вы читаете текст, который комментируете? Советую почитать про tagless final encoding чтобы узнать о том, как много информации может предоставлять сигнатура функции.

                                                                      >> Вообще, пришла в голову простая мысль — сторонники функционального подхода реально не знают, что такое требования жизни
                                                                      map-reduce (hadoop, spark), erlang, aws lambda… как жаль что «сторонники функционального подхода» продолжают тащить в продакшен эту свою функциональщину
                                                                        –3
                                                                        >> вот только это увеличивает вероятность багов и более того распространяет эту заразу в клиентский код

                                                                        Опять не совсем правильный подход. Эволюция ведь, как ни странно, всё ещё работает, то есть отбирает наиболее адаптированных к условиям обитания. Поэтому возникает интересный вопрос — а почему в нынешних условиях процветают индусы? А функциональные языки задвинуты куда-то в академии.

                                                                        >> Вы читаете текст, который комментируете? Советую почитать про tagless final encoding чтобы узнать о том, как много информации может предоставлять сигнатура функции.

                                                                        Я читаю текст, который комментирую. А вы читаете? В моём сообщении было про неверно интерпретируемое название. И заметьте — это ещё цветочки. То есть что для полноты понимания вам стоит отказаться от всей документации и попробовать понимать чужой код по сигнатурам.

                                                                        >> map-reduce (hadoop, spark), erlang, aws lambda… как жаль что «сторонники функционального подхода» продолжают тащить в продакшен эту свою функциональщину

                                                                        Для справки — aws lambda работает с разными языками, поэтому выделение из них исключительно функциональных говорит о вашей исключительной предвзятости.
                                                                          +2
                                                                          Опять не совсем правильный подход. Эволюция ведь, как ни странно, всё ещё работает, то есть отбирает наиболее адаптированных к условиям обитания. Поэтому возникает интересный вопрос — а почему в нынешних условиях процветают индусы? А функциональные языки задвинуты куда-то в академии.

                                                                          А где это они процветают? Среднестатистический индус очень несчастный человек.


                                                                          Я читаю текст, который комментирую. А вы читаете? В моём сообщении было про неверно интерпретируемое название. И заметьте — это ещё цветочки. То есть что для полноты понимания вам стоит отказаться от всей документации и попробовать понимать чужой код по сигнатурам.

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


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

                                                                          То, что вы не поняли о чем идет речь не делает вам чести.

                                                                            +3
                                                                            > Поэтому возникает интересный вопрос — а почему в нынешних условиях процветают индусы? А функциональные языки задвинуты куда-то в академии.

                                                                            Не знал, что я работал в академии и «деливерил бизнес валью» используя только чистые функции и иммутабельные структуры.
                                                                              +5
                                                                              > Эволюция ведь, как ни странно, всё ещё работает
                                                                              > Поэтому возникает интересный вопрос — а почему в нынешних условиях процветают индусы?
                                                                              Потому что сейчас благоприятные условия. Почитай про ленивцев. Эволюция никак не противоречит существованию менее «эффективных» форм жизни.
                                                                        +9
                                                                        А решает, например, лёгкость освоения языка, ибо нужно много программистов, а где их взять?

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

                                                                          0
                                                                          Это только программиста-исполнителя может не волновать наём. Программист-архитектор может придумывать такие системы, для поддержки которых нужно много людей.
                                                                            +2

                                                                            Наивно полагать, что, наняв программиста, его не придётся ничему учить.

                                                                              0
                                                                              Конечно. Надо рассматривать наём, обучение и много других факторов при выборе технологии.
                                                                                0

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


                                                                                Ну и не забываем, что даже если разработчик "знает" технологию — его всё-равно придётся переучивать пользоваться ею правильно в 90%.

                                                                                  +3

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

                                                                          +3
                                                                          Именно поэтому гуглы и придумали го, ибо им нужны миллионы индусов, которые задёшево и быстро освоят новый язык. Представьте себе, сколько времени займёт освоение хаскеля обычным индусом (из тех самых миллионов). Представили? Вот поэтому го идёт в массы, а хаскель тихо курит бамбук в академической среде.

                                                                          А потом получим write-only код. Который выполняет заданную задачу. Но потом дешевле — его выкинуть и написать заново под новые условия, потому что индусы дешевые — пускай работают.

                                                                            –1
                                                                            А в чём вы здесь увидели проблему? В конкуренции вам со стороны индусов?
                                                                              +1

                                                                              Ну, есть же state-of-the-art. Какое-то удовольствие от работ, помимо финансового, а еще и от технических решений (хороших). А с индусами все просто… Надо их нанимать, ставить им задачи, получать результат и жить на дельту (все так делают — чем мы хуже?)

                                                                                0
                                                                                Проблема не в state-of-the-art. Проблема в необходимости зарабатывать. Вокруг этого построена жизнь, поэтому state-of-the-art можно интересоваться, но если в конкуренции с индусами вы начинаете проигрывать — время задуматься о земном.
                                                                                  +4

                                                                                  Так не начну, в этом-то и суть :) Пока индусу "некогда" я подучу функторы, монадки, и напишу за день то, за что он запросил месяц. Вот и вся история.

                                                                                    –2
                                                                                    Если бы всё было так шоколадно, то индусы давно бы сдулись и над всем миром парили бы сплошные хаскелисты. Но что-то пошло не так и почему-то над миром парят всё больше сплошные индусы.
                                                                                      +3

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




                                                                                      Ответьте на вопрос, вы себя считаете индусом в таком случае? Потому как я слышу явный посыл "го все как индусы, я создал"?

                                                                                        –3
                                                                                        Я себя индусом не считаю. Но я объективно смотрю на реальность и вижу в ней очень простой факт — миллионы индусов реально (и некоторые — много) зарабатывают на жизнь программированием. И при этом все эти необъятные орды конкурируют и с хаскелистами и со всеми остальными, и в конкуренции не проигрывают. А вот хаскелистов (функциональщиков, в общем виде) очень мало. Если поделить индусов на хаскелистов, получим по сути деление на ноль с бесконечно большим результатом в пользу индусов. И это всё — объективная реальность, с которой стоит считаться.
                                                                                          +1
                                                                                          И при этом все эти необъятные орды конкурируют и с хаскелистами и со всеми остальными

                                                                                          Это ведь аргумент не в вашу пользу :) Необъятные орды конкурируют да все выиграть никак не могут, хаскельные проекты как были, так и есть. Только в этом треде несколько человек отметились с этим, я еще общался с чуваками из biocad и bank of america — чет они не торопятся переписывать на гошечку всё.

                                                                                            –6
                                                                                            >> Необъятные орды конкурируют да все выиграть никак не могут, хаскельные проекты как были, так и есть.

                                                                                            Они давно выиграли — 99% софта написано ими.

                                                                                            Ну а ниша для угрюмых и агрессивных любителей ФП, понятное дело, всегда найдётся, ведь вся наука — как раз для них, там ценят агрессивных аутистов, повышают по службе умелых интриганов и т.д. и т.п.

                                                                                            На том мои комментарии на сегодня закончены, ибо угрюмые аутисты опустили мне карму ниже плинтуса и теперь могу только раз в час что-то отвечать. Но зато я рад, что получил очередное доказательство угрюмости и агрессивности ФП сообщества — вот так они давят любую критику, ну и потому выживают лишь в тепличных условиях, типа разного рода «научных» заведений, где общепринятым тоном ко всем посторонним является что-то вроде «да эти мудаки даже близко не способны понять всё то, что понимаем мы, а потому — в топку их всех».
                                                                                              +3

                                                                                              Я предполагаю, что Вы специально нарывались и вели себя… ну, эм, некорректно с переходом на личности. Собственно, и бросая тень на эти 99% народа. А теперь удивляетесь результату. Ну, ок. Что сказать.

                                                                                                +4
                                                                                                На том мои комментарии на сегодня закончены, ибо угрюмые аутисты опустили мне карму ниже плинтуса

                                                                                                Как говорится, дело было не в бобине, да?..

                                                                                              +2
                                                                                              Но я объективно смотрю на реальность и вижу в ней очень простой факт — миллионы индусов реально (и некоторые — много) зарабатывают на жизнь программированием.

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


                                                                                              А то мне кажется, что ты это понятие как-то непонятно расширяешь. Если речь просто о мейнстримном направлении, то внутри него будут свои индусы, "элита" и что-то посредине. Возьмём, например, джаву: можно фигачить посредственный код пользуясь фреймворками и не особо разбираясь как оно внутри. А можно разбираться хорошо или даже писать эти фреймворки. Людей из последней категории я ну никак не могу отнести к "индусам", а хорошо зарабатывать будут как раз они.

                                                                            +10

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

                                                                              +2
                                                                              А можете кратко пояснить почему невозможна?
                                                                                +12

                                                                                Если кратко, то там используется библиотечная функция traverse, сигнатура которой невыразима в системе типов Go.




                                                                                Если подробнее, то посмотрим как устроена функция traverse, используемая библиотекой:


                                                                                class (Functor t, Foldable t) => Traversable t where
                                                                                    traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

                                                                                Попробуем перенести на Go хотя бы некоторые из её зависимостей.


                                                                                Вот есть Functor:


                                                                                class Functor f where
                                                                                    fmap :: (a -> b) -> f a -> f b

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


                                                                                То есть на Go это могло бы выглядеть как-то так:


                                                                                type FunctorIntString interface {
                                                                                    fmap(fn func(int) string) []string
                                                                                }
                                                                                
                                                                                func fmap(input []int) (fn func(int) string) []string {
                                                                                    output := make([]string, len(input))
                                                                                    for i, v := range input {
                                                                                        output[i] = f(v)
                                                                                    }
                                                                                    return output
                                                                                }

                                                                                Только вместо int и string должны быть доступны любые типы данных. Без дженериков из go2 тут не обойтись.


                                                                                Зачем такое необходимо? Ну вот возьмём простейшую реализацию traverse (только не надо приводить этот пример как пример сложности языка — это вообще-то часть стандартной библиотеки и прикладной программист никогда не будет писать подобный код):


                                                                                instance Traversable [] where
                                                                                    traverse f = List.foldr cons_f (pure [])
                                                                                      where cons_f x ys = liftA2 (:) (f x) ys
                                                                                
                                                                                ...
                                                                                
                                                                                    liftA2 g y = (<*>) (fmap g y)

                                                                                Здесь x — это очередной элемент исходного списка. Над ним вызывается пользовательская функция f, после чего результат (являющийся любой структурой данных по выбору пользователя!) поэлементно прогоняется через функцию (:) (это конструктор списка).


                                                                                Зачем пользователю нужна возможность самому выбирать структуру данных? Ну вот, например, чтобы этой структурой данных как раз и оказался тип Concurrently из библиотеки async. Если нельзя выбрать свою структуру данных, значит, нельзя подставить туда Concurrently, а тогда и библиотеки не будет.


                                                                                Но на самом деле всё ещё хуже, и даже дженерики из go2 тут не помогут. Чтобы понять почему, смотрим дальше и замечаем конструкцию pure []. Какой у неё тип? Если принять тип f за Applicative F => a -> F b, то у pure [] тип будет F [b]. И если мы взглянем на сигнатуру pure...


                                                                                class Functor f => Applicative f where
                                                                                    pure :: a -> f a

                                                                                то мы вообще не увидим f среди входных параметров.


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

                                                                              +9
                                                                              Ну конечно же — го отстой, а хаскель — это круто.

                                                                              Да, потому что хаскель позволяет написать такую (обобщённую) библиотеку для конкурентности, другую (обобщённую) библиотеку для деревьев, а потом их (друг про друга ничего не знающих) легко совместить.


                                                                              И наконец, на С# код написан много быстрее, чем на хаскеле.

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

                                                                                0
                                                                                Как эта библиотека работает? Там можно настраивать количество воркеров?
                                                                                Если записей миллион, сколько сетевых соединений будет создано?
                                                                                  +1

                                                                                  Нет, нельзя, потому что она сама в себе всем рулит. Если вы хотите настраивать количество воркеров, то берете вместо этого расширение библиотеки async-pool, и там настраиваете всё, что надо.

                                                                                +4
                                                                                Но разве мы постоянно меняем структуры в программе?

                                                                                Думаю, если бы менять структуры было бы легко — вы бы делали это куда чаще.

                                                                                0

                                                                                Хаскель осмысленно стремится НЕ быть индустриальным языком, а оставаться языком академическим. Если вы хотите покататься на типосёрборде по типоволне, и при этом остаться с индустриальным (пригодным к продакшену) языком — используйте Rust.

                                                                                  0

                                                                                  А в чем эта академичность заключается, можно узнать? Я часто это слышал про хаскель, но вот на примере этой задачи (да и по отзывам знакомых) я не заметил каких-то проблем. Ну библиотек не 100 разных под каждую задачу на любой вкус и цвет, а только одна-две, то есть экосистема в этом плане бедновата, но всё, что нужно, вроде есть: жсоны парстить можно, в постгресы/монги ходить можно, веб-серверы поднимать можно, сваггер.жсон генерируется. Что еще нужно?


                                                                                  Если вы хотите покататься на типосёрборде по типоволне, и при этом остаться с индустриальным (пригодным к продакшену) языком — используйте Rust.

                                                                                  Раст в этом плане намного слабее. Когда завезут Const generics/GAT тогда еще что-то можно будет говорить. Но пока — увы.

                                                                                    0

                                                                                    Если вы говорите про "vs Java", то да, у хаскеля есть много (но не так много как у индустриальных языков, преимущественно в районе библиотек). Если вы говорите "vs C++/C", то ответ очевидный — GC и runtime. Я с трудом себе представляю программирование микроконтроллера на haskell, но вполне представляю — на Rust (речь не про extreme embedded, когда память в байтах считается, но про что-то умеренное, с сотнями кб).


                                                                                    С точки зрения же "vs Java" есть ещё одно "но" — иммутабельность. Да, можно встать на уши и написать без мутабельности, но на практике мутабельность полезна и удобна (особенно, если кто-то удерживает шаловливые ручки от комбинации мутабельности и sharing, привет ownership/borrow).


                                                                                    Понятно, что всегда можно откопать фичу, которой нет в языке и говорить, что без этого жизнь невозможна. Я, вон, до сих пор страдаю, что, казалось, бы, в таком богатом языке как Rust, и до сих пор нет питонового yield для конструирования замыканий. В реальности, для защиты проекта от глупых ошибок и принуждения программиста к clarity того, что он пытается написать, Rust более чем хорош. Особенно, если компилятор бъёт по ручкам и не позволяет сделать Странное из-за нарушения lifetimes.

                                                                                      +1
                                                                                      Если вы говорите про "vs Java", то да, у хаскеля есть много (но не так много как у индустриальных языков, преимущественно в районе библиотек). Если вы говорите "vs C++/C", то ответ очевидный — GC и runtime. Я с трудом себе представляю программирование микроконтроллера на haskell, но вполне представляю — на Rust (речь не про extreme embedded, когда память в байтах считается, но про что-то умеренное, с сотнями кб).

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


                                                                                      Для МК конечно же никакие языки с ГЦ не годятся, берите раст, скорее всего не ошибетесь.


                                                                                      С точки зрения же "vs Java" есть ещё одно "но" — иммутабельность. Да, можно встать на уши и написать без мутабельности, но на практике мутабельность полезна и удобна (особенно, если кто-то удерживает шаловливые ручки от комбинации мутабельности и sharing, привет ownership/borrow).

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


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

                                                                                      Ну, мне очень не хватает асинк-авейтов (я попробовал ночную альфа-верси гипера, и по документацию построить рабочий пример не очень получилось), да и копи-паста в той же стандартной библиотеке расстраивает. Реализуй они do-нотацию изначально (хотя бы Ad-hoc, как в C#), то ушла бы куча копипасты из стд реализаций Option/Result/Iterator/Future/..., не нужно было бы делать отдельно ?, async/await и так далее.


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

                                                                                        0

                                                                                        База данных, кеш — это разделяемая мутабельность. Пока приложение может делегировать всю разделяемую мутабельность в удаленную базу данных и использовать для кеширования только удаленные сервисы типа Memcached/Redis, все хорошо. Но иногда надо подтянуть изменяемое состояние прямо в память приложения.


                                                                                        Распространенность (и оправданность) этого "иногда", скорее всего, сужается, с распространением микросервисов, serverless, и быстрых in-memory баз данных.


                                                                                        С другой стороны, разделяемая мутабельность в Haskell есть, но, возможно, с достаточно плохой конкурентностью. Впрочем, то же самое можно было сказать про Go вплоть до версии 1.9, и не то что бы люди очень сильно страдали без этого инструмента. Класс задач, где хорошая конкурентность разделяемой мутабельности критична, очень узок.

                                                                                          +4
                                                                                          С другой стороны, разделяемая мутабельность в Haskell есть, но, возможно, с достаточно плохой конкурентностью.

                                                                                          Конкурентность, кстати, прекрасная. STM отлично скейлится (ну, пока у вас транзакции не наступают на пятки друг другу и не уходят в ретраи, но это верно в любом языке), более низкоуровневые и голые всякие там MVar'ы — так вообще.


                                                                                          И кеши тоже. Не так давно заворачивал вызовы какого-то микросервиса в кеш, так там латентность кеша оказалась не особо отличима от плюсового кода (таймер больше неопределённости вносил).


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

                                                                                            +1

                                                                                            Спасибо за замечание. Я сейчас понял что stm-containers использует hash trie, это решение действительно должно хорошо скалироваться.


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

                                                                                            А должна быть?) Поменять одну и ту жу ячейку памяти из двух потоков одновременно это чистая гонка.

                                                                                          0
                                                                                          Ну, мне очень не хватает асинк-авейтов (я попробовал ночную альфа-верси гипера, и по документацию построить рабочий пример не очень получилось), да и копи-паста в той же стандартной библиотеке расстраивает. Реализуй они do-нотацию изначально (хотя бы Ad-hoc, как в C#), то ушла бы куча копипасты из стд реализаций Option/Result/Iterator/Future/..., не нужно было бы делать отдельно ?, async/await и так далее.

                                                                                          async/await, Option, Iteration — звучат как фичи которые хорошо сделаны в Kotlin. Смотрели в сторону Kotlin/Native?

                                                                                            +1

                                                                                            Котлин мне совсем не интересен, потому что это просто Better Java. Он уменьшает всякий бойлерплейт всяким сахаром, но принципиально ничего не меняет, сахара мне и в C# хватает, неделю назад еще чутка подсыпали :). Вот Scala другое дело. У меня в планах использовать Haskell для изучения концепций ФП и потом тащить скалу в прод. Но это пока так, прикидки на будущее.

                                                                                              +2
                                                                                              У меня в планах использовать Haskell для изучения концепций ФП и потом тащить скалу в прод.

                                                                                              Лучше тащите эту, если вам скала нужна ради интеропа с остальной экосистемой JVM.

                                                                                                +2

                                                                                                Или Clojure :)

                                                                                                  0

                                                                                                  У скалы вроде своего тоже хорошего много. Я слышал много интересного про Monix/ZIO/cats/...

                                                                                            +1
                                                                                            Да, можно встать на уши и написать без мутабельности, но на практике мутабельность полезна и удобна (особенно, если кто-то удерживает шаловливые ручки от комбинации мутабельности и sharing, привет ownership/borrow).

                                                                                            Видимо, тут у каждого свой опыт.


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

                                                                                              +1

                                                                                              "Числодробильность" задачи это не бинарное свойство, а градиент. Есть задачи которые можно назвать числодробилками и с которыми справятся и Rust, и Java, со скоростью не ниже чем С/С++ (в отдельных случаях — выше).

                                                                                                +2

                                                                                                Безусловно, это спектр.


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

                                                                                                  –1

                                                                                                  Есть еще одна подобласть — написание баз данных. Как раз-таки не попадает ни туда, ни сюда. Я думаю, сейчас Rust — это наиболее оптимальный выбор, особенно когда туда завезут рантайм-кодогенерацию.

                                                                                                    +1

                                                                                                    Ну вот, теперь мне жаль, что БД писать не приходилось.


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

                                                                                            +1
                                                                                            дело в том, что ИЗНАЧАЛЬНО, Хаскель действительно создавался как академический.
                                                                                            Целью было создать функциональный ленивый и чистый язык.
                                                                                            Однако в самом же начале оказалось, что лениво нельзя написать print a, чисто же нельзя написать
                                                                                            a == b, а так же одновременно 2 + 2 и 2.0 + 2.0.
                                                                                            Но Хаскель смог разрешить эти сложности и выстрелил как хороший язык. Сейчас он вполне себе индустриальный. Этим прежде всего занимаются Industrial Haskell Group и Commercial Haskell Group
                                                                                            +3

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

                                                                                              0
                                                                                              Разве академичность, то есть корректность и осмысленность всех элементов языка мешает продакшену? По-моему, только помогает.
                                                                                                0

                                                                                                Язык — это не только bnf и лямбды, но и ещё масса lore. Из простейшего — нет поддержки приватных репозиториев артефактов (приватного hackage), нормального offline-режима (даже gradle разродился!). Библиотеки тоже не совсем enterprise-grade, особенно по мере приближения к SOAP'у. Я сходу ткнулся — gtk всё ещё на второй версии только.


                                                                                                Нет блокировки вендореных версий (Cargo.lock), про удовольствие от TH можете сами рассказать.

                                                                                                  +1

                                                                                                  Хорошие замечания, спасибо.

                                                                                                    +2
                                                                                                    Поддержка приватных репозиториев технически есть, через конфиг stack/cabal. Честно говоря, не знаю, хорошо ли она работает, кажется, все пользуются приватным монорепозиторием.

                                                                                                    gtk3 и gi-gtk на 3 версии давно.

                                                                                                    > Библиотеки тоже не совсем enterprise-grade

                                                                                                    Это у всех так, кроме Явы, наверно.

                                                                                                    Блокировки есть (cabal freeze, например), не знаю, как вы искали.

                                                                                                    От TH одно удовольствие, да, если не кросс-компилировать.
                                                                                                      +2
                                                                                                      gtk всё ещё на второй версии только

                                                                                                      вот только не надо вот этого. https://hackage.haskell.org/package/gi-gtk живёт и здравствует, а тот gtk, который нашли вы, оттого и заглох, что писался вручную

                                                                                                        +3
                                                                                                        Из простейшего — нет поддержки приватных репозиториев артефактов (приватного hackage)

                                                                                                        Пойду расскажу коллегам с прошлой работы, которые приватный hackage поднимали.


                                                                                                        Ну и да, никто не мешает вам закосить под модного программиста на go и в stack.yaml в extra-deps указывать пути к вашему гиту.


                                                                                                        нормального offline-режима

                                                                                                        Это что такое?


                                                                                                        Библиотеки тоже не совсем enterprise-grade, особенно по мере приближения к SOAP'у

                                                                                                        Нет абстрактных фабрик синглтонов? Зато есть просто синглтоны.


                                                                                                        Нет блокировки вендореных версий (Cargo.lock)

                                                                                                        stack-снапшоты не оно?


                                                                                                        про удовольствие от TH можете сами рассказать

                                                                                                        Для тех задач, которые он решает — отличное решение, претензий нет.

                                                                                                      +1
                                                                                                      Язык не может чего-то хотеть или к чему-то стремиться. Это может сообщество, но про него говорить тяжело — внутри все хотят разного. В том числе — тащить хаскель в сторону тупого индустриального языка для повседневного оперденестроения.
                                                                                                      +4
                                                                                                      Была взята задача, удобная для ФП. Автор тяготеет к ФП, имеет опыт с Хаскелем и C# и в первый раз видит 100% императивный Go. Результат очевиден.

                                                                                                      А вот на вопрос опроса,
                                                                                                      >Какой язык показал себя лучше?
                                                                                                      По моему вывод из этой статьи — C# — он позволяет писать и ФП, как показано, и
                                                                                                      императивно, и быстро и беспроблемно. Можно бы добавить в голосовалку.

                                                                                                      PS. Для этой задачи, вероятно самым простым был бы JS
                                                                                                        +6

                                                                                                        Я взял задачу, которая мне показалось одновременно достаточно простой, но при этом интересной, и затрагивающий сильные стороны одной парадигмы (работа с деревьями) и другой (удобная асинхронность и гринтреды). И мне результат не был очевиден. Я думал я получу 100 строк на го раза в 2 быстрее, чем на хаскелле, и написал бы "ну, тут 100 строк кода, но зато смотрите как быстро. При изменении требований можно выкинуть и за еще 10 минут получить 100 новых строк с нужными правками", а вышло немного по-другому.


                                                                                                        По моему вывод из этой статьи — C# — он позволяет писать и ФП, как показано, и императивно, и быстро и беспроблемно. Можно бы добавить в голосовалку.

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

                                                                                                          +1

                                                                                                          Так. Работа с деревьями удобна в ФП. Конкурентность и параллельность — это тоже удобно в ФП. А для чего же тогда удобно ИП, кроме того, чтобы быструю сортировку написать?

                                                                                                            +1

                                                                                                            Вычислительная математика (перемножения матриц, решения СЛАУ), игры, компьютерная графика, обработка сигналов, вычисления в ограниченной памяти.
                                                                                                            Тут всё же надо признать, что мутабельность важна, но с точки зрения заблаговременной защиты от ошибок, мутабельность не должна быть по умолчанию. (Вот как в современных языках, вроде Раста или Хаскеля)

                                                                                                              +2

                                                                                                              Вы шутите? Вы перечислили именно те области, где голанг отсутствует и никогда не выстрелит.
                                                                                                              Его удел — это тулинги и сетевые сервисы. Все. Никто не будет на нем писать большие проекты вроде комп. игр, тем более, что там есть свои уже устоявшиеся подходы и фреймворки. Голангу также заказана дорога в ускорение на гпу.
                                                                                                              Я уж не говорю, что Golang — пример фреймворков с GC. Поэтому это точно не про быстродействие (по крайней мере, не больше — чем java).

                                                                                                                0

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

                                                                                                            +3

                                                                                                            Перечитывал комментарии, и решил подчеркнуть один момент:


                                                                                                            Автор тяготеет к ФП, имеет опыт с Хаскелем и C# и в первый раз видит 100% императивный Go. Результат очевиден.

                                                                                                            Мой опыт ФП ограничивается написанием LINQ-запросов в шарпе и реализации некоторых задачек из книжки по теории категорий. Хаскель, как и го, я видел первый раз в жизни (хотя и слышал о них разную априорую информацию вроде строго компилятора Haskell и неконфигурируемого gofmt), и считаю что они были в достаточно равных условиях. Особенно, если учесть, что на го мы писали уже имея на руках решение на Haskell, и немного лучше представляя себе пространство решений.

                                                                                                              –3
                                                                                                              Ну насколько я помню по комментариям, достаточно устойчивый интерес к Хаскелю подразумевает предварительную, возможно только теоретическую, подготовку.
                                                                                                              А вот на Го наоборот, скорее похоже на попытку решения напролом, не используя типовые решения языка. Да и решается задача скорее фронтенда, в то время как Го — бекэндный язык.
                                                                                                              Собственно, некоторая польза от статьи есть — показано, что можно и на ходу х сломать (aka Go'ing=).
                                                                                                              Вот только непонятно, чего же барину не хватает в С#? Хаскель, Раст и Го ему не замена.
                                                                                                                +1
                                                                                                                Да и решается задача скорее фронтенда, в то время как Го — бекэндный язык

                                                                                                                Давайте еще задачи бить на фронтовые и бекендовые? Что это значит в Вашем понимании? Сервис, который отдает данные в виде единого блоба на клиент — тоже вполне себе бекенд. Ну, и сила Java/Haskell etc. в том, что они вполне позволяют ваять stand-alone приложение (но это тоже вне разделения на front / back).

                                                                                                                  +1
                                                                                                                  Ну давайте. Как, если подумать, бьют например вакансии на фронт и енд. Именно эта задача — сформулирована под определенные условия.

                                                                                                                  А сила ява во фронте и хаскеля это такая… абстрактная.
                                                                                                                    +3

                                                                                                                    В моем окружении, по крайней мере, в применении к веб-разработке — фронт — это то, что у пользователя в браузере выполняется на JS...

                                                                                                                      0
                                                                                                                      В моём окружении половина пользовательского интерфейса работает на сервере. А бэкенд — это то, что пользователь совсем не видит.
                                                                                                                        +1

                                                                                                                        ну, если так рассуждать, то Java, python anything else на "фронте" — т.е. то что выполняется на сервере, но отдает данные юзеру (или приложению у юзера — будь то веб-приложение в браузере, андроид или мак). И в таком ключе Java, python anything else тоже прекрасно работают ) Вы сами-то на чем программируете?

                                                                                                                          0
                                                                                                                          Да, конечно. Что угодно можно использовать на фронте, но с разной пользой. И даже на клаентсайде Haskell компилируют в JS или в WASM, может быть, и Go тоже.

                                                                                                                          На фронтентде обычно используют то, что имеет готовые веб-фрэймворки и генерить HTML, а на бэкенде — то, что умеет дробить числа и предсказуемо расходовать ресурсы.

                                                                                                                          Но это деление очень нечёткое, обсуждаемую задачу нельзя отнести только к одной из этих областей.
                                                                                                                            +3
                                                                                                                            Я пишу, в основном, на Хаскеле, но не веб, а утилиты и ядро ОС. Но какое это имеет значение?
                                                                                                                    +4
                                                                                                                    Ну насколько я помню по комментариям, достаточно устойчивый интерес к Хаскелю подразумевает предварительную, возможно только теоретическую, подготовку.

                                                                                                                    Подготовку какого плана? Ну знаю я то такое функтор, это в рамках раста вот такой вот трейт


                                                                                                                    pub trait Functor<_> {
                                                                                                                       fn map<T, U, F>(self: Self<T>, map: F) -> Self<U> 
                                                                                                                       where F: FnOnce(T) -> U;
                                                                                                                    }

                                                                                                                    и реализуем его например для типа Option:


                                                                                                                    impl Functor<_> for Option<_> {
                                                                                                                       fn map<T, U, F>(self: Self<T>, map: F) -> Self<U> 
                                                                                                                       where F: FnOnce(T) -> U {
                                                                                                                          match self {
                                                                                                                             Some(x) => Some(map(x)),
                                                                                                                             None => None
                                                                                                                          }
                                                                                                                       }
                                                                                                                    }

                                                                                                                    Ну так эта функция в рамках стд либы живет, просто к трейту не привязана, и теперь и вы тоже знаете, что это. А теперь скажите, насколько оно релевантно статье и какие преимущества это дало? Насколько я помню по статье, вопросы в гугл, там ничего не было "как работать с функторами в хаскелле", там было "смаппить одно дерево на другое", и попытка взять первое решение со stackoverflow, а работа с map на массивах/слайсах/… у любого разработчика включая даже низкоуровневые типа С++ в крови. Мне кажется, это не самая сложная задача для умных людей, которыми программисты ИМХО являются.

                                                                                                                      0
                                                                                                                      Эмм. Этот пост абсолютно нерелевантен ни топику ни моему вопросу выше o_O
                                                                                                                        +1

                                                                                                                        В чем подготовка заключается можно узнать? В том что я чуть-чуть в математике разбирался (топологический раздел алгебры по сути)?

                                                                                                                          –1
                                                                                                                          В знании синтаксиса и понятий Хаскеля. И в стремлении использовать ФП куда надо и куда не надо.
                                                                                                                          А ответ на вопрос таки будет?
                                                                                                                          Вот только непонятно, чего же барину не хватает в С#? Хаскель, Раст и Го ему не замена.
                                                                                                                            +5
                                                                                                                            В знании синтаксиса и понятий Хаскеля.

                                                                                                                            Так я их и не знал.


                                                                                                                            И в стремлении использовать ФП куда надо и куда не надо.

                                                                                                                            А куда не надо?


                                                                                                                            А ответ на вопрос таки будет?

                                                                                                                            1. мне не хватает нормальной работы с нуллябельностью. Как я уже говорил, в хаскелле я могу написать map2 (+) Just 5 Just 10 и получить Just 15. В сишарпе мне придется писать a.HasValue && b.HasValue ? a.Value + b.Value : null. Это часто неудобно. Любые не-инстанс методы через null-propagation прокинуть нельзя. В примере это оператор +, но примеров намного больше
                                                                                                                            2. Отдельно хочется сказать привет тому, что типы вообще нуллябельные. Идея тащить в язык Option плоха по многим причинам, поэтому его и не будет. Nullable reference types фича ничего не изменит.
                                                                                                                            3. Мне не хватает гибкости решений, в частности интерфейсы не могут содержать статических членов. Нахрена это нужно? Ну например для такого случая


                                                                                                                              interface IParseable<T> 
                                                                                                                              { 
                                                                                                                                  static T Parse(string value)  
                                                                                                                              }
                                                                                                                              
                                                                                                                              T ReadFromConsole<T>() where T : Parseable<T> => T.Parse(Console.ReadLine();

                                                                                                                            4. Стандартная ирерхия коллекций полный шлак (Array<T> наследуется от IList<T>, но каждый второй метод бросает NotSupportedException, это вообще что такое?), а из-за того как работают интерфейсы в ООП это не исправить. С тайпклассами старую иерархию можно было бы задеприкейтить и сделать новую, не сломав никакого кода.
                                                                                                                            5. Опять же из-за тайпклассов нельзя выразить вещи вроде траверсаблов, а я вроде показал как с ними удобно работать. LINQ был прорывом, а он ведь работает только с IEnumerable. С траверсаблами он был бы в разы круче.
                                                                                                                            6. Нет ADT. Это вообще ужас полный. Эмулировать на новом свитче можно, но неудобно
                                                                                                                            7. Эксепшны вместо ошибок. Смотря на сигнатуру функции нельзя понять, всегда ли она успешно завершается или нет. Если сигнатура вдруг поменялась, компилятор ничего не подскажет и программа будет падать в рандомных местах. С другой стороны в try catch заворачивается то, что никогда упасть не может. В итоге код разбухает, потому что try catch очень много места занимает, и люди его лепят просто на всякий случай чтобы не падало.
                                                                                                                            8. Общий случай предыдущего пункта: из-за того что функции грязные никогда не знаешь, не лезет ли безобидный метод в статический кэш, сеть или еще куда.
                                                                                                                            9. ...

                                                                                                                            ...


                                                                                                                            Я могу продолжать еще долго. Вам хватит причин, или нужны еще?

                                                                                                                              0

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


                                                                                                                              Да и исключения тоже не проблема. В любом нетотальном языке есть возможность не вернуть никакого значения из функции, и Раст с Хаскелем — не исключения.


                                                                                                                              Вот пунктов 3 и 5 правда порой не хватает.

                                                                                                                                +2
                                                                                                                                Ну, нулы занимают всё же не так много места. Как правило, операции над ними всё равно сводятся либо к арифметическим, либо к вызовам методов.

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


                                                                                                                                var state = route == null
                                                                                                                                            ? null
                                                                                                                                            : new RoutePointActorState(route.HubInfo.HubId);

                                                                                                                                или вот


                                                                                                                                if (createdOn.HasValue)
                                                                                                                                    httpClient.DefaultRequestHeaders.TryAddWithoutValidation(
                                                                                                                                        "CreatedOn",
                                                                                                                                        createdOn.Value.ToString("yyyy-MM-ddTHH\\:mm\\:ss.fffffffZ"));

                                                                                                                                И таких случаев очень много, учитывая, что конструктор нельзя как method group использовать, что вывод типов на нем не работает, и тд и тп.


                                                                                                                                Да и исключения тоже не проблема. В любом нетотальном языке есть возможность не вернуть никакого значения из функции, и Раст с Хаскелем — не исключения.

                                                                                                                                Вопрос в идеологии языка. В хаскелле я могу условно линтер настроить или на ревью не пропустить код который плохо себя ведет. В сишарпе же я бы хотел, но не могу, потому что все библиотеки так работают. О чем говорить, когда отмена таски в языке реализуется через проброс эксепшна) А потом лови, то ли TaskCancelledException, то ли OperationCancelledException, то ли AggregateException ex when (ex.InnerException is TaskCancelledException),… Ну это же ужас просто.

                                                                                                                                  +1
                                                                                                                                  Да ладно вам. Вот я щас открыл случайный микросервис и нашел там такое

                                                                                                                                  Это один раз встречается или в каждом микросервисе / в каждом методе? В первом случае пофиг, во втором случае надо просто метод-расширение сделать.


                                                                                                                                  А потом лови, то ли TaskCancelledException, то ли OperationCancelledException, то ли AggregateException

                                                                                                                                  Ловить надо, конечно же, OperationCanceledException — ведь TaskCancelledException его наследник. А внутри AggregateException никакого TaskCancelledException оказываться не должно, если оно там вдруг есть — это уже где-то в коде баг.

                                                                                                                                    0
                                                                                                                                    Это один раз встречается или в каждом микросервисе / в каждом методе? В первом случае пофиг, во втором случае надо просто метод-расширение сделать.

                                                                                                                                    Ну я не берусь судить, но я думаю на 500-1000 строк кода оно встречается хотя бы раз. Выносить в метод смысла нет, потому что каждый раз используется в единственном месте, где-то RoutePointActorState, а где-то какой-нибудь RoutePointsState


                                                                                                                                    Ловить надо, конечно же, OperationCanceledException — ведь TaskCancelledException его наследник. А внутри AggregateException никакого TaskCancelledException оказываться не должно, если оно там вдруг есть — это уже где-то в коде баг.

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

                                                                                                                                      0
                                                                                                                                      думаю на 500-1000 строк кода оно встречается хотя бы раз

                                                                                                                                      Так это совсем не напрягает. Я же всё-таки не с телефона программу пишу...


                                                                                                                                      Нет, мне нравится когда Option/Maybe получает общеязыковую поддержку монад. Но именно как побочный эффект существования этой самой поддержки, в качестве основной причины существования монад в языке Option/Maybe не подходит никак.


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

                                                                                                                                      Но тут только Идрис поможет.

                                                                                                                                        0
                                                                                                                                        Так это совсем не напрягает. Я же всё-таки не с телефона программу пишу...

                                                                                                                                        Ну, я не говорю что это киллерфича. Просто неудобство. Киллерфича это расширяемость снаружи, хкт и вот это все.


                                                                                                                                        Но тут только Идрис поможет.

                                                                                                                                        Да не обязательно, в расте T -> Result<T> это ломающее изменение.

                                                                                                                                          0

                                                                                                                                          Так и в C# возвращаемый тип поменять — ломающее изменение. Но прямой аналог исключения — паника. Добавление паники в метод — не ломающее изменение.

                                                                                                                                            +3

                                                                                                                                            Ну я об этом и говорю. Ошибки которые производит функция не являются частью сигнатуры (результата) метода. шарпы не разделяют по сути ошибки и паники, и то и то работает через эксепшны. Я не встречал распространенных библиотек которые бы использовали что-то кроме исключений для обработки ошибок, даже ожидаемых. Пример с OperationCancelledException показателен. Никому в расте в голову бы не пришло делать это паникой. Поэтому если мы раньше не могли отменять, а теперь отменяем, то это ломает код и мы должны прокинуть обработку отмены. А в сишарпе найти все места где нужно было бы обрабатывать это намного более нетривиально.

                                                                                                                                              0

                                                                                                                                              Лучше го вспомните. Как я понимаю, там либо паника (и приплыли), либо передача кодов ошибок по всему стеку вызова (а-ля как было в Си и раннем С++). Лучше уж эксепшены — ей Богу.

                                                                                                                                                +3

                                                                                                                                                Ну так и в расте то же самое. Но у раста есть два больших преимущества:


                                                                                                                                                1. У вас есть first-class АДТ, а не просто "пара результатов из функции". Разница очень простая, растовый Result можно сохранить в переменную, а вызов функции с несколькими результатами в го — нельзя.
                                                                                                                                                2. Чтобы передавать по всему стеку вызова было не больно сделали оператор ?.

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


                                                                                                                                                Вот, можно поиграться с примером: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=46b43cd318ffb17f0b6b92f3132123cc. В данном случае ошибка (в нашем примере, число больше десяти) прокидывается по всему стеку вызовов main -> d -> c -> b -> a, но это не накладывает никаких неудобств.

                                                                                                                                                  0
                                                                                                                                                  А обработка это ошибки как будет выглядеть?