Linq and Learning to Be Declarative
Posted3 months agoActive2 months ago
nickstambaugh.devTechstory
calmpositive
Debate
60/100
LinqC#Declarative ProgrammingFunctional Programming
Key topics
Linq
C#
Declarative Programming
Functional Programming
The article discusses the benefits of using LINQ in C# and adopting a declarative programming style, sparking a discussion on the trade-offs and best practices of using LINQ.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
17m
Peak period
34
Day 8
Avg / period
9.5
Comment distribution57 data points
Loading chart...
Based on 57 loaded comments
Key moments
- 01Story posted
Oct 9, 2025 at 10:50 AM EDT
3 months ago
Step 01 - 02First comment
Oct 9, 2025 at 11:07 AM EDT
17m after posting
Step 02 - 03Peak activity
34 comments in Day 8
Hottest window of the conversation
Step 03 - 04Latest activity
Oct 20, 2025 at 9:08 AM EDT
2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45528508Type: storyLast synced: 11/20/2025, 4:26:23 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
I think C# is the best functional programming language because you always have access to a procedural code safety valve if the situation calls for it.
100% purity down the entire vertical is a very strong anti-pattern. You want to focus on putting the functional code where it is most likely to be wrong or cause trouble (your business logic). Worrying about making the underlying infrastructure functional is where I start to zone out.
Does F# care if a DLL it references was coded in a functional style?
It really isn't. The benefit of pure all the way down is that later you can replace bits with something more performant if necessary. But starting out with the idea that some bits should never be pure just means none of it is. The beauty of pure functional programming is that its very compositional nature means that you can replace a component without having a detrimental effect to everything its composed with: as long as you maintain referential transparency for that component.
What we gain from imposing the pure functional constraints on ourselves is genuine composition. If we compose two pure functions into a new function, that resulting function will also be pure. This is the pure functional programming super power that leads to fewer bugs, easier refactoring, easier optimisation, faster feature addition, improved code clarity, parallelisation for free, and reduced cognitive load. Opting out for arbitrary reasons at certain stages of the vertical threatens all of that.
"Functional core, imperative shell" is a great tradeoff position though.
>> Does F# care if a DLL it references was coded in a functional style
Deeper problem: it can't know. It can only assume. I'd have to check how the loader works but it may be the case that "first call to a function in an external DLL" is not stateless (and can error!) because it triggers the linker.
Logging is a side-effect and should be treated as such by using a declarative side-effect type, like an IO monad, or could be tracked for later persistence with a Writer monad.
The oft repeated “Functional core, imperative shell” is just nonsense when you try to do it for real. It’s one of those dogmatic statements that gets dragged out in these types of discussions and adds nothing. This is why effect monads are so useful: there’s no need to create arbitrary boundaries to have effectful and pure code.
Alan Turing already proved the equivalence between lambda calculus and the Turing Machine: anything imperative can be done functionally. There really is no need to create arbitrary boundaries.
In languages like C# it may be pragmatic to do so in certain circumstances, like making ASP.NET request-handlers invoke pure functional computations and return the result of the invocation. This is pragmatic/preferable to writing a competitor to ASP.NET. Outside of situations like that I don’t see any value to an ‘imperative shell’, whatever that actually means.
Luckily you can log functionally.
"Pure functional" isn't a style choice. The judges won't hold up big placards with 10 on them because you executed your business logic in style.
It's a contract that your code will give the same output given the same input. The contract goes both ways: your ability to supply a caller with functional code is made easier by your callees supplying you with functional code.
Someone decides that that's draconian and says "what's the worse that can happen?" and starts mutating in a library dependency. Well now your backtracking parser may or may not be able to backtrack. Your transactional code may or may not be able to be rolled back. Your multi-threaded code may or may not be free of races. `true || f()` no longer means the same thing as `true`. You might need to start scaffolding all your unit tests with @Before and @After to setup and teardown state, and give up running them in parallel.
Maybe you really, really, really need to fire the missiles at the bottom of the call chain. Fine, just mark that method as 'red' so I get a compiler error when I try to serve up 'blue' code to my callers.
As long as you're showing off the syntax (matter of taste) on blog posts you might as well get some mileage out of the semantics (guarantees).
Here's things Clojure, for example, offers:
1. Structural sharing which makes you less likely to have to dig in to procedural code.
2. The transient function which takes an immutable data structure and makes it... not immutable... you can now do your procedural code, then switch back into immutability land with persistent!.
3. If the above fails, you can dip into Java.
So yeah, I vehemently disagree with C# being the best. I could kinda see it if you said F# (type safety), but C# itself? Uhhh.
I really love C# but I will also say that 100% purity is a super power that C# will never have (can't have 'em all).
The final example has signature List<Product> but tries to return IOrderedEnumerable<Product>.
I recognise that this is very much a taster rather than even a full introduction, so the author didn't want to explain IOrderedEnumerable<T>, but please when writing blogs, run through your code examples and make sure they compile.
This means that your audience can follow along.
( It just needs a .ToList() on the end. )
In production, I agree that clarity should come first.
The author stating that the lambda is "better" because it's less lines is also silly. It's been a while since I've written C#, but pretty sure the SQL-like version can be formatted to a single line as well:
List<Product> GetExclusiveProducts(List<Product> source) => (from p in source where p.ProductTitle == "iPhone" orderby p.TypeOfPhone select p).ToList();
What's important to me is that I can understand the intent of the code, that I can reason about how that code will be executed at runtime, and that I can easily debug the code if needed.
Of course, there's no need to go full enterprise Java. Never go full enterprise Java.
But having ten lines of clear code that's easily debuggable and which runtime characteristics is predictable is much better than one dense line that's difficult to untangle, or a few that's hard to predict what will do etc.
I have a couple of samples that I think might surprise you if you think C# must look like Java...
* A game of pontoon [2] - I create a Game monad which is an alias for a monad-transformer stack of StateT > OptionT > IO. This shows how terse you can get, where the code almost turns into a narrative.
* Newsletter sender [3] (which I use for my blog) [4] - This generalises over any monad as long as the trait requirements are met. This is about as declarative as you can get in C#. It's certainly pushing the language, but is still elegant (in my eyes anyway).
[1] https://github.com/louthy/language-ext
[2] https://github.com/louthy/language-ext/blob/main/Samples/Car...
[3] https://github.com/louthy/language-ext/blob/main/Samples/New...
[4] https://paullouth.com
The API docs are all auto-generated [1] so sometimes my repo markdowns get out of sync when I refactor!
[1] https://louthy.github.io/language-ext
Also better to pass IEnumerable to the function instead of List, for the same reasons
Also I forget the syntax but you can use an extension method to add it to linq I think
So from an API perspective, I think returning IEnumerables can end up being too cute. The caller has to deal with this unknown thing.
In general you should be clear about what you return and loose about what you accept. If your whole API is returning IEnumerables and some are expensive to iterate and others are not its actually less clear what I'm getting. And this isn't to say List is always the answer either.
Cases where it isn't:
- risk of multiple execution (there is a CA warning for this)
- when returning data protected by a mutex, you should always materialize a copy of the returned list/array rather than return an enumerable
... and then the author proceeds to presenting clunky, unreadable fluent syntax
That was a good joke for the afternoon.
These patterns are commonplace for many years now in many languages.
The intention was to show how declarative code can come from imperative with a 'true' one-liner.
i've worked with people in the past who refused to allow any actual LINQ in the codebase (use resharper to convert it to fluent!) even though it essentially became obfuscated.
Or are you suggesting that the majority of programmers struggle to read and understand fluent method chaining?
I don’t have a dog in this fight because this blog post is very novice oriented. I’m just genuinely confused why you think it’s unreadable or “clunky”. What is it about the fluent example that you find clunky?
Writing your own LINQ provider is a very niche activity done by people who want to translate or “transpile” C# expression trees into something else.
It is fundamentally a difficult endeavor because you’re trying to construct a mapping between two languages AND you’re trying to do it in a way that produces efficient target code/query AND you’re trying to do that in a way that has reasonable runtime efficiency.
Granted, on top of that, I’m sure LINQ provider SDKs probably add their own complexity, but this isn’t an activity that C# developers typically encourage.
Also, less lines is not a good argument for SQL-style syntax vs method-call syntax. The good argument is that the SQL-style syntax is limited to only a few basic operations, when there are many more useful methods available.
Another reason is that this does not compile:
This method returns IOrderedEnumerable<Product>, not a list. To fix it, you would need to either change the return type, or go outside of the SQL-style syntax and call the ToList method:Also adjacently worth mentioning that I tend to use Dapper instead of EF, so there's even less use of this type of expression all around unless it's actual SQL or assembled SQL via a query builder.
This is a very unfortunate joke: Python has list (and generator) comprehension expression for a long time (2.3?) which are similar to LINQ. At some point in the history many languages stole useful expressions from other paradigms.
Let’s joke on BASIC, it always works.
I love Python, its my main daily driver, both at work and by preference for most of my personal coding, but Python comprehensions and genexps are much more limited than LINQ language level query syntax (Scala’s visually-similar construct is more like LINQ in capabilities) and Python—purely because of core and stdlib convention which also drive convention for the ecosystem, not actual structural features—lacks anything like the method syntax as a common API (unlike, say, Ruby).
EDIT: Thinking about it a little bit, though, it should be possible in theory to implement LINQ in Python without language level changes (including providing something close to but not quite as clean as the language level query syntax[0]) as a library via creative use of inspect.getsource and ast.parse, both for providing the query syntax and for building the underlying expression tree functionality around which providers are built (support for future python versions would require implementing translation layers for the ASTs and rejecting unsupported new constructs). Conceptually, this is similar to how a lot of embedded DSLs in Python for numeric JIT, compiling GPU kernels, etc., from (subsets of) normal Python coded are done.
[0] existing comprehension/genexp syntax looks similar, but relies on simple iteration, not pushing code execution out to a provider which may be doing something very different behind the scenes, like mapping "if..." clauses into SQL WHERE clauses for a database query.
For example, in a game you may have a situation where you maintain a runtime list of objects with a certain property but you want to filter by some other property
For my game it's irrelevant because most linq loops are only triggered by events so there's no loops running on every frame.
For example, lets say you have state machine based AI, and a scripted event triggers tells an enemy starship to change allegiance. The enemy starship then scans a runtime set of enemy factions using LINQ that are within 3 km to identify new targets to attack.
However, in my Java days, I've very much enjoyed using streams. Looking back at LINQ now, it seems like a nice DSL around streams (which probably exist in C# land, too).
Interested in commentary around this!
Looking at that 'one-liner' I get strong perl vibes, or is it chills?..
But it’s also a fine line, when to use it and when not to. While LINQ is easy to read and make sense of, it is far from being easy to debug. Especially on inexperienced teams I tend to limit my LINQ usage and try to write more “debuggable” code. But that’s my approach, I would love to hear other peoples thoughts on it.
In performance-critical code at the time it had to be avoided due to allocations and poor performance compared to loop-based implementations.
However, the new versions of .NET have been reducing this penalty [1], to the point where it might make sense to try LINQ first if it's more concise/clear then only rewrite after profiling!
[1] https://devblogs.microsoft.com/dotnet/performance-improvemen...