HackerReposA FIELD GUIDE TO OPEN SOURCE
ProSponsor the shelf♧ 0
← Back to top all-time
louthy
language-ext
C#2014–2026
louthy / language-ext

language-ext

C# pure functional programming framework - come and get declarative!

20Distinct people
109Mentions
1Explicit recommenders
7.1KGitHub stars
REPOSITORY AT A GLANCE

What to know before exploring

What it is

C# pure functional programming framework - come and get declarative!

GitHub

Where it is used

No documented product use was verified in the last source check.

What people are saying

I take this approach in my language-ext project [1]. I wrote it because as well as wanting to use F#, I had a decade old C# project to maintain, and I wanted to bring…

louthy used a recommendation phrase. One indexed comment matches. This is a phrase match, not a verdict.

No classified warning is attached to this page. Read the discussions before treating silence as approval.

Also in these discussions

Mentioned in the same Hacker News threads. That is not proof they are substitutes.

This week on Hacker News

Nov 8–Nov 15 UTC · Direct repository links and separately labeled related project stories. Indexed through 11/15/2026, 10:01:16 PM UTC.

No direct repository links were found in the indexed HN discussions over the past seven days. Older discussions are available below.

Mentions over time

HN mentions by year109 dated mentions

Observed mentions: 2014-11-19 17:06:14 UTC to 2026-09-16 19:56:17 UTC.

051015202014-01-01 00:00:00 UTC to 2015-01-01 00:00:00 UTC (exclusive): 1 mentions; 1 submissions, 0 comments20142015-01-01 00:00:00 UTC to 2016-01-01 00:00:00 UTC (exclusive): 6 mentions; 0 submissions, 6 comments2016-01-01 00:00:00 UTC to 2017-01-01 00:00:00 UTC (exclusive): 21 mentions; 2 submissions, 19 comments2017-01-01 00:00:00 UTC to 2018-01-01 00:00:00 UTC (exclusive): 14 mentions; 1 submissions, 13 comments2018-01-01 00:00:00 UTC to 2019-01-01 00:00:00 UTC (exclusive): 5 mentions; 0 submissions, 5 comments2019-01-01 00:00:00 UTC to 2020-01-01 00:00:00 UTC (exclusive): 12 mentions; 0 submissions, 12 comments2020-01-01 00:00:00 UTC to 2021-01-01 00:00:00 UTC (exclusive): 8 mentions; 0 submissions, 8 comments20202021-01-01 00:00:00 UTC to 2022-01-01 00:00:00 UTC (exclusive): 8 mentions; 0 submissions, 8 comments2022-01-01 00:00:00 UTC to 2023-01-01 00:00:00 UTC (exclusive): 3 mentions; 0 submissions, 3 comments2023-01-01 00:00:00 UTC to 2024-01-01 00:00:00 UTC (exclusive): 7 mentions; 1 submissions, 6 comments2024-01-01 00:00:00 UTC to 2025-01-01 00:00:00 UTC (exclusive): 10 mentions; 0 submissions, 10 comments2025-01-01 00:00:00 UTC to 2026-01-01 00:00:00 UTC (exclusive): 6 mentions; 0 submissions, 6 comments2026-01-01 00:00:00 UTC to 2027-01-01 00:00:00 UTC (exclusive): 8 mentions; 0 submissions, 8 comments. Incomplete calendar period.2026*

* An incomplete calendar period is marked with an outlined bar. Its count is not directly comparable with a full period.

Counts come from the repository’s dated source evidence. Index cutoff: 2026-11-02 08:16:59 UTC.

View mention counts and date ranges
UTC ranges include their start and exclude their end. Select a period to filter the source evidence.
PeriodStart (UTC)End (exclusive, UTC)MentionsSubmissionsComments
20142014-01-01 00:00:00 UTC2015-01-01 00:00:00 UTC110
20152015-01-01 00:00:00 UTC2016-01-01 00:00:00 UTC606
20162016-01-01 00:00:00 UTC2017-01-01 00:00:00 UTC21219
20172017-01-01 00:00:00 UTC2018-01-01 00:00:00 UTC14113
20182018-01-01 00:00:00 UTC2019-01-01 00:00:00 UTC505
20192019-01-01 00:00:00 UTC2020-01-01 00:00:00 UTC12012
20202020-01-01 00:00:00 UTC2021-01-01 00:00:00 UTC808
20212021-01-01 00:00:00 UTC2022-01-01 00:00:00 UTC808
20222022-01-01 00:00:00 UTC2023-01-01 00:00:00 UTC303
20232023-01-01 00:00:00 UTC2024-01-01 00:00:00 UTC716
20242024-01-01 00:00:00 UTC2025-01-01 00:00:00 UTC10010
20252025-01-01 00:00:00 UTC2026-01-01 00:00:00 UTC606
2026 (partial)2026-01-01 00:00:00 UTC2027-01-01 00:00:00 UTC808

What Hacker News says

12 sources
I've been programming for 34 years - 25 of those professionally. In the early days of my programming life it was super important to know how the code would run on the metal (and much of my time was spent writing assembler for that reason). Processors were slow, memory access was slow, and memory quantity was small (32k on my first computer). I spent a decade in the games industry building graphics engines and hand interleaving assembler to get the maximum amount of juice out of whatever console I was writing for (I beat what Sony said their Playstation 1 could actually do). Then OOP happened and much of the early guarantees about how something ran went away, abstraction everything meant we couldn't reasonably know what was happening behind the scenes. However, that wasn't the biggest issue. Performance of CPUs and memory had improved
Read the rest
significantly to the point where virtual method calls weren't such a big deal. What was becoming important was the ability to manage the complexity of larger projects. The big deal has come recently with the need to write super-large, stable applications. Sure, if you're writing relatively small applications like games or apps with limited functionality scope, then OOP still works (although it still has some problems). But, when applications get large the problems of OOP far outstrip the performance concerns. Namely: complexity and the programmer's inability to cognitively deal with it. I started a healthcare software company in 2005 - we have a web-application that is now in the order of 15 million lines of code. It started off in the OOP paradigm with C#. Around 2012 we kept seeing the same bugs over and over again, and it was becoming difficult to manage. I realised there was a problem. I (as the CTO) started looking into coping strategies for managing large systems, the crux of it was to: * Use actor model based services - this helped significantly with cognition. A single thread, mutating a single internal state object, nice. Everyone can understand that. * Use pure functional programming and immutable types The reason pure functional programming is better (IMHO) is that it allows for proper composition. The reason OOP is worse (IMHO) is because it doesn't. I can't reasonably get two interfaces and compose them in a class and expect that class to have any guarantees for the consumer. An interface might be backed by something that has mutable state and it may access IO in an unexpected way. There are no guarantees that the two interfaces will play nicely with each other, or that some other implementation in the future will too. So, the reality of the packaging of state and behaviour is that there's no reliable composition. So what happens is, as a programmer, I'd have to go and look at the implementations to see whether the backing types will compose. Even if they will, it's still brittle and potentially problematic in the future. This lack of any kind of guarantee and the ongoing potential brittleness is where the cognitive load comes from. If I have two pure functions and compose them into a new function, then the result is pure. This is ultimately (for me) the big deal with functional programming. It allows me to not be concerned about the details within and allows stable and reliable building blocks which can be composed into large stable and reliable building blocks. Turtles all the way down, pure all the down. When it comes to performance I think it's often waaaay overstated as an issue. I can still (and have done) write something that's super optimised but make the function that wraps it pure, or at least box it in some way that it's manageable. Because our application is still C# I had to develop a library to help us write functional C# [1]. I had to build the immutable collections that were performant - the cost is negligible for the vast majority of use-cases. I believe our biggest problem as developers is complexity, not performance. We are still very much working with languages that haven't really moved on in 20+ years. Yes, there's some improvements here and there, but we're probably writing approximately as many lines of code to solve a problem as we were 20 years ago, except now everyone expects more from our technology. And until we get the next paradigm shift in programming languages we, as programmers, need coping strategies to help us manage these never-ending projects and the ever increasing complexity. Does that mean OOP is dead as an idea? No, not entirely. It has some useful features around extensible polymorphic types. But, shoehorning everything into that system is another billion dollar mistake. Now when I write code it always feels right, and correct, I always feel like I can trust the building blocks and can trust the function signatures to be honest. Whereas the OOP paradigm always left me feeling like I wasn't sure. I wish I'd not lost 10+ years of my career writing OOP tbh, but that's life. Is functional programming a panacea? Of course not, programming is hard. But it eases the stress on the weak and feeble grey matter between our ears to focus on the real issue of creating ever more impressive applications. I understand that my reasons don't apply to all programmers in all domains. But when blanket statements about performance are wheeled out I think it's important to add context. [1] https://github.com/louthy/language-ext
In Why Isn't Functional Programming the Norm? [video]
In my language-ext library it's pretty trivial: class Hours : NewType<Hours, double> { public Hours(double x) : base(x) {} } It gives you: * Equality operators, Equals, and IEquatable * Ordering operators, CompareTo, and IComparable * GetHashCode, ToString * Map, Select, Bind, SelectMany, Fold, ForAll, Exists, Iter * Explict conversion operator to convert from the NewType to the internal wrapped type * Serialisation * Hours.New(x) constructor function (often useful when used with other methods that take first-class functions). It's even possible with the more complex variants to provide predicates that will run on construction to constrain the values going in. There's also NumType for integer numeric types like int, long, etc. and FloatType for floating-point numeric types like float, double, etc. They give additional
Read the rest
functionality by default. Also, units of measure [4]: Length x = 10*inches; Area y = x * x; Time t = 10*sec; [1] NewType - https://github.com/louthy/language-ext/tree/master/LanguageE... [2] NumType - https://github.com/louthy/language-ext/tree/master/LanguageE... [3] FloatType - https://github.com/louthy/language-ext/tree/master/LanguageE... [4] Units of Measure - https://github.com/louthy/language-ext/tree/master/LanguageE...
In Be wary of functions which take several parameters of the same type
> Yes, lack of the higher kind polymorphism makes it harder to express these abstract types universally Here's a way of doing ad-hoc polymorphism to implement a more general monad in C#. It's limited in that the return type for Bind can be any monad (although it still type checks). There are a few other issues (not least it's ugly as hell). But, it does allow for building of very general monadic operations (along with other ad-hoc polymorphic types). // Typeclass (ish) public interface Monad<MA, A> { MA Return(A x); MA Fail(object error = null); MB Bind<MonadB, MB, B>(MA ma, Func<A, MB> f) where MonadB : struct, Monad<MB, B>; } // Class instance of Monad for Option public struct MOption<A> : Monad<Option<A>, A> { public MB Bind<MonadB, MB, B>(Option<A> ma, Func
Read the rest
<A, MB> f) where MonadB : struct, Monad<MB, B> => ma is Some<A> x ? f(x.Value) : default(MonadB).Fail(); public Option<A> Return(A x) => new Some<A>(x); public Option<A> Fail(object error = null) => new None<A>(); } // Option 'discriminated union' public interface Option<A> { } public class Some<A> : Option<A> { public readonly A Value; public Some(A value) => Value = value; } public class None<A> : Option<A> { public None() { } } // Testing adding any two M<int> types together. Doesn't need to just work with // ints, it's just a simple example. public class Test { public void Test1() { // Some 200 var r1 = AddAnyMonads<MOption<int>, Option<int>>(new Some<int>(100), new Some<int>(100)); // None var r2 = AddAnyMonads<MOption<int>, Option<int>>(new Some<int>(100), new None<int>()); } public MInt AddAnyMonads<MonadInt, MInt>(MInt ma, MInt mb) where MonadInt : struct, Monad<MInt, int> => default(MonadInt).Bind<MonadInt, MInt, int>(ma, x => default(MonadInt).Bind<MonadInt, MInt, int>(mb, y => default(MonadInt).Return(x + y))); } I have a more complete example in language-ext [1] which unifies monads that take inputs (like Reader and State) and produce output (like Writer) with the more simple monads like Option. [1] https://github.com/louthy/language-ext/blob/master/LanguageE...
In From Design Patterns to Category Theory (2017)
Monads in C# [1] Arr<A>, Lst<A>, Seq<A>, Set<A>, HashSet<A>, Que<A>, Stck<A>, Option<A>, OptionAsync<A>, Either<A>, EitherAsync<A>, Try<A>, TryAsync<A>, TryOption<A>, TryOptionAsync<A>, Reader<Env, A>, Resource<A>, Writer<MonoidW, W, T>, State<S, A>, Task<T> (extensions), Nullable<T> (extensions), Validation<Fail, Success>, Validation<MonoidFail, Fail, Success> + Monad transformer stack [1] https://github.com/louthy/language-ext/
In From Design Patterns to Category Theory (2017)
It doesn't feel very declarative in Rust. Personally, I'm finding it hard to see the intent (I haven't written a line of Rust in my life, so take that with a pinch of salt, but I am a polyglot programmer). Really, Haskell's do notation is the big winner when it comes to parser combinators, as the direction of the flow of the parser is easy to follow, but also you can capture variables mid-flight for use later in the expression without obvious nested scope blocks. It's possible to capture variables with `and_then` by the looks of it, but any suitably complex parser will start to end up quite an ugly mess of nested scopes. I ported Haskell's Parsec to C# [1], it has LINQ which is similar to Haskell's Do notation. Simple parsers [2] are beautifully declarative, and even complex ones, like this floating point number parser [3], are trivial t
Read the rest
In Learning Parser Combinators with Rust
One thing that is missing from Go is computation expressions (in C# 7.0 async/await was generalized to a computation expression). Computation expressions are extremely powerful feature in F# which introduced the idea of async/await. If it is generalized then you can properly handle code failures better. In the Go community they have found returning the result/error tuple is tedious enough that people don't properly handle their errors. I would argue this is because of the lack of generics and computation expressions. In F# I can do something like this: let result : Task<Result<MyObject, Error> = asyncResult { let! validatedObject = validateObject object let! dbObject = GetSomethingFromTheDatabase object return dbObject } doSideAffectLastCodeAndHandleAnyErrors result This lets you create little DSLs and code
Read the rest
that you can handle errors in a central location and in your main code just get the work done without thinking about it. Makes for a really great workflow. See also https://github.com/louthy/language-ext
In Show HN: Muon, a low-level programming language inspired by C, C# and Go

MAKE YOURSELF AT HOME

A few shortcuts

/
Search the collection
?
Open this guide
Esc
Close dialogs and navigation

Your saved repositories and interests stay in this browser.