As I wrote in the preface of the previous article , I am in search of a language in which I could write less and have more security. My main programming language has always been C #, so I decided to try two languages ββthat are symmetrically different from it on the complexity scale, about which until now I had only heard, but could not write: Haskell and Go. One language became known for saying "Avoid success at all costs" *, while the other, in my humble opinion, is its complete opposite. In the end, I wanted to understand what would turn out to be better: intentional simplicity or intentional rigor?
I decided to write a solution to one problem, and see how easy it is in both languages, what is their learning curve for a developer with experience, how much should be studied for this and how idiomatic is the βnewbieβ code in one and the other case. In addition, I wanted to understand how much I would eventually have to pay for satisfying the Haskell compiler and how much time the famous goroutine convenience would save. I tried to be as unbiased as possible, and I will give a subjective opinion at the end of the article. The final results surprised me very much, so I decided that it would be interesting for the Khabrovsk citizens to read about such a comparison.
And immediately a small remark. The fact is that the expression (*) is often used ironically, but this is only because people pars it incorrectly. They read it as "Avoid (success) (at all costs)", that is, "no matter what happens, if it leads to success, we must avoid it," while the real phrase reads as "Avoid (success at all costs) ", that is," if the price of success is too high, then we must step back and rethink everything. " I hope that after this explanation it ceased to be ridiculous and acquired a real meaning: the ideology of the language requires the proper planning of your application, and do not insert adhoc crutches where they cost too much. Goβs ideology, in turn, is rather βthe code should be simple enough so that in case of changing requirements it would be easy to throw it away and write a new one.
Without further ado, I took the puzzle that was invented by comrade 0xd34df00d and it sounds like this:
Suppose we have a tree of identifiers of some entities, for example, comments (in memory, in any form). It looks like this:
|- 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/
, , , .
- .
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# 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
, ,
? , 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 ) . , (, ), β !
,
, , 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 + " ") } }
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
. , , β . , : β , β , .
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
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)
g, ctx := errgroup.WithContext(ctx)
i, c := i, c // correct capturing by closure
g.Go(func() error
, , .
, ? , . ( ), , (- ), . , , .
, ? , , , . getCommentById
.
.
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 |
2 | 4 | - | |
30 | 10 | - | |
15 | 50 | - | |
β | β | β | |
β | β | β | |
** | ββ | β | β |
~30 | 1 | 1 | |
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
, : , β .
, :
β , ?..