17 Pieces of C# Syntax That Make Your Code Short
Become a sponsor to access source code ► / source-code-17-c-95476297
Join Discord server with topics on C# ► codinghelmet.com/go/discord
Enroll course Beginning Object-Oriented Programming with C# ► codinghelmet.com/go/beginning...
The syntax of the C# programming language is changing. It has been changing since version 1, and we are still witnessing the addition of more details to it. Have you ever considered why we are getting these pieces of syntax and not some other?
In this video, we will revisit a number of seemingly minor improvements added to the language over the years and draw them to a conclusion: Novel C# syntax makes writing pure functions easy.
It takes time to accept pure functions as a design tool and a lot of practice to make the most out of them. But one thing I promise to you: Once you get there, you will never look back to the old-school imperative coding.
And more: Your code will be way shorter than it used to be. How much shorter? 50-70% on average. That should motivate every programmer to start using the novel C# syntax as intended.
⚡️Chapters:
⌚ 00:00 Intro
⌚ 00:53 Imperative code (100% length)
⌚ 03:17 Object-oriented code (57% length)
⌚ 05:35 Comparing different styles
⌚ 06:45 Functional code (50% length)
⌚ 09:17 Pure functions (40% length)
⌚ 11:58 Conclusion
Thank you so much for watching! Please like, comment & share this video as it helps me a ton!! Don't forget to subscribe to my channel for more amazing videos and make sure to hit the bell icon to never miss any updates.🔥❤️
✅🔔 Become a patron ► / zoranhorvat
✅🔔 Subscribe ► / @zoran-horvat
⭐ Learn more from video courses:
Beginning Object-oriented Programming with C# ► codinghelmet.com/go/beginning...
⭐ Collections and Generics in C# ► codinghelmet.com/go/collectio...
⭐ Making Your C# Code More Object-oriented ► codinghelmet.com/go/making-yo...
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
⚡️ Have a look at our other Videos :
👉 Using GitHub Copilot to Write Complex Code | Step-by-step Tutorial ► • Using GitHub Copilot t...
👉 Coding with GitHub Copilot - Beginner to Master | VS Code Demo ► • A Comprehensive Guide ...
👉 What is Covariance and Contravariance in C# ► • What is Covariance and...
How to Initialize a Clean ASP.NET Core Project with Entity Framework Core and Identity ► • How to Initialize a Cl...
👉 The Null Conundrum: A Guide to Optional Objects in C# ► • How to Avoid Null Refe...
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
⭐ CONNECT WITH ME 📱👨
🌐Become a patron ► / zoranhorvat
🌐Buy me a Coffee ► ko-fi.com/zoranhorvat
🗳 Pluralsight Courses ► codinghelmet.com/go/pluralsight
📸 Udemy Courses ► codinghelmet.com/go/udemy
📸 Join me on Twitter ► / zoranh75
🌐 Read my Articles ► codinghelmet.com/articles
📸 Join me on LinkedIn ► / zoran-horvat
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
👨 About Me 👨
Hi, I’m Zoran, I have more than 20 years of experience as a software developer, architect, team lead, and more. I have been programming in C# since its inception in the early 2000s. Since 2017 I have started publishing professional video courses at Pluralsight and Udemy, and by this point, there are over 100 hours of the highest-quality videos you can watch on those platforms. On my KZhead channel, you can find shorter video forms focused on clarifying practical issues in coding, design, and architecture of .NET applications.❤️
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
⚡️RIGHT NOTICE:
The Copyright Laws of the United States recognize a “fair use” of copyrighted content. Section 107 of the U.S. Copyright Act states: “Notwithstanding the provisions of sections 106 and 106A, the fair use of a copyrighted work, including such use by reproduction in copies or phono records or by any other means specified by that section, for purposes such as criticism, comment, news reporting, teaching (including multiple copies for classroom use), scholarship, or research, is not an infringement of copyright." This video and our KZhead channel, in general, may contain certain copyrighted works that were not specifically authorized to be used by the copyright holder(s) but which we believe in good faith are protected by federal law and the Fair use doctrine for one or more of the reasons noted above.
#csharp #dotnet #functionalprogramming
I would argue that code readability is far more important than reducing the code by 3, 5, or even 10 lines. Not to mention, that the code should be easy to debug. It seems like a challenge to make the code as short as possible which might be suitable for Codewars, but not for production code. So I would use "syntax improvement" that are really syntax improvements - that will show the intent and keep code readable and easy to debug.
I agree, but then I would add that mapping expressions are also more readable than the imperative code. Let alone that they are reducing bug count by an order of magnitude.
i agree, you are missing one point, the bonus way is also easily unit testable by very little and readable blocks
Readability and shortness do not oppose each other. Short codes are each to capture at one gaze; hence improving readability.
@@lordicemaniac Expressions are easier to test than imperative blocks.
@@zoran-horvat that is what i said, at least tried to... that the last bonus form maybe looks harder to read at first, but it is much easier to make very easy to read unit tests
I adore all your lectures, including on Pluralsight. You have truly changed my programming style/vision/life and my code is 10x less buggy than it was 10 years ago! Zoran Guru of Functional Programming! IoC + Interfaces + Linq + SOLID + Functional Programming (no loops, no deep branches, no cyclomatic complexity, only Linq and self-described small methods) == Success. Thanks a lot!
I like how your videos summarize most of the features from any C# versions, especially the new ones. I've seen the "when" keyword before, but never understood its use until now, same for using switches to declare variables.
Designers of C# are constantly watching what kind of code people write. Most of the additions to the language stem from practical needs and their net result is shorter code, i.e. the language saves us from repeatedly writing the same idioms.
@@zoran-horvat Interesting fact I see
I watch your videos time to time, and I have to admit that each time I watch I appreciate more the content, because I understand the code and the use-cases where I would use these techniques. You're a great mentor, I've started learning from your videos in the beginning of my career, now I feel happy to have materialized that knowledge.
As he says; F# is even more readable (preferable?). Here is a variant: let tryParse (input:string) = match System.Int32.TryParse input with | true, v -> Some v | _ -> None let produceSum (input:int seq) = match input |> Seq.toList with | [max; next; _] -> max + next | _ -> - 1 let sumGreatestTwo (input:string seq)= input |> Seq.choose tryParse |> Seq.sortDescending |> produceSum
That removes at least a half of keys required to type the same code.
I like a lot your channel and your functional approach. I've been trying to do my code like this for years
Excellent content. I'm deadly curios about the performance between them.
Zoran Im a active Patreon subscriber and your videos are always helpful!. To be honest your videos make me uncomfortable (in a good way). It pushes me to find my gaps and progress my abilities. Thank you!
Thank you for your support!
Your comment so perfectly describes how I feel also. I really value this mans content
Great video Zoran! The last code presentation is not quite readable for me but I definitely would use functional programming due to its shortness and mutable resistance in general. 🤩
Thanks! It was hard but very exciting!
very interesting video, i myself try to use some more modern syntax of csharp, it is usually an iterative process, balancing human readability, syntax and performance
Your videos are very educational. Keep up the good work!
Awesome techniques. Very educational. Thank you for your effort!
Mind. Blown.
I do the FP styled code most of the time in my daily work, including using method groups as delegates (especially so even). My personal style is to do a bit less in expression bodied methods as i find the fellow who didn't write it lay struggle to read it, also still haven't taken the time to use all the Linq methods i could. Though my past experiences affect that as i used to write code where an IEnumerable was an ungodly memory hog very easily and Linq methods could easily box in not just the obvious variable but half a function chain. Learning off the aversion where appropriate (even the C#12 Linq simply doesn't hold a candle to careful proc code on blazing paths, but those are slow to write without bugs)
I like this condensed abstracted format you have begun making where you iterate on a simple example. It really helps highlighting not just the "how" but the "why". My personal "why" has mostly been a feeling I have had for a few years, but that does not go far in persuading colleagues. Now, a few questions - some could be viewed as thinly cloaked critiques, but they are posed as questions since I might be missing something: 1) In your final iteration, your parameter is repeatedly named `tuple`. Referring to your comment at 3:33 ("name after purpose, not type"), surely this should be `state` (or similar), no? 2) In your switch expression, the second and fourth case differ only on `count` (`1`, `2`, respectively). Could they not be collapsed to a pattern matching on `(var max, _, 1 or 2)`? 3) Your l.50 at 4:22 seems to be sacrificing security for brevity: Dereferencing `all[0]` and `all[1]` three times looks like a bug magnet to me. I would prefer a preceding line with `var (first, second) = (all[0], all[1])`, would you agree? Might there be a way to inline it, so as to get both brevity and security? 4) While using pattern matching switch expression, I find it easy to get lost in the details because the patterns themselves cannot be abstracted away and given names. Surely switch patterns can be used in both "good" and "bad" ways. I would love to see this addressed in a future video.
I agree with your comments. There will be some videos about the switch expressions and pattern patching expressions.
Happy new year!! Now, I can be free to use "when" clause in switch expression body and many thanks for great tutorial. Btw, it seems the seed values for the max and the next in functional variations should be int.MinValue instead of 0.
No need for int.MinValue because there is count set to 0 to invalidate both values initially.
@@zoran-horvat you are right. Actually, what I thought was, those initial values could have made the switch case shorter, for example of Pure version: (var m, _, var c) when num > m => (num, m, c+1), (var m, var n, var c) when num > n => (m, num, c+1), _ => tuple with { count = tuple.count + 1 }, However, after your reply, I found this rule could be applied to all the cases and your intention was to apply the same logic for all.
I think I would've clumsily reduced it to about 6 Linq statements. Something like: var result = items .Select(n => int.TryParse(n, out var val) ? (int?)val : null) .Where(n => n.HasValue) .OrderByDescending() .Take(2) .ToArray(); return result.Length == 2 ? result.Sum() : -1;
You should use .OrderDescending() and result.Sum()!.Value for the code to compile - it's probably the most concise functional solution. There's at least this interesting but rarely discussed factoid that C# automatically lifts operators to nullable which is why this approach works in the first place.
That takes O(N logN) time and O(N) space for an unknown N. Only if requirements guaranteed that N is small could we try this condensed approach. The requirements from the video are open to the possibility of working with extremely large inputs, and so I have implemented an O(N) time and O(1) space algorithm.
The pure method is beautiful! I love the Aggregate method of boiling down an IEnumerable to a single output, it can be used for tons of use cases. My challenge is seeing the pattern. Moving from OOP to FP requires you look at the requirements from a different perspective. Sometimes, when I cannot see the FP solution right away, I will write it in OOP and then refactor until it is something elegant and pure. Thanks Zoran!
I really enjoyed this video. It's a great overview of modern programming and oddly enough I'm currently looking into functional programming to see if I can snatch paradigms and bring them over to C#. My main question as a game developer... do I need to be concerned for performance employing such a style of programming?
Game development has a large part that is performance critical, both in terms of CPU-bound operations and garbage collection. In that respect, most of the advice I gave in this video (and most other videos on my channel) is not good advice for game developers. On the other hand, all the techniques I show are tried and tested in business applications and services, where you can truly cut your codebase in half.
As a unity developer myself, I can assure you that you can effectively use some form of functional programming in game development. Sure making everything immutable and creating copies everywhere is not a good idea when we are working with large objects, but using pure functions in the underlying logic is largely feasible.
I use Option monads and Linq all the time in Unity. It is not a concern unless the profiler tells you so. Most of the time performance issues are in asset memory allocations (uncompressed textures etc)and loading said assets. Async functional code and Profiler are your friends.
@@Bankoru I've just had a google of Option Monads; something I've seen explained by here before and used before without realising. Thanks for the responses on this, actually really helpful 👍
I like using these tricks to shorten my code, but I've noticed that sometimes it has a tendency to make the code less readable, so I try to balance those things
Actually, the game is to train your eyes to see expressions, rather than statements. Once you get over that, you will start reading mappings and expressions fluently, and it will be imperative constructs and block statements that will hurt readability.
@@zoran-horvat Yes! In the end, it's all about conventions. One could easily argue that a for loop makes looping more difficult to understand, because all of the looping logic is contained in one piece of syntax and you need convention around how to interpret the syntax for it to be unambiguous. I for one am very happy to see more and more functional "conventions" make it into languages like C#. Yes, they may seem foreign at first and it takes time to integrate them into your own vocabulary, but they bring so much additional expressivity in the way they convey intent, it's almost crazy to think that I ever coded without them!
@@JaconSamsta Same with me, and my colleagues testify in the same spirit. Once you get there, you only turn back when there is justification: an express request for performance, an in-place algorithm, multipass algorithm, etc.
Wow, just beatuiful, thank you.
I can honestly say that I'm writing code with pattern matching et al. Just the last step with delegates and Linq to make. Awesome Zoran.
I like the last example. I think the difficulty lies in the lack of understanding of the Aggregate method more than the code itself.
That was exactly my point. Once you switch your mind to seeing expressions, you start feeling odd when faced with something that is not an expression.
Aggregate ( TAccumulator seed ,
I like the Aggregate method (i.e. "reduce" everywhere else) but I tend to not use it because let's face it, it's hard to read. And even if you master it, other devs reading your code will struggle.
To be very pedantic, we used the aggregate function but didn’t count the lines of it. Nevertheless, great example for moving from traditional object oriented to functional. Is there a video on map-reduce concept (not just the LINQ functions) in C# already by you? If not please make one. I have always struggled to recall it as it’s not used everyday.
I have actually come back to this video, and honestly I it's incredible just how many fewer memory allocations there are with the functional approach. I ended up settling with this implementation which uses the same memory usage, but is easier for me to read. public static int SumOfTwoLargest(IEnumerable items) { var result = items .Select(item => { int.TryParse(item, out int number); return number; }) .Aggregate((max: 0, next: 0, count: 0), (acc, number) => acc.count switch { 0 => (number, acc.next, 1), 1 => number > acc.max ? (number, acc.max, 2) : (acc.max, number, 2), 2 when number > acc.max => (number, acc.next, acc.count), 2 when number > acc.next => (acc.max, number, acc.count), _ => acc }); return result.count == 2 ? result.max + result.next : -1; }
Can u make video about reflection and attributes im confused in that one
Great content
Hi Zoran. How to find a bug in the middle of a long, chained LINQ expression? Do you use the QuickWatch window or do you use another technique?
Why would you make a long, chained LINQ expression? If there are many operations to chain, I would expect to see them split into logical sections. If an operation makes a complex transform, I would expect to see that transform pulled out into a separate method or a function. The rules of managing complexity in LINQ are the same as in any other portion of code - don't let the code grow beyond a limit you can manage.
You discuss separate domain from infrastructure. My question is what are other names/concepts of infrastructure.
Brilliant.
How good is the last "ulatimate" solution in terms of performance compared to the procedural approach? I see a lot of function calls there.
It is the job of a compiler, especially the JIT compiler, to inline those calls. You should train your eyes to view code through transforms and not through CPU instructions, mostly because anything you imagine will execute on a CPU is probably not true. It is indicative that either approach would accumulate to no more than 0.1% of execution time if execution includes loading the data from persistent storage, and to way under 0.05% of time if producing the response would include network transfer to a distant location. In other words, execution time is irrelevant. Development time, extensibility and bug count are the primary concerns for an engineer.
I was curious too, so ran some Benchmarks. In terms of performance, both "functional style" versions perform better than the more procedural styles. The "Functional" version seemsto consistently perform the best out of the 4, and personally seems the more universally readable of the two functional styles to me, so I'd go with that style personally. They also don't require any allocations unlike the more procedural versions, which tends to be the more common limitation I've come across. | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | |--------------------- |-------- |------------------:|------:|----------:|------------:| | ProceduralMethod | 0 | 2.560 ns | 1.00 | 32 B | 1.00 | | ObjectOrientedMethod | 0 | 2.613 ns | 1.02 | 32 B | 1.00 | | FunctionalMethod | 0 | 1.275 ns | 0.50 | - | 0.00 | | PureMethod | 0 | 12.430 ns | 4.86 | 128 B | 4.00 | | | | | | | | | ProceduralMethod | 1 | 20.769 ns | 1.00 | 112 B | 1.00 | | ObjectOrientedMethod | 1 | 20.779 ns | 1.00 | 112 B | 1.00 | | FunctionalMethod | 1 | 13.387 ns | 0.64 | 40 B | 0.36 | | PureMethod | 1 | 25.185 ns | 1.21 | 168 B | 1.50 | | | | | | | | | ProceduralMethod | 10 | 128.382 ns | 1.00 | 288 B | 1.00 | | ObjectOrientedMethod | 10 | 137.218 ns | 1.07 | 304 B | 1.06 | | FunctionalMethod | 10 | 95.041 ns | 0.74 | 40 B | 0.14 | | PureMethod | 10 | 126.616 ns | 0.99 | 168 B | 0.58 | | | | | | | | | ProceduralMethod | 100 | 1,056.616 ns | 1.00 | 1256 B | 1.00 | | ObjectOrientedMethod | 100 | 1,121.603 ns | 1.06 | 1272 B | 1.01 | | FunctionalMethod | 100 | 903.564 ns | 0.86 | 40 B | 0.03 | | PureMethod | 100 | 1,040.499 ns | 0.98 | 168 B | 0.13 | | | | | | | | | ProceduralMethod | 1000 | 10,481.245 ns | 1.00 | 8496 B | 1.000 | | ObjectOrientedMethod | 1000 | 12,829.850 ns | 1.22 | 8512 B | 1.002 | | FunctionalMethod | 1000 | 10,251.213 ns | 0.98 | 40 B | 0.005 | | PureMethod | 1000 | 11,721.219 ns | 1.12 | 168 B | 0.020 | | | | | | | | | ProceduralMethod | 100000 | 1,378,139.537 ns | 1.00 | 1049162 B | 1.000 | | ObjectOrientedMethod | 100000 | 1,639,344.766 ns | 1.19 | 1049132 B | 1.000 | | FunctionalMethod | 100000 | 1,212,847.956 ns | 0.88 | 41 B | 0.000 | | PureMethod | 100000 | 1,321,638.203 ns | 0.96 | 169 B | 0.000 | | | | | | | | | ProceduralMethod | 1000000 | 17,178,708.152 ns | 1.00 | 8389238 B | 1.000 | | ObjectOrientedMethod | 1000000 | 19,084,339.955 ns | 1.11 | 8389254 B | 1.000 | | FunctionalMethod | 1000000 | 12,412,192.083 ns | 0.72 | 46 B | 0.000 | | PureMethod | 1000000 | 13,565,147.292 ns | 0.79 | 174 B | 0.000 |
I would have thought you will put the tuples into using aliases. And I'm sure there is a reason why. Would you care to elaborate? Thx
Actually, I was thinking about doing that but eventually dropped the idea. Maybe I could play a bit with that in a separate video.
if I saw the last approach in a PR I would nope out
So, your mind is not there yet. That is not a problem - keep learning.
@@zoran-horvat i do understand it... and i'd still reject it. Not because its bad code, but because i'd be the only one on the team that could maintain it, present or future.
@@adambickford8720 I was leading one such team and I have invested a lot in explaining this programming method. One of the members came to me a couple of years later to tell me that the team is still doing it that way, and that they would never trade it for their prior practices. I have never accepted the lack of education among the programmers I worked with. It is fine to not know something - there are tons of technologies I encounter daily and I don't know them. Not knowing is normal. But it is absolutely unacceptable to stick to sub par designs and refuse to learn better. I have done it through my career as a developer, as a team lead, and as a CTO - and it worked every time. But learning must be demanded explicitly. I never left it as an opt-in choice to my colleagues.
@@zoran-horvat we have had very different experiences! My employers have *actively resisted* that kind of solution because it requires 'rock star' devs and has too much risk. They can't afford "ivory tower" solutions that require additional ramp up time educating a team. They want the most fungible cogs possible as they expect the code to far outlive the team. That kind of code becomes some esoteric thing written by the greybeards of yesteryear nobody dares touch. TBC, it's incredibly frustrating to me the answer is to dumb down the solution instead of leveling up the devs. But the reality is I have to fight tooth-and-nail just to get people to use a 'reduce' properly. And when they report 'the PR works, but it's a hack' the boss will choose 'done right now' over 'done right' every time.
The Discord link is broken - is Discord available for everyone? I love you content. Took me a long time to find someone with conceptual understanding and broad vision even though I am beginner at C#. There are a lot of simple solutions on TY but no one give answers to "why that desing? why that solution?". Big thanks!
Try now. I have replaced the link with a redirect link that should always be up to date.
@@zoran-horvat All good now. Thanks!
It reminds me the old days of C. When you open a .h file, y will see one line with 120 characters ( in 80x25) and wonder what it does and how. Sorry, im too old for these: if I need more than 10" to understand a part of a code, this code needs either a comment or refactor it.
Thirty years ago t'was an' it ain't no C these years no more.
@@zoran-horvat '87
Pretty solid code, i can tell that folding through the use of an FSM is kind of a pattern for you.
My worry with C# having so many different ways to do things in the same language is that when you have a shared code base with lots of different people, there will be lots of different "flavours" or styles and implementation techniques using different styles of the language ... i.e its going to encourage inconsistency. But then on the flip side, it makes the language so powerful and means it can be used in more places. So yes, I'm not entirely sure how I feel about the current direction of C#.
It is common nowadays to split large domains into smaller components - services, vertical slices, all sorts of them. I don't expect different styles to flourish within a single component if it is small enough.
Nice approaches but, how about performance between each approach, which is better?
What is performance when the data is loaded from storage, transformed, and then sent over the Internet? Whatever you write, and whichever coding style you adopt, the time your code takes will never approach one percent of the request-response time.
@@zoran-horvat While this is true, if you concede that this approach may not be suitable for something like Game development, and if you are advocating that people should adopt a more functional approach to their C# development - I think spending a minute or so being transparent about the performance of each approach would be welcome, even if it's negligible in 95% of use-cases.
@@conbag5736 The primary target of C# are business applications. Game development, system applications, embedded systems, operating system - you can do most of that in dotnet, but you should know on your own what tradeoffs each includes and how it differs from mainstream programming.
I ran a benchmark for the OOP, Functional and Pure cases. Surprisingly, functional had the best performance, followed by Pure (~18% slower), then OOP(~26% slower). In terms of memory allocation, OOP is surprisingly high (almost x1000 higher than functional in the 10000 strings case). I wish I could just post the table here. I find the pure case more interesting, but certainly less readable. Overall I'd stick to functional in this particular case (better performance/readability/memory) despite it not even using Linq (and it is faster specifically because it doesn't, although performance difference is negligible).
@@Bankoru The first two solutions collect the data into the list, and the last two don't - that is the difference in memory. Regarding CPU, I am not surprised to see the last two solutions coping well because all four solutions execute the same arithmetic operations in the same order, with only minor differences caused by the compiler optimizing them differently. Therefore, under the line, all that counts is the code structure on the screen, and that is where we can benefit the most from more expressive forms - functional design and expressions.
Another great video from Zoran! I do wonder if Microsoft are having ideas of merging C# and F# into a single language one day.
I suppose not. Those are two views on software design.
@@zoran-horvat They are but with the rise in popularity of functional style programming along with what has been added to C# over the years has made me think there could be plans for this one day or to phase one of them out. I'm thinking quite some time in the future - 10+ years.
I would love to see the performance for each method, for example the enumerator will add overhead when lowered to IL.
There is an analysis in the comments. Contrary to what many opponents of modern C#, it turns out that the two functional variants are the fastest. I expected that because I have seen it so many times already.
Hey, I had the same thought so tested it, and wrote about it here: blog.onelivesleft.com/2024/01/modern-c-performance-in-brief.html
In the object oriented example you could reduce the curly braces to 0 if you remove the braces on the for each loops since c# counts the code in both of them as a single line so it is valid. And you also save some lines
In a traditional code formatting style, that is usually considered an extreme and dangerous practice. I have nothing against it, personally.
@@zoran-horvat I also have nothing against it personally because of the fact that it’s in the way I format my code makes it easier for me personally to see what it does but I am also against it in certain cases like if else statements I wouldn’t do it but if it was just a single if statement I usually do it to make it simpler. I do also understand that it may not be the best I understand both sides here
Typed this code out and traced it in debug. Double awesome ! About readability, is it possible to use an alias for the tuple ? The tuple structure is repeated half a dozen times and makes it look busy. I tried an alias but couldn't make it compile, needs C# 12. I converted to a struct using ReSharper and made it immutable but it needs a constructor, deconstruct; just ugly.
I feared you would come with a bug report. I never ran that code while I was working on it :) All I know about its correctness comes from my trust in the development method I applied.
@@zoran-horvat It works great, even with a list of 1 or 0 items and also negative numbers. I'm just saying it's a little verbose with the repetition of the tuple.
Actually, I rewrote it using a SortedSet of the largest numbers instead of a tuple with another aggregate to sum them. IMHO, it's simpler and can be parameterized for how many max values to sum. Same line count. But I appreciate the demo of C# features, for me it's having the wisdom and experience to know how and where to use them instead of procedural style. I wonder also about performance, are there pitfalls to avoid? I need to prototype more before changing production code. Time is the enemy! In theory, I'm not allowed at the laptop in weekends 🙃
@@nickbarton3191 SortedSet removes duplicates. If you wanted to go that way, the SortedList is the right collection to use.
@@zoran-horvat Quite right, depends on the exact requirement I suppose but I didn't think about it.
I love it, but my tech lead hates I even use expression bodied methods.
I know of cases like that...
Late 20th century code - I’m going to borrow that line ! Problem is we get taught to do that then when we get a job, it’s hard to unlearn this.
I know. I've been there, I've done that - for a decade at least. The good thing is that all this I am teaching exists in books. That is how I found about the problem through my career and then gradually improved.
Since I have a habit of benchmarking competing solutions for the same problem before picking one, I benchmarked each approach from this video. I tested 1, 5, 10, 100 and 1000 input items, all random from range (-4096, 4096) - using a fixed seed number for consistency. The last two methods win on memory allocation, which was constant (40 bytes on my machine) - so they scale best in that regard. The third method wins on performance, while the last one was sometimes twice as slow (even slower than the first two). Note that I ran this on a potato (2009 Macbook running Linux Mint), so the results may be different on something less ancient, but I'd expect the relative numbers to be just the same. So I guess the moral of the story is: BenchmarkDotNet is your friend :)
I plan to make a video on benchmarking, too. But you should be aware of the overall operation. Since each of the methods completes in microseconds, and data fetching that involves persistent storage would cost milliseconds, these methods are entirely irrelevant in the holistic performance analysis. Each variant is acceptable, so the decision is with maintainability, flexibility, and other -ilities that we need in software development.
I did some benchmarking as well. I tested list sizes of 10, 100, 10_000, and 1_000_000. I generated a random set of numbers (same set each run, generated outside the benchmarks so won't influence the runtime or memory scores) and stored them in an array (so minimal overhead on enumeration). .NET 8. Ryzen 3700 CPU. BenchmarkDotNet v0.13.12 I think the most surprising was that MagicSum1 was slower than MagicSum0. These are basic syntactic sugar improvements, but MagicSum1 was ~5% (size 10 and 100) to 25% (size 10_000 and 1_000_000) slower than MagicSum0. Meanwhile, memory usage comparisons between MagicSum0 and MagicSum1 were a bit confusing. It varied between MagicSum1 using twice as much memory, to MagicSum0 using twice as much memory. Not sure why things varied like that. MagicSum2 and MagicSum3 were a vast improvement on memory allocation, using almost none regardless of data set size since they don't convert all the strings to a list of ints. However speedwise, MagicSum2 substantially outperformed MagicSum3. MagicSum3 was the slowest of all the algorithms for all data set sizes aside from 1_000_000, where it returned to being on par with MagicSum0. On the other hand, MagicSum2 was always the fastest, ranging from 10% to 20% faster than MagicSum0 (what I used as the baseline). For 1_000_000 data points, the difference between the worst (MagicSum1) and the best (MagicSum2) was 10 milliseconds (MagicSum2 being 35% faster than MagicSum1) and 13 MB of allocations. There was also a 5 millisecond difference between MagicSum2 and MagicSum3. I also tried making a variant which used MagicSum2 as a base, but using explicit if checks to see if the self-assignment of the default switch condition caused any additional performance overhead. The performance of the two variants was basically identical within the noise level, so the "syntactic sugar" penalty we saw going from MagicSum0 to MagicSum1 isn't hurting MagicSum2. Same when switching MagicSum3 to a generic variant. Overall, I'd say that MagicSum2 is the best for performance, memory, and readability. MagicSum3 is OK if you prioritize composability over the other metrics, and I think would also be preferable for generics (as long as performance isn't a major concern). So yes, go for modern code and pattern matching no matter what, but the step towards composed functional programming still needs additional thought based on your particular needs.
@@David-id6jw Thank you for this input. You have got the point that the greatest advantage of the GetMagicSum3 is composability, which is favored more than speed in functional designs. However, its speed primarily depends on how the patterns are selected. I did literally nothing to help the compiler and that is the native result of letting the compiler do it all for us. The principal reason why we don't try performance optimizations in FP first is that the data crunching code is way faster than I/O anyway. Any function that requires I/O before and/or after data processing will take as much time as I/O dictates, with or without optimization of its CPU-bound part.
Hi @zoran-horvat, I'm one of many developers out there that are still struggling to understand and shift our comprehension and perspective into a functional paradigm. I'm also not the brightest one I would say. So my question is, how can we improve ourselves, make it familiar, and to get better in this topic?
Read everything you can grab. Learn every day. I have been doing that for the last 20 years and that has become my way of life ever since.
Awesome
string[] nums= ["1","2","3","4","NAN","77","dude","23"] // modern syntax i prefer is 4 lines int sumlargest2 = nums.Select(x=>int.TryParse(x,out int i32)?i32:0) .OrderByDescending(i=>i) .Take(2) .Sum();
That makes an assumption of a small sequence of strings. Since that assumption is not listed in the requirements, your solution is not acceptable under the current requirements.
F# is a lot of fun.
The primary mistake here is measuring by LoC, which you very well reduce by including both the if statement and the code statement if the same line, which would otherwise be 2 or more lines. This greatly hinders readability, much like how the more "advanced" methods do. The point is not to write clever and short code, but readable and fast enough for your use case. The only thing I can commend for in this video is the showcase of C# features that everyone writing C# should definitely know. But returning magic tuples instead of declaring record structs for them comes against the point of the video, which is showing advanced C# features that people should be using in their code.
I believe my point should have been made from the other end, though I'm not sure if I could communicate it well to the audience: _if_ you choose to design behavior in functional style, rather than object-oriented or procedural, _then_ you will have the novel syntax at your disposal and _that_ will help make your code shorter by a factor of two.
Brilliant implementation! Can I give constructive criticism? Instead of using Aggregate, isn't the classic foreach loop better in terms of performance and readibility code? Something like this code: int MySumMethod(IEnumerable items) { (int max, int next, int count) tuple = (0, 0, 0); foreach (var item in items) { if (int.TryParse(item, out int number)) { tuple = tuple switch { (_, _, 0) => (number, 0, 1), (var max, _, 1) when number > max => (number, max, 2), (var max, _, 1) => (max, number, 2), (var max, _, 2) when number > max => (number, max, 2), (var max, var next, 2) when number > next => (max, number, 2), _ => tuple }; } } return tuple.count == 2 ? tuple.max + tuple.next : -1; } What do you think? Thank you
Once you see why Aggregate is better, you will never look back. Here is what makes it better: it _forces_ you to have an explicit transform. Without it, there is no barrier to stop your codebase from becoming 100% procedural, losing every single benefit you could get from having explicit small, composable transforms.
Hi @@zoran-horvat thanks for your reply. My criticism of the procedural whole is based not on the goodness of the code, which I do not discuss (on the contrary, I like it very much as in your implementation), but on the performance (and in my job I have to control that). If I do a trivial benchmark test between the Aggregate and foreach mode I proposed, I get these results: | Method | Mean | Error | StdDev | Gen0 | Allocated | |------------- |---------:|--------:|--------:|-------:|----------:| | GetMagicSum3 | 253.7 ns | 4.79 ns | 4.48 ns | 0.0253 | 40 B | | MySumMethod | 129.9 ns | 1.63 ns | 1.36 ns | 0.0253 | 40 B | This is one of the problems with this approach: apart from the readability of the code, which can be confusing for the inexperienced, the performance is often inferior (very high number of jumps, stack, etc...) and this is what still stops me in the all-procedural approach. At the moment I prefer a 'mixed' approach.
@@az6876 You are discussing 0.1 microsecond where even the simplest database query or file request that would feed the data in takes at least 1ms. There is no point in optimizing one 10,000th part of execution time.
@@zoran-horvat no, it's not 1ms, but 40% less time. In a realtime context it makes a lot of difference.
@@az6876 LINQ is not made for realtime applications, nor is C# for that matter, not even foreach. And, obviously, it is not 40% - for actual percentage, you must include the data fetching and result dispatch time. If that includes any I/O, as it does, then that 40% drops down to well under 0.1%.
Our brains must be programed to think this way through repetition.
Precisely. I haven't met a programmer who didn't understand this process. But I have heard numerous team leads who say their team members cannot understand it. Those leads believe their colleagues are incompetent.
one line int sumlargest2 = nums.Select(x=>int.TryParse(x,out int i32)?i32:0).OrderByDescending(i=>i).Take(2).Sum();
That algorithm requires O(N logN)) time and O(N) space. You cannot assume that is allowed unless requirements clearly state it is, e.g. by giving an acceptable upper bound for N. The solution from the video makes no such assumptions. It produces the result in O(N) time and O(1) memory.
What about this approach? int SumOfLargestTwo(IEnumerable source) => source.Aggregate( (default(int?), default(int?)), (current, next) => current switch { (var x, var y) when x is null || next > x => (next, y), (var x, var y) when y is null || next > y => (x, next), _ => current }, r => (r.Item1 + r.Item2) ?? -1); // ((x,y)) => (x+y) ?? -1 in the future
Instead of a tuple, I would have used a locally defined immutable record. The tuple requires the definition to appear multiple times, increasing cognitive overhead unnecessarily.
In many places in code it is not a tuple but an assignment to several variables. On the other hand, stepping from ValueTuple to a record class may require careful consideration of performance, especially if it is instantiated many times in a loop.
Good point with regard to performance.
While code readability is an important aspect (Prefer understandable over clever code) - All those syntactical improvements are exactly why modern C# feels so much better than Java. Java is still almost stuck in the imperative phase, while we have a vast array of choices to pick from.
Impressive coding skills. Still, I would not recommend using that kind of advanced c# in an Enterprise project. Simply because all teams members won't have the same skillset - this type of code will be a paria that only one or two developers will dare touching. After 5 years it will be known as the code with technical debt. IMHO, writing the shortest/smartest code is something you do when you have a small dedicated team. In an Enterprise, writing easy to read code is king for long levity.
I am trying to communicate the opinion that code based on expressions, pattern matching, and value mapping is also the simplest and easiest to understand. I have done that in my teams and, once you put things that way, members on the team accept it. You should not fear giving your colleagues an opportunity to learn.
@@zoran-horvat In my experience - things that are truly good, will be simplified by time. In other words, pattern matching syntax and functional program seems immature - but will probably mature and get better tooling support and "better" syntax if they bring business value. Happy new year!
@@Lazzerman42 I am sure these constructs will be even simpler in the future.
Even though I follow the changes carefully I am getting a little bit worried about the direction of the language. Even though this is readable, and we implement somewhat a single responsibility principle, I would argue that the simplicity is getting lost in the syntax. But that’s just me.
Wow
Does the shortened code actually make it run significantly faster? If not then it's a waste of time because all you are doing is making it much harder to read, understand and ultimately maintain. This is why shorthand completely fell out of use, sure it was compact and efficient to write but almost nobody could read it.
The final code is an expression. Your comment indicates that you prefer procedural code over declarative, and that is so 1980s. The sooner you rid that mindset, the better for you, trust me. There is a very simple empirical proof for that. I know many programmers personally who made that step and none of them ever turned back to say that expressions are not readable or something. That tells you should learn to favor expressions, too.
The last version is unreadable to me :(
A matter of training the eye. Trust me, soon enough all code will look like that.
@@zoran-horvat Btw, great thought provoking videos, even if I may not necessarily adopt the style.
Last two xd
😁 have no insightful comment other than the emoji, anyway, thank you man.
Really interesting, but i think it could also work without the need of handling the count variable simplifying the Advance method
The method must distinguish the case when there are less than two items in the sequence from other cases.
I mean something like this: return number > tuple.max ? (number, tuple.max) : number > tuple.next ? (tuple.max, number) : (tuple.max, tuple.next); This should work regardless the value of the count variable
@@banster85 But what about the case when there are no two numbers in the sequence?
@@zoran-horvatin this case the next variable will remain with its initial value 0 so the ProduceSum method can do the check on this variable, if zero returns -1 otherwise the sum
@@banster85 What if the input number is zero?
I am surprised such things are not obvious for people
You can see from the comments how many programmers fiercely oppose this style, holding on to the practices that are decades old.
I generally agree with this channel, but damn I really dislike how unreadable the last 2 methods are. Looks a bunch of mathematical mumbo jumbo. I'd personally settle somewhere between the 2nd and 3rd variant
The ultimate lesson here is don't write your own code, use a library written by someone smarter than you. Unless you want to be the person actually writing a library like that...
I will strike that code in PR review faster than light. It’s not the compiler to figure out the code It’s your F job. 1 - Readability 2 - Performance I swear to good I have enough to all of those who think that having performance issue is acceptable because you write business application.
Performance? Oh, where have you been when another commenter has posted the results of measuring performance of the four methods? Guess what, the two functional methods are the fastest. Now you can take your preconceived opinions back. Better luck next time. Consider this a revenge of the compiler
@@zoran-horvat False the message made by the other user show that a foreach + matching pattern is 40% faster. My points are still valid. It you work in a large team where the code is handle by 3 or 5 person you have a issue of readability and skill level no everyone is interested by MS new sugar syntax. So I Will stay on my position I Will strike that code for corporate use but for you home project go head.
@@oligreenfield1537 How did you remove the part about performance from your opinion? You were very specific that this code is causing a performance issue that is somehow ignored because it is a business application. Now that it turned there is no performance issue, you pretend you never said it. My point is that your preconceived notion of a lacking readability is equally wrong, but you cannot see that from that 1990s trench in which you are dwelling.
@@zoran-horvat just a side - note, I feel 90's apps were much more stable and bug-free (and faster) than today's 😔
@@zzzzz2903 Actually, that is quite untrue. I know, I was there.
The fact that I can, doesn't mean I should.
I would code this 'functionally' in java streams, which are kinda like linq if you squint. I tried to avoid int specific features like `sum()`, I may not have fully understood the reqs: var magicSum = Arrays.stream("421739".split("")) .map(Integer::valueOf) .sorted(Comparator.reverseOrder()) .limit(2) .collect(collectingAndThen( toList(), twoLargest -> twoLargest.size() == 2 ? twoLargest.getFirst() + twoLargest.getLast() : -1 ));
The problem with this solution is that it requires O(N logN) time and O(N) space to complete, where there is no upper bound on N. The solution from the video runs in O(N) time and O(1) space under the same constraints.
@@zoran-horvat I'm going for something that feels straight forward to understand and maps to the problem in an 'obvious' way. This largely avoids the low-level imperative 'machinery' without being so abstract it doesn't readily convey meaning in this problem context. If there's an actual SLA we can certainly do something in our chain to achieve that.
@@zoran-horvat It looks like java added something similar to java 21 but, as usual, it seems more limited. I see what you're doing w/the count, it essentially acts as a state identifier vs explicitly having to match all of the parts in the pattern to infer state. It kind of reminds of using bit flags vs explicit booleans to manage things; clever, but not obvious. var patternMatching = Arrays.stream("32459722".split("")) .map(Integer::valueOf) .reduce( new TwoGreatest(0, 0, 0), (twoGreatest, number) -> switch (twoGreatest) { case TwoGreatest(_, _, var count) when count == 0 -> new TwoGreatest(number, 0, 1); case TwoGreatest(var a, _, var count) when count == 1 && number > a -> new TwoGreatest( number,a, 2); case TwoGreatest(var a, _, var count) when count == 1 -> new TwoGreatest( a,number, 2); case TwoGreatest(var a, _, var count) when count == 2 && number > a -> new TwoGreatest( number,a, 2); case TwoGreatest(var a, var b, var count) when count == 2 && number > b -> new TwoGreatest( a,number, 2); default -> twoGreatest; }, (a, b) -> new TwoGreatest(Math.max(a.a, b.a), Math.max(b.a, b.b), a.count) ); var sum = patternMatching.count == 2 ? patternMatching.a + patternMatching.b : -1;
This avoids the O(N) space issue while still allowing for more than 2 items (does youtube block github links?): var limit = 2; var topN = stream("45432722557".split("")) .map(Integer::valueOf) .reduce( List.of(), (largestNFound, number) -> concat(largestNFound.stream(), of(number)) .sorted(Comparator.reverseOrder()) .limit(limit) .toList(), (a, b) -> concat(a.stream(), b.stream()).toList() ); var sum = topN.size() >= limit ? topN.stream().mapToInt(Integer::intValue).sum() : -1;