Functional Programming with C# - Simon Painter - NDC Oslo 2020
HTML-код
- Опубликовано: 5 сен 2024
- Functional Programming is becoming increasingly popular and relevant with each year that goes by. With so much discussion around languages such as F#, Haskell and Erlang, it can seem as though getting started with Functional programming would mean first learning a whole new syntax...but what if it didn't?
Most .NET developers are familiar with the use of Linq, and basic constructs such as IEnumerable, Func delegates, arrow functions and ternary expressions, but did you know that you can use all of this to implement some of the most powerful patterns and techniques from the world of functional programming?
This talk will demonstrate how, using only familiar features available in out-of-the-box C#, to write Functional code that is:
* More robust
* Easier to read
* Easier to maintain
As well as these benefits, Functional code is a great enabler for the use of concurrency with Async functions and Serverless applications with technologies such as Azure Functions.
This talk might be of interest to anyone looking into moving to a new platform, or in improving the scalability of an existing application, or even just interested in seeing what Functional Programming is all about, but all within the comfort of a familiar language.
We might even attempt the impossible, and explain what a Monad is!
Best video on functional programming in C# hands down!
You're being far too kind :)
This is exactly what I was looking for, I was going to explore exactly "putting values into a box" to deal with exceptions, all my googling has pointed to returning special type or null and filtering out afterwards, I just wasn't happy with that solution, but this is exactly what I wanted.
Thank you!
The only problem is that people often won't care about that special case. I really like how you have to check both cases in Haskell.
The worst thing one could do is sreturn a valid value on error. For example, if you convert a string to int in C and something goes wrong, you get 0.
It is nice that you think on your own. Outside the box, one could say. :D
Thanks. Was relieved to learn that the temperature conversion was mainly a vehicle to explain monads. I would argue that the classic implementation in that case is favorable because the functional approach gains little while deferring and obfuscating a lot (bind, identity, the Funcs, all things to understand/inspect in case of trouble).
As you say, I just wanted a simple example of a Monad flow, and I didn't want to get bogged down in the complexities of the logic of what I was doing. You're right that I probably wouldn't ever bother to do that conversion in real life with monads, there are probably many more succinct methods. I thought it showed off the process well enough though.
@@madsimonj Yep, I agree it is a nice way to illustrate. A formal description is way harder to wrap your head around IMO. Thanks for sharing your talk.
@@QuiddelQuaddel you're very welcome! It was an honour to be able to deliver it!
I've been adding more of these select blocks and such to my program, but I'm so used to for loops that it's natural to use them.
Foreach is just as readable, if not even more, as long as you split the contents into a separate local function.
For those who don't know, a local function is a recent-ish feature that allows you to make a small function inside of another, whose syntax is identical to a regular class method, minus access modifiers and other strange or unneeded syntax. I use them all over the place in my code.
I do like local functions, and certainly do use them myself. I agree that ForEach loops are perfectly readable, so long as you keep them small. The temptation is always going to be for future developers to add more functionality in, though. There's nothing constraining what you do. Select isn't perfect, but it's much harder to ram in code that doesn't belong there.
beautiful video, thank you so much :)
you're very welcome indeed!
Unity needs a functional API. MonoBehaviour makes it nearly impossible to write functionally.
Helper extensions methods can vastly simplify stuff like currying, partial application, un-currying, function composition, combinators, etc.
absolutely. I've got another talk on that exact subject floating around on youtube somewhere...
9:51 I am so happy that somebody agrees with me. Many students from my university, even those who passed the functional programming course, insist that
# Pseudocode
# Looks bad to me because it requires resetting a variable
def sum(list)
s = 0
for (a : list) s+=a;
return s
end
is much more readable than
(foldl + 0 list)
or
#Pseudocode
def sum(list)
return empty?(list) ? 0 : first(list) + sum(rest(list));
end
I'm just jealous you got a functional programming course! My university was just about up to date with Object Orientation!
I wonder if in the future we will have names like John Developer, Emily Datascientist etc, referring to people's father's line of work
Could be. My surname doesn't quite match my occupation, does it? Ironically, I've recently started to learn to paint. So far as I'm aware, I'm the first in my family to do so!
In validate Nino, you could’ve apply piping to do the replacement only once
validate Nino isn't doing a replacement at all, it's validating that a set of rules have been followed in the character set of the Nino - i.e. only certain characters are allowed in certain positions
Why are you replacing the space with an empty string? I'm in the USA - don't know what a nino is. In the }.All( x => x(nino.Replace...) Does that only happen when the function is true? Does it happen before the function is evaluated? What's happening there?
A NINO is a "National Insurance Number", it's a number given to everyone for working out certain kinds of tax in the UK. They are often written out in pairs of characters with spaces between them - e.g. (this is a made up number) AB 12 34 45 C. For validation purposes, it's easier if I make sure I know the spaces are gone. Strictly speaking the replace is being done every time there. I'd probably use a Map() on this to make it a 2-step operation if I were doing it more seriously, but I'd not introduced Map() at this point.
Your Maybe monad bind's encapsulation of exception handling is far more typical for the Result monad not the Maybe monad.
you have a point there, I was just going with Maybe because it's so well known. I think it tends to be a lot of people's first Monad.
57:05
The validation example is not ideal... re fail on first error validation is painful for the end user.
It's preferable that all the validation errors are accumulated so that the end user to fix them all at once, as opposed to serially fix 1, resubmit and try again to discover the next problem.
Validation Monad is good for this re its applicative supports error accumulation
you're right, and that's easily done by swapping the First for a Select, but I wasn't necessarily going for anything too deeply "real world", I just wanted to give a taste of the Functional way of approaching problems
I'll have a look into that, thanks!
5:23 "A monad is just a monoid in the category of endofunctors"
Source: "Hitler reacts to functional programming"
FP not for the UI? Man, Elm does it like a pro.
(I never wrote Elm, so I can't comment on that)
What he (probably) means is that in functional programming, functions should not fail. If your program fails to read input or produce output, that is a failure and potential crash.
Of cause you can do GUI with functional languages, but then they become slightly less functional as a result.
That's why the few languages that are 100% functional are esotheric.
What @theDarkOne629 says. Creating an entire application entirely of pure side-effect free functions is a pipe dream, unless it's a system with no users and no external interactions of any sort. In which case what would it actually do?
I dont understand why 13:05 should be more readable. Or testable, you're just doing an Equivalent check for the resulting strings or list, which is a terrible check. And whats the Should() function even doing? Where are any of these functions declared or documented?
It gets worse at 15:13 with stuff like _parseData and _getBestStories. Where *is* this code? Its just moved out of visibility, thats not an improvement! You may as well just call them _trustMeDude
Not convincing me there... at all! I dont feel like this is a good introduction to functional programming, especially for people that have never heard about it.
It's fundamentally splitting the implementation of the function and its use. That gives you two things, a meaningful name for the function which you can later reuse. Also the special kind of functions that have no side effects, means that if a test DOES fail you have a failing function somewhere in the pipeline. The biggest benefit to functional programming is the lack of side effects - meaning you won't get as many bugs that happen in very niche scenarios such as function B not working only when running function B after function A.
The readibility aspect is exaclty that, the function states what it does and you trust it, it doesn't matter if it's lying because tests will show that later and you can determine that the bug is not in this function because the pipeline makes sense. It's easier to debug seperate small functions in isolation than it is to debug a for loop with multiple parts that mutate the state.
But I do agree that a good functional programming introduction is learning an actual functional language such as Haskell or any dialect of Lisp because the C# way of thinking is giving way too much freedom to actually learn functional programming correctly.
I think his intent was to demonstrate that by going functional with LINQ, you can achieve composition, encapsulation and separation of concerns with minimal code. These are all desirable properties in OO. From my experience the difference between FP and OO are minimal, it's the naming conventions of patterns that really screw people up. I became a much better OO programmer after learning FP. I still prefer OO, but learning both helped me design better interfaces because of the focus on composition that you're forced to have with FP.
The "Should()" function comes from the FluentAssertions Nuget package. It's an easier read equivalent to something like Test.Assert(x, y); I'd assumed it was common enough not to need explanation, apologies, I'll make sure I explain that in future.
I have a repo on GitHub where you can see the code of those functions, but I'd thought it was fairly self-explanatory, it's pretty much just the same code you've already seen, but captured in a Lambda expression. Again, I'll maybe see about clarifying that in future. I'm not sure whether I've even kept that slide in the current version of this talk that I give.
In fairness, I wasn't especially concerned with *what* the code was trying to accomplish, I was more interested in how the bits of functionality were put together. I'd probably have considered it equally valid as a demo if I *had* put _trustMeDude() in (although there's no real story behind that code), because it doesn't honestly matter for demo purposes. I doubt anyone's interested in literally re-creating the code flow I showed there. This was intended to be a bit more high-level. Likewise with the Centrigrate/Fahrenheit check I demonstrated, there are better ways of doing that, I just didn't want to get bogged down in the specifics of a complicated bit of business logic.
I'm sorry you didn't like the talk, but feel free to reach out to me if you'd like to discuss in further detail. I'm always happy to talk code & Functional Programming is a particular interest of mine.
@@madsimonj If your point is that FP makes code readable, you cannot simply hide code away into functions that are called what the code is going to do. Because changing the implementation of trustMeDude() is going to be exactly as bad - if not worse - than just having the code in-line. And I highly doubt the goal of FP is to make code read like SQL statements, which a lot of people find archaic to begin with, and are actively using frameworks to avoid. The code at 30:00 is not readable, its pure gravel for anyone who isnt familiar with lamba functions, and 47:26 is putting a whole lot of busywork with .Bind(), .ToIdentity() and once again *Lambda functions* around DEAD SIMPLE MATH. What's the point? 7 Lines of code? here's my code: celsius = (fahrenheit-32) * 5 / 9. Stuff like this only makes FP look extremely silly and arduous, its the same as OOP maximalists who turn everything into a class just for the heck of it being an object. You however are turning one-liners into five functions calls and a constructor because apparently, more functions = better code.
@@danielschmider5069 Actually....we kind've are trying to change C# to be more like SQL here. Sort've. SQL and FP have something in common, in that they're both *Declarative* languages, rather than OO, which is Imperative. Declarative languages are more about taking a step back and being less concerned with the order of operations, or the detail of *how* things are done. Imperative is all about being involved with the maximum level of detail when it comes to _what_ is done _when_ and _how_. Think about how in a T-SQL statement, you don't really care which order it executes the list of operations you describe. In fact, the first operation you write - the SELECT, is likely to be one of the last things done.
It might be true that a lot of people find SQL archaic, but it has its fans, and I personally prefer Dapper over EF when I'm developing. However good a tool like EF is, it'll never compare to a well-crafted T-SQL statement. I've worked with developers that even prefer SQL over C#, and put nearly all the logic they can in SPs and SSIS packages. That's a bit further than I'd go with it, but you get the idea.
I'd also hope that any modern c# developer would be familiar with Lambda functions at this point. They've been part of the language since (I think) C# 4, and most of the examples I provided will work with nearly any version of C# likely still to be in production. Maybe with some very minor adjustments.
You are completely right about the Fahrenheit -> Celcius conversion being too simple to bother with a monad structure. If this were the real world, I'd probably write something similar to what you had there. I might even simplify it to a ratio multiplier. Monad operations are something I actually usually use in situations where I'm dealing with external dependencies that I can't be sure will work correctly, as they're out of my control. I used the temperature conversion example because I'm British (and thus love talking about the weather) and because it's easy to understand without getting bogged down in the logic of something that isn't relevant to this topic.
Just -> Some, Nothing -> None
yes, I've heard all sorts of variations on the naming convention. I use both tbh. There doesn't seem to be a single document defining the FP standard anywhere, sadly.
Functional programming is great, I love it.
But when it comes to the real world, you'll write imperative code with mutable variables like everyone else. Because if you don't, you'll have time to take a nap before your program finishes its job. Just look at any LINQ benchmark against manual imperative code, it gets destroyed. In fact, if you look at the guidelines for the Roslyn developpers, one of the first points is literally : do not use LINQ. It's sad, but that's how it is.
Functional programming is a lot more than LINQ, and you don't have to use LINQ to do functional programming. I've rewritten class-based C# code into functional F# and achieved dramatic improvements in performance. There are specific cases where mutable approaches will be faster than immutable ones, but in the 'real world' if elite performance is mission critical you won't be using C# in the first place. For most LOB software, developer productivity and clean, readable, maintainable code trumps performance almost entirely.
@@jasonhurdlow6607 Real world doesn't mean real-time, C# is a very fast language when used right.
Consider a list of 100k objects you need to update. Functionnally you would create another list and make a copy of every object before updating those copies (using the "with" operator with records for example).
Imperative code would just keep the same list and mutate the objects in place (unless it's wrong to do so, ofc). That's the kind of scenario I'm talking about, and the difference in performance is massive.