Как я писал в предисловии предыдущей статьи, я нахожусь в поисках языка, в котором я мог бы писать поменьше, а безопасности иметь побольше. Моим основным языком программирования всегда был 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, потому что у вас нет возможности конкурировать с разработчиками стандартной библиотеки: то что можно делать им, простым смертным — не получится.


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



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

Комментарии (233)


  1. trolley813
    30.09.2019 10:06
    +2

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


    1. PsyHaSTe Автор
      30.09.2019 10:22
      +1

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


    1. zawodskoj
      30.09.2019 14:57
      +1

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

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


      1. PsyHaSTe Автор
        30.09.2019 15:06

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


  1. nlinker
    30.09.2019 10:17
    +1

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


    1. PsyHaSTe Автор
      30.09.2019 10:25

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


  1. zawodskoj
    30.09.2019 11:10
    +3

    На самом деле, ситуация страшная. Го, вроде как, позиционируется как простой и быстрый в освоении язык, но, когда человек с опытом двух огромных (Rust, C#) языков не может справиться с простой задачей на 20 минут и сталкивается с проблемами чуть ли не с первых строк, к чему придет новичок, для кого Го станет первым языком? Какое качество продукта в результате выйдет?
    Да и зачем тогда нужен Го, если со своим назначением (простота и легкость обучения) он справиться не может, а другие языки решают те же задачи лучше и с меньшим количеством проблем?


    1. Siemargl
      30.09.2019 15:28
      +1

      Слишком много обобщений на слишком маленькой базе для экстраполяции.


    1. cblp
      30.09.2019 15:49
      +2

      Простота тяжела, лёгкость сложна. Всё это было у Рича Хикки (Simple made easy).


  1. TSR2013
    30.09.2019 11:38

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


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

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


    1. 0xd34df00d
      30.09.2019 16:28

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

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


  1. vintage
    30.09.2019 12:25

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


    1. PsyHaSTe Автор
      30.09.2019 14:19
      +1

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


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


      1. MooNDeaR
        30.09.2019 15:12

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


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


        1. PsyHaSTe Автор
          30.09.2019 15:16
          +1

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


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


      1. vintage
        30.09.2019 18:36

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

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


        1. PsyHaSTe Автор
          30.09.2019 18:49

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


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


          1. vintage
            30.09.2019 19:02

            А если хотим?


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


            1. PsyHaSTe Автор
              30.09.2019 22:04
              +1

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


  1. fillpackart
    30.09.2019 13:53

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


  1. user_man
    30.09.2019 14:04
    -1

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

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

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

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

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

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

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


    1. PsyHaSTe Автор
      30.09.2019 14:25
      +1

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

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


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

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


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

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


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

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


      1. user_man
        30.09.2019 14:48

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

        Я же специально выделил часть вашего текста:

        я точно знаю что происходит на каждой строчке программы


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

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

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

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

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


        1. PsyHaSTe Автор
          30.09.2019 15:03
          +2

          Я же специально выделил часть вашего текста:

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


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

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


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

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


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

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


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


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

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


          1. Evir
            01.10.2019 10:28

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


            1. PsyHaSTe Автор
              01.10.2019 10:38
              +3

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


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


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


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

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


              1. mayorovp
                01.10.2019 10:55

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

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


                1. PsyHaSTe Автор
                  01.10.2019 11:10
                  +1

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


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

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


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

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


              1. Deosis
                01.10.2019 11:58

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


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


                1. PsyHaSTe Автор
                  01.10.2019 12:12
                  +3

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


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


                1. gecube
                  01.10.2019 12:49
                  +1

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


                1. mayorovp
                  01.10.2019 12:52
                  +1

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


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


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


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


                  1. 0xd34df00d
                    01.10.2019 16:06
                    +1

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


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


                    1. mayorovp
                      01.10.2019 16:15

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


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


                      1. 0xd34df00d
                        01.10.2019 16:28

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

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


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

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


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


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


                        1. mayorovp
                          01.10.2019 16:51

                          Вам просто неоткуда взять a, кроме как из парсера-аргумента к fmap.

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


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


                          1. 0xd34df00d
                            01.10.2019 16:57

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


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


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

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


                            1. mayorovp
                              01.10.2019 17:22

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


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


                              1. 0xd34df00d
                                01.10.2019 17:43

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


                                1. mayorovp
                                  01.10.2019 18:52

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


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


                                  1. PsyHaSTe Автор
                                    01.10.2019 18:58

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


                                    1. mayorovp
                                      01.10.2019 19:03

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


                                      1. 0xd34df00d
                                        01.10.2019 19:04
                                        +1

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


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


                                  1. 0xd34df00d
                                    01.10.2019 19:04
                                    +1

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


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


                1. 0xd34df00d
                  01.10.2019 16:02
                  +1

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


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


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

                  но то такое.


              1. Evir
                01.10.2019 19:07

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

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


                1. PsyHaSTe Автор
                  02.10.2019 06:58
                  +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 дня легко сэкономил и я выправился, но сидеть и улучшать код времени не было. Работает, и хорошо :) Но вообще конечно надо бы все эти лесенки убрать.


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


                  1. Evir
                    02.10.2019 13:11

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

                    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…


                    1. PsyHaSTe Автор
                      02.10.2019 13:47
                      +1

                      смотрите, точка между функциями означает композицию. 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 )

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




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


                      1. mayorovp
                        02.10.2019 14:01

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


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


                        1. PsyHaSTe Автор
                          02.10.2019 15:28

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



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


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


                      1. LEXA_JA
                        02.10.2019 15:31
                        +1

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


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


                        1. PsyHaSTe Автор
                          02.10.2019 15:37

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


            1. cblp
              01.10.2019 10:40
              +3

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


              1. Evir
                01.10.2019 19:21

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


                1. 0xd34df00d
                  01.10.2019 19:23

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

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


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

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


                1. cblp
                  02.10.2019 09:10
                  +2

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


                1. cblp
                  02.10.2019 09:15
                  +2

                  Всё отлично читается и по кусочкам. Я думаю, вы привыкли к Алгол-С-подобному синтаксису, а к ISWYM-подобному ещё не привыкли. Если поработаете с Хаскелем некоторое время, научитесь и ему.


          1. user_man
            01.10.2019 15:26
            -4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


            1. gecube
              01.10.2019 16:05

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


            1. PsyHaSTe Автор
              01.10.2019 16:19
              +3

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

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


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

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


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


              1. 0xd34df00d
                01.10.2019 16:34
                +2

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

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


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

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


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

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


              1. worldmind
                01.10.2019 18:54
                +1

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


                1. develop7
                  01.10.2019 19:19

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


                  1. worldmind
                    01.10.2019 19:24

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


                    1. develop7
                      02.10.2019 01:33

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


                      1. worldmind
                        02.10.2019 09:17

                        Применим и применяется это разщные вещи.


              1. user_man
                02.10.2019 17:37
                -1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


                1. mayorovp
                  02.10.2019 17:45

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

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


                  1. user_man
                    03.10.2019 11:53
                    -4

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


                    1. PsyHaSTe Автор
                      03.10.2019 13:37
                      +2

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


                1. PsyHaSTe Автор
                  02.10.2019 20:46
                  +1

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

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


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

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


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

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


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


                  1. user_man
                    03.10.2019 12:35
                    -5

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


                1. 0xd34df00d
                  02.10.2019 22:09
                  +2

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

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


                  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.


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


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

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


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

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


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

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


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


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

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


                  1. Cerberuser
                    03.10.2019 06:05

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

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


                    1. mayorovp
                      03.10.2019 06:40
                      +1

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


            1. 0xd34df00d
              01.10.2019 16:20
              +4

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


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

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


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

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


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

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


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

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


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


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

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


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

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


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


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

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


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

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


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

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


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

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


            1. kolpeex
              01.10.2019 16:39
              +3

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

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

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


              1. user_man
                02.10.2019 16:51

                >> вот только это увеличивает вероятность багов и более того распространяет эту заразу в клиентский код

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

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

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

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

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


                1. PsyHaSTe Автор
                  02.10.2019 17:15

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

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


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

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


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

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


                1. kolpeex
                  03.10.2019 11:30
                  +3

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

                  Не знал, что я работал в академии и «деливерил бизнес валью» используя только чистые функции и иммутабельные структуры.


                1. kolpeex
                  03.10.2019 14:51
                  +3

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


        1. nexmean
          30.09.2019 15:08
          +1

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

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


          1. cblp
            30.09.2019 15:56

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


            1. vintage
              30.09.2019 18:48
              +1

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


              1. cblp
                01.10.2019 12:25

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


                1. vintage
                  01.10.2019 13:40

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


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


                  1. babylon
                    01.10.2019 14:00
                    +1

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


        1. gecube
          30.09.2019 19:27

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

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


          1. user_man
            01.10.2019 15:39

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


            1. gecube
              01.10.2019 16:01

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


              1. user_man
                01.10.2019 16:16

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


                1. PsyHaSTe Автор
                  01.10.2019 16:21
                  +2

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


                  1. user_man
                    02.10.2019 16:39

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


                    1. PsyHaSTe Автор
                      02.10.2019 17:16
                      +1

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




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


                      1. user_man
                        03.10.2019 12:41
                        -3

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


                        1. PsyHaSTe Автор
                          03.10.2019 13:43
                          +1

                          И при этом все эти необъятные орды конкурируют и с хаскелистами и со всеми остальными

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


                          1. user_man
                            04.10.2019 13:23
                            -3

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

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

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

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


                            1. gecube
                              04.10.2019 13:28
                              +2

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


                            1. PsyHaSTe Автор
                              04.10.2019 13:37
                              +2

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

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


    1. mayorovp
      30.09.2019 14:54
      +3

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


      1. worldmind
        30.09.2019 15:12

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


        1. mayorovp
          30.09.2019 16:25
          +3

          Если кратко, то там используется библиотечная функция 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 первым делом пытается построить выбранную пользователем структуру данных для одного элемента, причём в этой структуре будет храниться не выбранный пользователем тип данных, а другой! А у нас ещё даже нет ни одного экземпляра, у которого можно было бы через интерфейс вызвать хоть один метод.


    1. 0xd34df00d
      30.09.2019 16:31
      +3

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

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


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

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


      1. anton19286
        02.10.2019 06:37

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


        1. PsyHaSTe Автор
          02.10.2019 06:59
          +1

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


    1. vintage
      30.09.2019 18:42

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

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


  1. amarao
    30.09.2019 14:14

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


    1. PsyHaSTe Автор
      30.09.2019 14:32
      +1

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


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

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


      1. amarao
        30.09.2019 15:19

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


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


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


        1. PsyHaSTe Автор
          30.09.2019 15:40

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


          1. leventov
            30.09.2019 21:37

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


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


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


            1. 0xd34df00d
              30.09.2019 21:54
              +3

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

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


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


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


              1. leventov
                30.09.2019 22:19

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


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

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


          1. leventov
            30.09.2019 21:41

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

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


            1. PsyHaSTe Автор
              30.09.2019 21:53
              +1

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


              1. 0xd34df00d
                30.09.2019 21:56
                +1

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

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


                1. leventov
                  30.09.2019 22:21

                  Или Clojure :)


                1. PsyHaSTe Автор
                  01.10.2019 18:30

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


        1. 0xd34df00d
          30.09.2019 16:36

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

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


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


          1. leventov
            30.09.2019 21:45

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


            1. 0xd34df00d
              30.09.2019 22:00
              +1

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


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


              1. leventov
                30.09.2019 22:31
                -1

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


                1. 0xd34df00d
                  30.09.2019 22:43

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


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


      1. Vitter
        30.09.2019 17:09

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


    1. nexmean
      30.09.2019 15:48
      +1

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


    1. cblp
      30.09.2019 17:16

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


      1. amarao
        01.10.2019 13:16

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


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


        1. PsyHaSTe Автор
          01.10.2019 13:34
          +1

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


        1. cblp
          01.10.2019 13:38
          +2

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

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

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

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

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

          От TH одно удовольствие, да, если не кросс-компилировать.


        1. develop7
          01.10.2019 13:42
          +1

          gtk всё ещё на второй версии только

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


        1. 0xd34df00d
          01.10.2019 16:39

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

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


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


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

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


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

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


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

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


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

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


    1. wiz
      01.10.2019 14:40
      +1

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


  1. Siemargl
    30.09.2019 15:25

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

    А вот на вопрос опроса,

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

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


    1. PsyHaSTe Автор
      30.09.2019 15:43

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


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

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


    1. nexmean
      30.09.2019 15:51
      +2

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


      1. nlinker
        30.09.2019 19:13

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


        1. gecube
          30.09.2019 19:34
          +1

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


          1. nlinker
            30.09.2019 20:34

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


    1. PsyHaSTe Автор
      30.09.2019 23:04
      +2

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


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

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


      1. Siemargl
        01.10.2019 01:22
        -3

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


        1. gecube
          01.10.2019 03:03

          Да и решается задача скорее фронтенда, в то время как Го — бекэндный язык

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


          1. Siemargl
            01.10.2019 09:46

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

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


            1. gecube
              01.10.2019 10:00
              +2

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


              1. cblp
                01.10.2019 10:43

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


                1. gecube
                  01.10.2019 11:00

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


                  1. cblp
                    01.10.2019 12:19

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

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

                    Но это деление очень нечёткое, обсуждаемую задачу нельзя отнести только к одной из этих областей.


                  1. cblp
                    01.10.2019 12:20
                    +2

                    Я пишу, в основном, на Хаскеле, но не веб, а утилиты и ядро ОС. Но какое это имеет значение?


        1. PsyHaSTe Автор
          01.10.2019 08:46
          +1

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

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


          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 на массивах/слайсах/… у любого разработчика включая даже низкоуровневые типа С++ в крови. Мне кажется, это не самая сложная задача для умных людей, которыми программисты ИМХО являются.


          1. Siemargl
            01.10.2019 09:43

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


            1. PsyHaSTe Автор
              01.10.2019 09:47

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


              1. Siemargl
                01.10.2019 11:22

                В знании синтаксиса и понятий Хаскеля. И в стремлении использовать ФП куда надо и куда не надо.
                А ответ на вопрос таки будет?

                Вот только непонятно, чего же барину не хватает в С#? Хаскель, Раст и Го ему не замена.


                1. PsyHaSTe Автор
                  01.10.2019 11:33
                  +2

                  В знании синтаксиса и понятий Хаскеля.

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


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

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


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

                  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. ...

                  ...


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


                  1. mayorovp
                    01.10.2019 11:54

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


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


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


                    1. PsyHaSTe Автор
                      01.10.2019 12:18

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

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


                      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. mayorovp
                        01.10.2019 12:39
                        +1

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

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


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

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


                        1. PsyHaSTe Автор
                          01.10.2019 12:53

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

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


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

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


                          1. mayorovp
                            01.10.2019 12:58

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

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


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


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

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


                            1. PsyHaSTe Автор
                              01.10.2019 13:00

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

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


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

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


                              1. mayorovp
                                01.10.2019 13:02

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


                                1. PsyHaSTe Автор
                                  01.10.2019 13:24
                                  +2

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


                                  1. gecube
                                    01.10.2019 13:54

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


                                    1. PsyHaSTe Автор
                                      01.10.2019 14:21

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


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

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


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


                                      1. worldmind
                                        01.10.2019 18:44

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


                                        1. PsyHaSTe Автор
                                          01.10.2019 19:00
                                          +1

                                          Ну например вот так


                                          1. worldmind
                                            01.10.2019 19:08

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


                                            1. PsyHaSTe Автор
                                              01.10.2019 19:12
                                              +1

                                              На самом деле в реальности даже так так никто не будет делать. Вы сделаете свою ошибку AppError, и будете просто в неё конвертировать автоматически. Например вот.


                                              С макросами рутину всегда легко автоматизировать. И протестировать вещи до того, как они попадают в язык. Удобный отстойник + возможность реализовать то, что в языке еще нет (например, в статье про раст мы за 5 минут реализовали шарповый nameof)


                              1. Cerberuser
                                01.10.2019 13:03

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


                                (синхрон)


                  1. Siemargl
                    01.10.2019 13:31

                    пример в 1 наверное неточный

                    int ?a = null;
                    int ?b = 4;
                    var x = a + b;
                    Console.WriteLine("Hello World {0}", x);
                    if (!x.HasValue) Console.WriteLine("Hello Null World {0}", x);
                    
                    nullabe и исключения — известное и привычное зло, а вот остальное подпадает под хотелки и сование ФП куда не надо )

                    только вот дело не только в языке, а еще и сравнимый набор фреймворков надо найти к языку


                    1. PsyHaSTe Автор
                      01.10.2019 18:41

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


                      long? ticks = ...;
                      var timespan = ticks != null ? TimeSpan.FromTicks(ticks) : null;

                      при этом в расте


                      let ticks = ...;
                      let timespan = ticks.map(TimeSpan::FromTicks);

                      С одной стороны мелочь. А с другой раздражает очень)




                      Еще прикольная штука в расте, условная реализация. Например:


                      struct Option<T>;
                      
                      impl Ord<T: Ord> for Option<T> {...}
                      impl Eq<T: Eq> for Option<T> { ... }

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


                      теперь возьмем шарп:


                      class Option<T> : IComparable<T>, IEquitable<T> {}

                      Если мы напишем так, то не сможем создать Option<object> например, потому что он не реализует эти трейты. А сделать сортировку на них хотелось бы...


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


                      1. Siemargl
                        01.10.2019 19:18

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

                        Во-вторых я не понимаю, Раст не тема статьи и не тема обсуждения в этой ветке. Какого х вы на него постоянно съезжаете??? Что там проблемного с этим (nullable/ADT) в Го и Хаскеле?

                        В третьих, при разворачивании нуля в расте получим панику, что еще хуже, чем исключение в С№. Или пишем аналогичные лесенки матчей. Прикольно, но не более.

                        В С№ есть нормальное наследование т.ч. с интерфейсами и методы расширения. Не хватает?


                        1. 0xd34df00d
                          01.10.2019 19:19

                          А (G)ADT есть в Go?


                        1. PsyHaSTe Автор
                          01.10.2019 19:34

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

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


                          Во-вторых я не понимаю, Раст не тема статьи и не тема обсуждения в этой ветке. Какого х вы на него постоянно съезжаете??? Что там проблемного с этим (nullable/ADT) в Го и Хаскеле?

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


                          Проблем с АДТ в хаскелле нет, а в го просто их нет. Собственно, и в шарпах нет.


                          В третьих, при разворачивании нуля в расте получим панику, что еще хуже, чем исключение в С№. Или пишем аналогичные лесенки матчей. Прикольно, но не более.

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


                          В С№ есть нормальное наследование т.ч. с интерфейсами и методы расширения. Не хватает?

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


                          1. Siemargl
                            01.10.2019 20:11
                            -1

                            в примере оберточка map_err(). дальше оффтоп обсуждать не буду

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


        1. cblp
          01.10.2019 10:47
          +1

          Делать запросы по HTTP — типичная задача и для бэкенда. К чему это было?


          1. Siemargl
            01.10.2019 11:15

            На единственный запрос клиента, мы порождаем неограниченное количество асинхронных подзапросов к серверам (неважно, http, odbc, ...).
            Сюр, который положит бэк — это же само-ДДОС атака.


            1. PsyHaSTe Автор
              01.10.2019 11:23

              Если вы краулите интернет то у вас нет возможности получить всё. Как пример задачки, у вас на входе список урлов, а на выходе HTML каждой страницы.


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


              1. Siemargl
                01.10.2019 11:26

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


                1. cblp
                  01.10.2019 12:23

                  Комментарии форума надо хранить и мутировать в БД, а не в памяти процесса.


            1. cblp
              01.10.2019 12:12

              Но они же в очереди, которой можно управлять. Как положат?


  1. KReal
    30.09.2019 15:26

    Но менее известно, что в высокоуровневых языках это Haskell. В Scala/F#/Idris/Elm/Elexir/… тусовках если не знаешь, на чем пишет твой визави — пиши на хаскелле, не ошибешься.


    Почему не OCaml? Сами же пишете «ML языки». :)


    1. cblp
      30.09.2019 16:00

      У Окамля существенно меньше поклонников.


      1. KReal
        30.09.2019 16:04

        Так-то да, хотя F# вроде как вполне ML и потихоньку набирает обороты.


        1. PsyHaSTe Автор
          30.09.2019 23:06
          +2

          Честно говоря, я не вижу особых преимуществ F# над C#, а проблем с компилятором и коммьюнити у него хватает. Одни только АДТ не повод мигрировать с одного языка на другой, тем более что с новым паттерн матчингом можно их достаточно удобно эмулировать на шарпах. Коммьюнити же у F# на любые фич реквесты говорит "не нужно". Ну как так что хкт не нужно в фп языке? А они в это верят.


  1. 0xd34df00d
    30.09.2019 16:26
    +1

    Отличная статья, буду на неё теперь ссылки давать. Мне бы терпения не хватило с Go мучаться.


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

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


    Дедлок чекер из коробки очень крут.

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


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

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


    И который за вас даже js-биндинги к API сгенерит.


    1. PsyHaSTe Автор
      30.09.2019 17:59
      +2

      Отличная статья, буду на неё теперь ссылки давать. Мне бы терпения не хватило с Go мучаться.

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


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


      1. NetBUG
        30.09.2019 19:44

        Кстати, компиляция кода на Хаскелле за ~30 минут — это же опечатка, правда?
        Проект на Rust быстрее выкачает и соберёт зависимости при первой сборке.


        1. PsyHaSTe Автор
          30.09.2019 19:54

          Нет, он правда долго качает и собирается. Скорее всего он тупил потому что у меня было запущено 2 студии и 3 идеи, которые дрались за CPU, но вообще он нетороплив.



          С другой стороны есть вероятность что это у хаскелля репозиторий тупил и медленно отдавал зависимости. Хотя какая разница, если результат один?


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


          1. gecube
            30.09.2019 19:59

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


          1. 0xd34df00d
            30.09.2019 20:04

            Я когда серьёзно обновляю LTS-снапшот в stack (со сменой версии компилятора), то после stack build иду пить чай и разминаться, да.


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

            На прошлой работе haskell infra team тупо делала образы docker'а со всеми установленными пакетами из LTS.


  1. worldmind
    30.09.2019 16:52

    Прям ностальгия по красоте хаскела — пару лет назад немного изучал, всё прикольно, но без использования забывается.
    Читал тогда всё что было — насколько помню минусов именно самого ФП/хаскела не так много (нехватка модулей, баги в них или модули на си, это не баг языка, а результат малой популярности):

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

    может я что упустил? 0xd34df00d potan


    1. worldmind
      30.09.2019 16:55

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


      1. cblp
        30.09.2019 17:12
        +1

        В каком-нибудь Хаскеле легко писать мутабельные алгоритмы с runST или чем-то аналогичным.


      1. 0xd34df00d
        30.09.2019 17:23
        +1

        Лучше вместо lua взять ATS тогда уж.


    1. 0xd34df00d
      30.09.2019 17:23
      +1

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

      Ну, берёте монаду ST, и всё.


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

      Я бы сказал, что да, проблема возможна. Да, она встречается на практике, но я не сказал бы, что существенно часто. Да и место утечки искать на самом деле не так уж сложно — просто берёте профиль потребления памяти, запихиваете его в threadscope, и сразу всё понятно.


      Но из-за той же ленивости, впрочем, можно писать вполне линейный и тупой код и получать O(1) потребление памяти.


      А ещё есть {-# LANGUAGE Strict #-} и StrictData.


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

      Ну вот бы в треде о сравнении с Go обсуждать отсутствие завтипов :)


      Дело не совсем в легаси (легаси как раз успешно ломают, это то самое avoid [success at all costs] — то applicative/monad proposal примут, то monoid/semigroup, сейчас вот звёздочку выпиливают как обозначение сорта типов). Дело в том, что под полноценные завтипы язык надо проектировать с нуля (иначе у вас легаси оказывается весь язык целиком, а это непродуктивно). Есть проблема с тем, как использовать термы на уровне типов. Есть проблема с тем, что язык не различает данные и коданные (и это куда большая проблема от ленивости, чем потребление памяти), а без этого различения трудно рассуждать о завершимости функций, а без завершимости функций у вас будут большие проблемы с тайпчекингом и консистентностью языка как логики. Есть проблема с тем, что некоторые фичи в языке мощнее, чем то, что вы обычно увидите в завтипизированных языках — PolyKinds тому пример.


      Но работа идёт.


      1. 0xd34df00d
        30.09.2019 17:48

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

        Хотя тут я, похоже, наврал.


        *Test> :t id
        id : a -> a
        *Test> id 5
        5 : Integer
        *Test> id Nat
        Nat : Type
        *Test> id Type
        Type : Type
        *Test> :t id Type
        id Type : Type

        Что-то я перестал понимать, как это работает конкретно в идрисе. Судя по беглому гуглу, там что-то вроде неявного universe polymoprhism + кумулятивные универсумы, где на самом деле нет Type, а есть кумулятивная иерархия Type 0 ? Type 1 ? ..., плюс автоматический вывод нужного индекса в точке использования.


        А ещё про это всё в случае хаскеля есть хорошая статья.


  1. gecube
    30.09.2019 17:57
    +2

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


    1. lorc
      30.09.2019 19:41

      Это то, что убило меня в Го. Когда пишешь на С, то все время держишь в голове: «так, тут у меня состояние шарится между потоками, надо не забыть про мютекс». Когда пишешь на Эрланге — кидаешься сообщениями и вообще не паришься.

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


      1. anton19286
        02.10.2019 07:56

        Do not communicate by sharing memory; instead, share memory by communicating.
        Я бы, наверное, описанную задачу решал так: не запускал горутину на каждую запись, а создал пул воркеров с входными и выходными каналами, во входной пихал айдишники, с выходного сваливал результаты в мап, потом закрыл входной чтоб воркеры закончились, а из мапа по-новой построил дерево. Никаких блокировок или гонок.


        1. gecube
          02.10.2019 10:21
          +2

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


          1. anton19286
            03.10.2019 06:44

            Я не утверждаю, что это правильно. Просто я так привык писать на го. как-то так вышло


            1. PsyHaSTe Автор
              03.10.2019 08:43

              Но тут вы создаете по дополнительному каналу на каждом узле. Вряд ли это даст какое-то улучшение...


              1. anton19286
                03.10.2019 09:37
                +1

                Разве? Ведь loadComments вызывается только один раз.
                Кстати, рекурсивную функцию, пожалуй, тоже удобнее засунуть вовнутрь.

                func loadComments(root intTree) map[int]comment {
                    var wg sync.WaitGroup
                    result := make(map[int]comment)
                    in := make(chan int)
                    out := make(chan comment)
                    nWorkers := 2
                    for i := 0; i < nWorkers; i++ {
                        go func() {
                            for id := range in {
                    	        out <- getCommentById(id)
                            }
                        }()
                    }
                    var loadCommentsInner func(node intTree)
                    loadCommentsInner = func(node intTree) {
                	for _, c := range node.children {
                            loadCommentsInner(c)
                        }
                        wg.Add(1)
                        in <- node.id
                    }
                    go func() {
                        for c := range out {
                            result[c.Id] = c
                    	    wg.Done()
                        }
                    }()
                    loadCommentsInner(root)
                    close(in)
                    wg.Wait()
                    close(out)
                    return result
                }
                


                1. PsyHaSTe Автор
                  03.10.2019 13:45
                  +2

                  А, ну это ж нечестно, вы сделали мапу :) Хотя бы код собирания дерева обратно если бы добавили то стало бы понятнее. Красота траверсабла в том и заключается, что он позволяет автомагически собрать оригинальную структуру, ничего априорно про неё не зная. Полагаю, это одна из причин, почему 0xd34df00d придумал именно эту задачу.


                  1. anton19286
                    04.10.2019 07:46

                    Да, го достаточно бедный, сущностей немного, иногда приходится извращаться, но потом этот ужас обычно прячется в библиотеки. В результате возможность переиспользования кода вполне на уровне, зря народ говорит про write-only. Хотя, с генериками такой, например, код был бы повеселее

                    type Futurer interface {
                        Await() interface{}
                    }
                    type future struct {
                        await func() interface{}
                    }
                    func (f future) Await() interface{} {
                        return f.await()
                    }
                    func WhenAll(f func(intTree) commentTree, v []intTree ) Futurer {
                        l := len(v)
                        var result = make([]commentTree, l)
                        var wg sync.WaitGroup
                        wg.Add(l)
                        for i := 0; i < l; i++ {
                            i := i
                            go func() {
                                result[i] = f(v[i])
                                wg.Done()
                            }()
                        }
                        return future{
                            await: func() interface{} {
                                wg.Wait()
                                return result
                            },
                        }
                    }
                    func GetCommentsTree(tree intTree) commentTree {
                        children := WhenAll(GetCommentsTree, tree.children)
                        var value = getCommentById(tree.id)
                        var childrenResults = children.Await().([]commentTree)
                        return commentTree{ value, childrenResults }
                    }
                    


                    1. mayorovp
                      04.10.2019 09:40
                      +3

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


                      1. anton19286
                        04.10.2019 10:43

                        Как я понял из текста, он не наступал, это люди сказали, что может наступить. Да и странно это как-то, в шарпе можно так делать, а в го нельзя. Хотя, там ведь тоже TPL неспроста придумали.
                        Кстати, был бы благодарен, если знатоки рефлексии в го показали, как это переписать, чтоб WhenAll принимала func(interface{}) interface{}


                        1. mayorovp
                          04.10.2019 11:10
                          +2

                          Ну так просто замените intTree на interface{} и всё. Ничего умнее без дженериков сделать всё равно не получится.


                          1. anton19286
                            04.10.2019 12:52
                            +1

                            Разобрался. Оказывается, на Go async/await вполне можно сделать в более-менее общем виде:

                            func WhenAll(f interface{}, s interface{} ) Futurer {
                                v := reflect.ValueOf(s)
                                fn := reflect.ValueOf(f)
                                if reflect.TypeOf(s).Kind() != reflect.Slice {
                                    panic("not slice")
                                }
                                if reflect.TypeOf(f).Kind() != reflect.Func {
                                    panic("not func")
                                }
                                if reflect.TypeOf(f).In(0) != v.Type().Elem() {
                                    panic("wrong arg type")
                                }
                                l := v.Len()
                                var result = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(f).Out(0)), l, l) 
                                var wg sync.WaitGroup
                                wg.Add(l)
                                for i := 0; i < l; i++ {
                                    i := i
                                    go func() {
                                        in := []reflect.Value{v.Index(i)}
                                        result.Index(i).Set(fn.Call(in)[0]) 
                                        wg.Done()
                                    }()
                                }
                                return future{
                                    await: func() interface{} {
                                        wg.Wait()
                                        return result.Interface()
                                    },
                                }
                            }
                            


                            1. mayorovp
                              04.10.2019 13:18
                              +1

                              А что там со скоростью такого кода?


        1. lorc
          02.10.2019 18:53
          +2

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

          В тоже время, одной из фишек Го заявляются легковесные green threads. Предполагается что их можно спавнить на каждый чих. Но shared state убивает всю простоту.


          1. anton19286
            03.10.2019 05:21

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


          1. nexmean
            03.10.2019 14:04
            +6

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


  1. youROCK
    30.09.2019 18:53

    Странно, что вы не взяли мой вариант из комментария, который упомянули в начале статьи ( habr.com/ru/post/466337/#comment_20591963 ):

    type Comment struct {
            ID       int        // задано
            Contents string     // заполняем сами
            Children []*Comment // задано
    }
    
    func loadComments(root *Comment) {
            var wg sync.WaitGroup
            loadCommentsInner(&wg, root)
            wg.Wait()
    }
    
    func loadCommentsInner(wg *sync.WaitGroup, node *Comment) {
            wg.Add(1)
            go func() {
                    // не факт, что можно просто так заполнять поля произвольной структуры
                    // из любой горутины, но вроде можно
                    node.Contents = loadCommentContentsByID(node.ID)
                    wg.Done()
            }()
            for _, c := range node.Children {
                    loadCommentsInner(wg, c)
            }
    }
    


    (полные исходники для демонстрации: play.golang.org/p/CnByHMJrC7K)

    Если теперь вам нужно будет ограничить конкурентность, например (совершенно точно нужно будет, если будете делать что-то похожее в продакшене), то уже становится интереснее с тем, как это будет выглядеть в Хаскеле. То есть, я уверен, что это тоже можно, просто код тоже будет сложнее. Если нужна обработка ошибок (она всегда нужна), то код тоже усложняется. Плюс нужно логирование, плюс походы в RPC, и т.д., и неожиданно получается, что код на Go не такой уж и сложный по сравнению с другими языками, хотя и да, Go не пытается сделать работу с конкуретностью полностью тривиальной. Для простых кейсов подойдет race detector (go build -race или go run -race). Мой пример, кажется, рейсов не вызывает, по крайней мере после 10 000 прогонов ошибок не нашел :).

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

    Что касается enterprise, ORM и «больших и тяжелых» фреймворков, они в Go сообществе не особо-то в почете, поэтому Вы и не нашли ничего хорошего.

    В любом случае, ваш опыт интересен, спасибо за статью :).


    1. 0xd34df00d
      30.09.2019 20:02

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

      Да не так уж много изменить придётся.


      Вместо библиотеки async возьмёте async-pool, создадите там таск-группу и возьмёте mapConcurrently из этой библиотеки, а не из исходной async. Получится что-то вроде withTaskGroup 10 (\tg -> mapConcurrently tg getCommentById tree) вместо mapConcurrently getCommentById tree, и всё.


      Если нужна обработка ошибок (она всегда нужна), то код тоже усложняется.

      Просто ещё один слой в монадном стеке, теперь ExceptT/MonadError.


      Плюс нужно логирование

      Ещё один слой. Вот они, прелести абстракций!


      плюс походы в RPC

      А оно ничем принципиально не будет.


    1. PsyHaSTe Автор
      30.09.2019 20:08
      +3

      Странно, что вы не взяли мой вариант из комментария, который упомянули в начале статьи

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


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

      Очень интересный вопрос, пожалуй сегодня вечером посмотрю, как это может выглядеть. В идеале если это будет отдельный комбинатор типа limit . mapConcurrent getCommentById tree, но скорее всего вместо mapConcurrent должна быть другая функция, принимающая целочисленный аргумент ограничения.


      Если нужна обработка ошибок (она всегда нужна), то код тоже усложняется.

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


      Плюс нужно логирование

      монадический трансформер WriterT


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

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


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

      Ну, я подумал, что в данном случае будет паритет по фишкам: абстрактные структуры данных (дерево в haskell) против удобной асинхронности (горутины в го).


      В любом случае, ваш опыт интересен, спасибо за статью :).

      Рад, что вам понравилось.




      Кстати, вспоминая статью "Какого цвета ваша функция", в го тоже функции разных цветов. Вот эта — синяя: func loadComments(node intTree) commentTree
      А вот эта — красная: func loadComments(resNode *commentTree, node intTree, wg *sync.WaitGroup)


      --


      Ну вот, пока писал за меня уже все ответили...


      1. youROCK
        30.09.2019 20:16

        С Вашего позволения, отвечу только на последнее утверждение :).

        Кстати, вспоминая статью «Какого цвета ваша функция», в го тоже функции разных цветов. Вот эта — синяя: func loadComments(node intTree) commentTree
        А вот эта — красная: func loadComments(resNode *commentTree, node intTree, wg *sync.WaitGroup)

        Да, хорошее замечание. С этим связана рекомендация делать публичными только синхронные API — сделать API асинхронным в Go элементарно, а вот наоборот может быть невозможно. На тему того, как работать с асинхронностью в Go, есть прекрасный (хоть и не самый свежий) доклад, очень рекомендую: www.youtube.com/watch?v=QDDwwePbDtw. И да, правильно работать с конкурентностью в Go сложно, поскольку он не настаивает на immutable структурах данных. Зато он может это делать достаточно эффективно, если всё реализовано правильно.


  1. cy-ernado
    30.09.2019 19:58
    +1

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

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


    Если разрешить мутировать изначальное дерево (что бы сделали большинство разработчиков на этом языке), то получается немного проще:


    type Tree struct {
        ID      int     `json:"-"`
        Comment string  `json:"comment"`
        Childs  []*Tree `json:"-"`
    }
    
    func (t *Tree) Traverse(fetchers chan<- *Tree) {
        fetchers <- t
        for _, tt := range t.Childs {
            tt.Traverse(fetchers)
        }
    }
    
    func (t *Tree) Fetch() {
        resp, err := http.Get("http://jsonplaceholder.typicode.com/todos/" + strconv.Itoa(t.ID))
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        if err = json.NewDecoder(resp.Body).Decode(t); err != nil {
            panic(err)
        }
    }
    
    func fetchAll(t *Tree, concurrency int) {
        trees := make(chan *Tree)
        go func() {
            t.Traverse(trees)
            close(trees)
        }()
        wg := new(sync.WaitGroup)
        wg.Add(concurrency)
        for i := 0; i < concurrency; i++ {
            go func() {
                defer wg.Done()
                for f := range trees {
                    f.Fetch()
                }
            }()
        }
        wg.Wait()
    }

    У меня сложилось впечатление, что автор статьи был изначально несколько предвзят к Go, как и автор постановки задачи. В ответ я бы предложил дополнить постановку задачи следующим образом:


    1. Без аллокации нового дерева
    2. Возможность контроля количества параллельных запросов
    3. Возможность отмены обхода по дереву в любой момент, но с сохранением уже полученных данных (т.е. частичный ответ)
    4. Глобальный таймаут на обход, после которого он заверашется с ошибкой
    5. Возможность протестировать взаимодействие по HTTP
    6. Отмена обхода после первой ошибки загрузки/десериализации данных (такая же, как в п.3)

    Мое решение на го я выложил на гитхаб: ernado/traverse. Получилось 167 SLOC с тестами, но я особо их не пытался экономить, и заодно реализовал задачу как cli утилиту:


    $ ./traverse -j 1 --timeout 30ms
    Failed: Get http://jsonplaceholder.typicode.com/todos/3: context deadline exceeded
    1=delectus aut autem
     2=quis ut nam facilis et officia qui
     3=
      4=
      5=

    По ^C как раз происходит отмена в соответствии с п.3., опция j отвечает за ограничение параллельных запросов, а timeout указывает глобальный таймаут.


    UPD: Забыл про п. 6


    1. 0xd34df00d
      30.09.2019 20:21
      +1

      Если разрешить мутировать изначальное дерево (что бы сделали большинство разработчиков на этом языке), то получается немного проще

      Не получается, так как дерево ID'шников и дерево комментариев — это вообще разные типы. И смешивать их (как в вашем примере) — плохая идея, потому что теперь компилятор не даст мне по рукам, если я случайно полезу доставать текст комментария в дереве, которое не было «материализовано».


      Без аллокации нового дерева

      См. выше.


      Возможность контроля количества параллельных запросов

      Написал рядом.


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

      Выносите всё это вычисление в отдельный async, который потом прибиваете через cancel.


      Возможность протестировать взаимодействие по HTTP

      В смысле, что именно протестировать?


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

      Вот это — единственное, что потребует не совсем тривиальных изменений исходного кода (что, кстати, естественно и хорошо, потому что поменялась семантика вашей программы, а если от изменения вашей семантики у вас не меняются типы, то изначально что-то было не в порядке). Будет дерево с узлами типа Maybe Comment вместо Comment в STM + набор потоков, которые в него пишут.


      Хотя в данном конкретном случае можно и без STM, а дерево с узлами типа MVar Comment. Чуть эффективнее, но придётся включать голову, чтобы писать код — с STM ошибки сделать куда труднее, чем с MVar.


      1. cy-ernado
        30.09.2019 20:33

        Не получается, так как дерево ID'шников и дерево комментариев — это вообще разные типы. И смешивать их (как в вашем примере) — плохая идея, потому что теперь компилятор не даст мне по рукам, если я случайно полезу доставать текст комментария в дереве, которое не было «материализовано».

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


        В смысле, что именно протестировать?

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


        Вот это — единственное, что потребует не совсем тривиальных изменений исходного кода. Будет дерево с узлами типа Maybe Comment вместо Comment в STM + набор потоков, которые в него пишут.

        Круто. Мне действительно интересно, как это будет выглядеть в реальном коде на Haskell и C#.
        Сейчас мой вариант на Go относительно прост, но там есть спорные места — ограничение на макс. глубину дерева (из-за отсутсвия TCO, впрочем, мб можно убрать рекурсию) и потенциальный data race (всё же мутировать дерево во время обхода по нему — тот еще веселый аттракцион).


        1. 0xd34df00d
          30.09.2019 20:41
          +2

          В го стараются работать с данными, а не типами, и большинство решений — ad-hoc, поэтому и такой подход.

          Ну так это же не что-то хорошее. По крайней мере, я бы даже на плюсах как минимум хранил бы дерево с узлами из std::variant<Id, CommentBody>, как максимум оно точно так же было бы параметризованное.


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

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


          Круто. Мне действительно интересно, как это будет выглядеть в реальном коде на Haskell и C#.

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


          потенциальный data race (всё же мутировать дерево во время обхода по нему — тот еще веселый аттракцион)

          Вот за это мы STM и гарантии компилятора и любим.


          1. gecube
            30.09.2019 20:44
            -1

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

            Что-то мне это напоминает… А! Точно! Заборы декораторов из питона.


            Ну так это же не что-то хорошее. По крайней мере, я бы даже на плюсах как минимум хранил бы дерево с узлами из std::variant<Id, CommentBody>, как максимум оно точно так же было бы параметризованное.

            Эх, опередили. Я тоже хотел упомянуть про С и С++ именно в этом ключе. А потом плакаться про рефлексию в рантайме. Этого в голанге нет и не нужно туда это тащить — взращивать очередного монстра.


            1. 0xd34df00d
              30.09.2019 20:56
              +3

              Что-то мне это напоминает… А! Точно! Заборы декораторов из питона.

              Или, ну… Стройные строки из вызовов функций!
              Выносить части кода в функции плохо?


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


              foo :: (MonadWriter [LogMessage] m,
                      MonadReader Config m,
                      MonadError ParseError m,
                      MonadHttp "mybestsite.com" m)
                  => ...
                  -> m (Tree Comment)

              то вы, глядя на одну эту сигнатуру (которую, как мы помним, проверяет компилятор), сразу делаете выводы:


              1. Эта функция может производить логоподобный набор записей [LogMessage].
              2. Эта функция может читать конфигурацию из некоторого глобального для неё окружения.
              3. Эта функция может завершиться ошибкой типа ParseError.
              4. Эта функция может делать запросы по HTTP (причём только с сайта mybestsite.com)1.
              5. Скорее всего, эта функция (или вызываемые ей) всё это действительно делает (иначе нафига писать констрейнты, это ж лишняя писанина).
              6. Но, самое главное, эта функция не делает ничего другого. Не читает из файлов, не пишет в лог обход логгера, не запрашивает ничего по хттп в обход реализации MonadHttp, не запрашивает никаких урлов, кроме как с сайта mybestsite.com, не майнит биткоины, не отсылает ваши пароли от ваших биткоин-кошельков.
              7. А, ну и её «нормальным» результатом является Tree Comment.

              1 Этот конкретный MonadHttp вы вряд ли найдёте уже готовым, но современный хаскель со всеми его расширениями системы типов позволяет что-то такое с такими гарантиями реализовать.


              Этого в голанге нет и не нужно туда это тащить — взращивать очередного монстра.

              Не понял, что не нужно? Возможность писать типобезопасный код не нужна?


          1. cy-ernado
            30.09.2019 20:54

            Ну так это же не что-то хорошее. По крайней мере, я бы даже на плюсах как минимум хранил бы дерево с узлами из std::variant<Id, CommentBody>, как максимум оно точно так же было бы параметризованное.

            А я бы и не сказал, что это однозначно плохо :)


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

            У меня можно подменить HTTP клиент, т.е. запрос -> ответ. Но смысл я понял, спасибо.


            Вот за это мы STM и гарантии компилятора и любим.

            Очевидно, что у всего есть цена. А еще было бы замечательно, если бы STM реализовали на уровне железа. В итоге приходим к тому, что у го есть свои трейдоффы, как и у всех ЯП.


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


            1. 0xd34df00d
              30.09.2019 21:03

              А еще было бы замечательно, если бы STM реализовали на уровне железа.

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


        1. gecube
          30.09.2019 20:42

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


        1. PsyHaSTe Автор
          30.09.2019 21:37
          +2

          Круто. Мне действительно интересно, как это будет выглядеть в реальном коде на Haskell и C#.
          Сейчас мой вариант на Go относительно прост, но там есть спорные места — ограничение на макс. глубину дерева (из-за отсутсвия TCO, впрочем, мб можно убрать рекурсию) и потенциальный data race (всё же мутировать дерево во время обхода по нему — тот еще веселый аттракцион).

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


          В случае сишарпа чтобы добавить мок достаточно отнаследовать HTTP хандлер и передать в конструктор:


          class TestHandler : HttpClientHandler
          {
              // мокаете ответ как хотите. Например, вставляем исскуственную задержку для третьего случая, но можно делать в общем случае что угодно
              protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
              {
                  if (request.RequestUri.PathAndQuery.EndsWith('3'))
                  {
                      await Task.Delay(5000, cancellationToken);
                  }
                  return await base.SendAsync(request, cancellationToken);
              }
          }
          
          ...
          
          private HttpClient HttpClient = new HttpClient(new TestHandler());

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


          private static async Task Main()
          {
              var timeout = TimeSpan.FromMilliseconds(500);
              HttpClient = new HttpClient(new TestHandler());
              var cancellationTokenSource = new CancellationTokenSource(timeout);
              cancellationTokenSource.Token.Register(() => HttpClient.CancelPendingRequests());
              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);
          }

          Скомбинировав оба варианта, получим, что №3 никогда не загрузится:


          1
            2
            3
              4
              5
          1 - delectus aut autem
            2 - quis ut nam facilis et officia qui
          
              4 - et porro tempora
              5 - laboriosam mollitia et enim quasi adipisci quia provident illum
          
          C:\Program Files\dotnet\dotnet.exe (process 16704) exited with code 0.

          https://gist.github.com/Pzixel/e882d6653937843b4fd412053bb69489


    1. PsyHaSTe Автор
      30.09.2019 21:51
      +3

      По пунктам:


      Без аллокации нового дерева

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


      Возможность контроля количества параллельных запросов

      var handler = new TestHandler(){MaxConnectionsPerServer = 5}


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

      Написал в ветке выше + Console.CancelKeyPress += (sender, e) => cancellationTokenSource.Cancel();


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

      Написал в ветке выше


      Возможность протестировать взаимодействие по HTTP (т.е. "замокать" поход в сеть, оставив всё остальное)

      Написал в ветке выше


      Отмена обхода после первой ошибки загрузки/десериализации данных (такая же, как в п.3)

      Вместо AggregateException в примере выше указать типы исключений, которые хотим ловить.


      Всё это касается сишарпа который я знаю. За Haskell надеюсь кто-то разбирающийся в теме напишет, желательно не египетскими иероглифами a <$> b ^. c >>= d.


  1. PsyHaSTe Автор
    30.09.2019 21:36

    del


  1. AcckiyGerman
    01.10.2019 17:44

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


    Исходные структуры данных:


    from dataclasses import dataclass
    
    @dataclass
    class IdNode:
        """tree for IDs"""
        id: int
        children: list
    
    @dataclass
    class MsgNode:
        """tree for messages"""
        message: str
        children: list
    
    # initial tree with IDs
    tree = IdNode(1, [
        IdNode(2, []),
        IdNode(3, [
            IdNode(4, []),
            IdNode(5, [])
        ])
    ])
    
    print(tree)

    Удаленная база сообщений (обращаюсь к ней через typicode, как и автор статьи) :


    {
      "messages": [
        {
          "id": 1,
          "message": "1:Оригинальный комментарий"
        },
        {
          "id": 2,
          "message": "2:Ответ на комментарий 1"
        },
        {
          "id": 3,
          "message": "3:Ответ на комментарий 2"
        },
        {
          "id": 4,
          "message": "4:Ответ на ответ 1"
        },
        {
          "id": 5,
          "message": "5:Ответ на ответ 2"
        }
      ],
      "profile": {
        "name": "typicode"
      }
    }

    Синхронное решение задачи:


    #!/usr/bin/env python
    import requests
    from data_structures import IdNode, MsgNode, tree
    
    api_url = "https://my-json-server.typicode.com/AcckiyGerman/demo/messages/"
    
    def get_comment_by_id(x):
        url = api_url + str(x)
        r = requests.get(url).json()
        return r["message"]
    
    def map_tree(node):
        return MsgNode(
            message=get_comment_by_id(node.id),
            children=[map_tree(child) for child in node.children]
        )
    
    message_tree = map_tree(tree)
    print(message_tree)

    Асинхронное решение задачи:


    #!/usr/bin/env python
    import aiohttp
    import asyncio
    from data_structures import IdNode, MsgNode, tree
    
    api_url = "https://my-json-server.typicode.com/AcckiyGerman/demo/messages/"
    messages = {}
    tasks = []
    
    async def get_comment_by_id(x, session):
        global messages
        url = api_url + str(x)
        r = await session.get(url)
        data = await r.json()
        messages[x] = data['message']
        print(f"request {x} finished")
    
    def initiate_tasks(node, session):
        """ starts a task for each message id in the tree, but not await for result """
        global tasks
        tasks.append(get_comment_by_id(node.id, session))
        for child in node.children:
            initiate_tasks(child, session)
    
    def map_tree(node):
        global messages
        return MsgNode(
            message=messages[node.id],
            children=[map_tree(child) for child in node.children]
        )
    
    async def main():
        async with aiohttp.ClientSession() as session:
            initiate_tasks(node=tree, session=session)
            await asyncio.gather(*tasks)
            message_tree = map_tree(tree)
            print(message_tree)
    
    if __name__ == "__main__":
        asyncio.run(main())

    Результаты:


    $ time ./sync_fetch.py 
    IdNode(id=1, children=[IdNode(id=2, children=[]), IdNode(id=3, children=[IdNode(id=4, children=[]), IdNode(id=5, children=[])])])
    MsgNode(message='1:Оригинальный комментарий', children=[MsgNode(message='2:Ответ на комментарий 1', children=[]), MsgNode(message='3:Ответ на комментарий 2', children=[MsgNode(message='4:Ответ на ответ 1', children=[]), MsgNode(message='5:Ответ на ответ 2', children=[])])])
    
    real    0m1,762s
    user    0m0,181s
    sys     0m0,012s
    
    $ time ./async_fetch.py
    IdNode(id=1, children=[IdNode(id=2, children=[]), IdNode(id=3, children=[IdNode(id=4, children=[]), IdNode(id=5, children=[])])])
    request 2 finished
    request 3 finished
    request 1 finished
    request 5 finished
    request 4 finished
    MsgNode(message='1:Оригинальный комментарий', children=[MsgNode(message='2:Ответ на комментарий 1', children=[]), MsgNode(message='3:Ответ на комментарий 2', children=[MsgNode(message='4:Ответ на ответ 1', children=[]), MsgNode(message='5:Ответ на ответ 2', children=[])])])
    
    real    0m0,473s
    user    0m0,137s
    sys     0m0,020s

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


    Я потратил около 3-х часов времени, чтобы написать правильное асинхронное решение, и мне не очень нравится результат. Рекурсивного обхода не получилось из-за необходимости собрать асинхронные обращения к удаленному серверу в одном месте, так что пришлось вводить еще две глобальные структуры messages и tasks


    Синхронное решение заняло 10 минут. Правда я никогда в работе с асинхронным питоном не работал.


    Может кто-либо подскажет, как можно короче написать.


    код на гитхабе


    1. PsyHaSTe Автор
      01.10.2019 18:05
      +1

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


      Любопытно, можно ли как-то упростить второй случай..


    1. mayorovp
      01.10.2019 18:59

      А зачем вы собирали задачи в одном месте? Разве нельзя было сделать вот так:


      async def map_tree(node):
          message, children = await asyncio.gather(
              get_comment_by_id(node),
              asyncio.gather(*[map_tree(child) for child in node.children])
          )
      
          return MsgNode(
              message=message,
              children=children
          )


  1. AlexeySoshin
    02.10.2019 14:14
    +3

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


    Работаю с Go года четыре, и в целом полностью согласен с выводами, сделанными за четыре часа знакомства. Go Community крайне токсичный, берет плохой пример с Rob'а Pike'а, и не берет хороший пример с Russ'а Cox'а.