
Comme je l'ai écrit dans la préface de l' article précédent , je suis à la recherche d'une langue dans laquelle je pourrais écrire moins et avoir plus de sécurité. Mon langage de programmation principal a toujours été le C #, j'ai donc décidé d'essayer deux langages symétriquement différents en termes de complexité, dont je n'avais entendu parler qu'avant, mais je ne pouvais pas écrire: Haskell et Go. Une langue s'est fait connaître pour dire "Évitez le succès à tout prix" *, tandis que l'autre, à mon humble avis, est son contraire. En fin de compte, je voulais comprendre ce qui allait être mieux: la simplicité intentionnelle ou la rigueur intentionnelle?
J'ai décidé d'écrire une solution à un problème, et de voir à quel point c'est facile dans les deux langues, quelle est leur courbe d'apprentissage pour un développeur expérimenté, combien devrait être étudié pour cela et comment idiomatique est le code "newbie" dans l'un et l'autre cas. De plus, je voulais comprendre combien je devrais éventuellement payer pour satisfaire le compilateur Haskell et combien de temps la fameuse commodité goroutine permettrait d'économiser. J'ai essayé d'être le plus impartial possible et je donnerai une opinion subjective à la fin de l'article. Les résultats finaux m'ont beaucoup surpris, j'ai donc décidé qu'il serait intéressant pour les citoyens de Khabrovsk de lire une telle comparaison.
Et immédiatement une petite remarque. Le fait est que l'expression (*) est souvent utilisée avec ironie, mais c'est uniquement parce que les gens ne la comparent pas correctement. Ils le lisent comme «éviter (succès) (à tout prix)», c'est-à-dire «quoi qu'il arrive, si cela mène au succès, nous devons l'éviter», tandis que la vraie phrase se lit comme «éviter (succès du tout) "), c'est-à-dire" si le prix du succès est trop élevé, il faut alors prendre du recul et tout repenser ". J'espère qu'après cette explication elle a cessé d'être ridicule et a trouvé un vrai sens: l'idéologie de la langue nécessite la bonne planification de votre application, et n'insérez pas de béquilles adhoc là où elles coûtent trop cher. L'idéologie de Go, à son tour, est plutôt «le code devrait être assez simple pour qu'en cas de changement des exigences il soit facile de le jeter et d'en écrire un nouveau».
Technique de comparaison
Sans plus tarder, j'ai pris le puzzle inventé par le camarade 0xd34df00d et cela ressemble à ceci:
Supposons que nous ayons un arbre d'identifiants de certaines entités, par exemple des commentaires (en mémoire, sous n'importe quelle forme). Cela ressemble à ceci:
|- 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 , . , :
- . , ,
comment
JSON - . ,
Task
Task
WhenAny
/WhenAll
. , , , . , , . — - -
for httpRequest := range webserer.listen() {
go handle(httpRequest)
}
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()
, ? , , , , . , :
func (n *IdTree) LoadComments(ctx context.Context) (*CommentTree, error)
— treeg, ctx := errgroup.WithContext(ctx)
—i, c := i, c // correct capturing by closure
— , , .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 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
, : , — .
, :

— , ?..