
كما كتبت في مقدمة المقال السابق ، فإنني أبحث عن لغة يمكنني من خلالها الكتابة بشكل أقل والحصول على مزيد من الأمان. لطالما كانت لغتي الرئيسية في البرمجة هي لغة C # ، لذلك قررت أن أجرب لغتين مختلفتين بشكل متماثل عنهما من حيث التعقيد ، وهو ما سمعت عنه من قبل فقط ، لكن لم أتمكن من الكتابة: Haskell and Go. أصبحت إحدى اللغات معروفة بقولها "تجنب النجاح بأي ثمن" * ، في حين أن اللغة الأخرى ، في رأيي المتواضع ، هي عكس ذلك تمامًا. في النهاية ، أردت أن أفهم ما الذي سيتبين أنه أفضل: البساطة المتعمدة أو الصرامة المتعمدة؟
قررت أن أكتب حلاً لمشكلة واحدة ، وأرى كم هو سهل باللغتين ، وما هو منحنى التعلم لمطور لديه خبرة ، وكم ينبغي دراسته لهذا الأمر ، وكيف أن المصطلح الاصطلاحي هو رمز "مبتدئ" في واحدة والحالة الأخرى. بالإضافة إلى ذلك ، أردت أن أفهم المبلغ الذي سأدفعه في النهاية لإرضاء المترجم Haskell والوقت الذي ستوفره راحة goroutine الشهيرة. حاولت أن أكون غير متحيز قدر الإمكان ، وسأقدم رأيًا شخصيًا في نهاية المقال. لقد أدهشتني النتائج النهائية كثيرًا ، لذا قررت أنه سيكون من المثير للاهتمام أن يقرأ مواطنو خابروفسك عن مثل هذه المقارنة.
وعلى الفور ملاحظة صغيرة. الحقيقة هي أن التعبير (*) غالبًا ما يستخدم بشكل مثير للسخرية ، ولكن هذا فقط لأن الناس يحللونه بشكل غير صحيح. قرأوه كـ "تجنب (النجاح) (بأي ثمن)" ، أي "بغض النظر عما يحدث ، إذا كان يؤدي إلى النجاح ، فيجب علينا تجنبه" ، في حين تقرأ العبارة الحقيقية كـ "تجنب (النجاح على الإطلاق) التكاليف) "، وهذا هو ،" إذا كان سعر النجاح مرتفعًا جدًا ، فيجب علينا التراجع وإعادة التفكير في كل شيء. " آمل أنه بعد هذا التفسير ، لم يعد أمرًا سخيفًا ووجد معنى حقيقيًا: تتطلب إيديولوجية اللغة التخطيط السليم للتطبيق الخاص بك ، وعدم إدراج عكازات مخصصة حيث تكلف الكثير. أما أيديولوجية Go ، فهي بدورها "يجب أن تكون الشفرة بسيطة بدرجة كافية بحيث يكون من السهل التخلص منها وكتابة واحدة جديدة في حالة تغيير المتطلبات.
تقنية المقارنة
بدون المزيد من اللغط ، أخذت اللغز الذي اخترعه الرفيق 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 , . , :
- . , ,
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
, : , — .
, :

— , ?..