Why F# evangelism isn't working (2015)
ericsink.com
ericsink.com
Well this is a blast from the past.
Back when I wrote this, I kinda hoped F# would surprise me and gain more traction than I expected. But 8 years later, if anything, it seems like the dominance of C# in the .NET ecosystem has grown.
F# is still a great language, but the main fact hasn't changed: C# isn't bad enough for F# to thrive.
Learning, how to use an Object System (a tree of objects/classes) is inherently hard
The current problem with F# is that it doesnt do enough to shield you from objects, it does what it can, but still to use F# effectively, you still need to learn some C# and a lot of API that basically Objects inside Objects inside Objects calling Objects calling Objects and more Objects
OOP is bad because eventually OO systems becomes too complex, OO API is intimidation
Separating Data from Behavior manages complexity better
If the only flaw in C# is knowing which method calls requires the new keyword because its a constructor, and which dont because its a factory, that is bad enough to want to avoid it
dog = dog_create({name: "foo", age: 12})
dog = dod_add_friend(dog2)
print(dog["friends"])
and we can avoid OO completely.oh... wait a minute
let dogBar = {Name = “bar”; Age = 11; Friends = []}
let dogFoo = {Name = “foo”; Age = 12; Friends = [dogBar]}
printfn “%A” dogFoo.Friends
The advantage is that it’s immutable and it’s guaranteed to don’t have null in any fields.
C# only introduced records recently, while F# was born with them.
And C# still hasn’t got ADT because it’s missing the Union types as far as I remember.I would characterize it a bit differently, seeing as, for example (and to your point), a purely functional lisp program is a tree of lambdas and macros. The same could be said of Haskell.
For me the issue is that classes and objects are actually pretty complicated things for what they are. It’s easy to not notice when you’re in the habit of using them, but really pause and think about how complicated they are. They have both structure and machinery that probably aren’t required for most abstractions: regardless, in OOP they get shoehorned into every problem.
This is why OOP ends up with a bunch of well known design patterns, whereas in FP they’re not reaaaaally a thing (arguably).
A tree of functions is probably the simplest possible way to build programs, at a fundamental level: I am not speaking in terms of individual preferences here, but really mathematical simplicity.
i am not saying we should not use trees ever, i am mainly saying, when the model is a very deep tree (or several deep trees and trees everywhere), its becomes overly complex
data models should be as flat as possible , and only nested when absolutely necessary
According to debugging tools in my web browser, your <div class=comment> is at level #15 under the <body> element. I wonder how would you model the in-memory representation of this web page, while keeping the model practical?
To your point, if the domain is a payment system, I can keep separate db's of Customer Info, Customer Purchases, Transaction Instances, Customer payment methods, etc. This seems like a domain suitable for functional code.
If the domain is a two stage orbital rocket, in which we must have a stateful system that has internal feedback loops (fuel consumption, vehicle trim, time of flight, time before stage separation, engine sensor data), our best software design is an object graph which causes spaghetti code ( does the navigation system belong to the electrical system, or the radio system? Wait, does the radio system belong to the electrical system? Wait, does the entire electrical system belong to the solid fuel system, since the electrical system is dependent on the generators partially, but what about the battery system? What critical components stay on the battery system if the generators are shut down?). I guess my point is, real life is a spaghetti relationship.
Consider the recent ISpace probe crash. The article says "software bug" but in reality it's more of a 'design flaw' and I would bet it's exactly because of the topic of this thread. The sensors were reading correct data, but the design/validation of the intercommunication data between sensors was designed wrong.
https://www.nytimes.com/2023/05/26/science/moon-crash-japan-...
And, yes, in theory this adds cycles which aren't allowed. However, since class dependencies are better represented as directed connections (which aren't usually used for trees in CS terms), it isn't a true cycle.
OO is one way to structure and model a system.
No matter what language you use will end up with some form of a struct, a set of values that belong together Then you will have list of some structs and trees of some structs
You will almost certainly have to create list/collections/groupings of structs. Because those are quite useful and universal
How you act on those collections is different between different idioms.
In other words you will create a model of data one way or another and you have to maintain it / change it, as required over time.
The data structures themselves are rather often based on or more database schema where the data will be extracted and saved.
A list: I give you a vector. Plain and simple. Not this insanity: https://referencesource.microsoft.com/#mscorlib/system/colle... [0] You do not need OO to create a vector (or even an array -- god forbid!)
As for trees: roll your own. They're simple enough, yet tightly-coupled with context that no generic implementation exists that is flexible enough. You do not need OO to create a tree. C has been working with trees long before the current Frankensteination of OO was even a twinkle in Gosling's eye.[1]
Data structures do not need inheritance -- they might need delegation (message passing that requires you to actually think about your system).
Data structures do not need encapsulation -- they most likely need namespaces. Realistically, most classes will be used as namespaces.
Data structures do not need polymorphism -- just implement the members you need, and name them appropriately (no 5+ word phrases, please. Please!)
What modern OO does is lower the barrier to productivity in the present, and then pays for it in the future. It's no different than writing your "planet scale" backend system in JS.
[0] Compare to: https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a0111...
[1] If you want to know why we have Java: some guys that didn't have the time to think about low-level (memory management specifically) things for their embedded applications, got sick of trying to learn C++, decided to make their own language. That's it. There was no grand plan or thoughtful design -- it's just a mismash of personal preference. The same people that described C++ as "being too complex" (fair) and using "too much memory" (lol)
I'm not a C# programmer, but the public API looks sound, and the entire thing is like 1K LOC including docstrings (I guess the inherited code would add to that).
https://referencesource.microsoft.com/#mscorlib/system/colle...
https://referencesource.microsoft.com/#mscorlib/system/colle...
Instead we have to go diving through the IList, which implements ICollection, which implements IEnumerable, which implements IEnumerable (again). Just because each interface is composed of another interface, doesn't mean you aren't using inheritance. You are effectively creating a custom inheritance tree through willy-nilly composition.
It is gratuitous to make this chain so deep, when the underlying code is just a handful of lines.
https://referencesource.microsoft.com/#mscorlib/system/colle...
https://referencesource.microsoft.com/#mscorlib/system/colle...
https://referencesource.microsoft.com/#mscorlib/system/colle...
----------------------------------------------------------------------------------------------------------------
The doc-strings are unnecessary. It's self-evident what most of the code does if you read it.
// Returns an enumerator for this list with the given
// permission for removal of elements. If modifications made to the list
// while an enumeration is in progress, the MoveNext and
// GetObject methods of the enumerator will throw an exception.
//
public Enumerator GetEnumerator() {
return new Enumerator(this);
}
// Returns the index of the last occurrence of a given value in a range of
// this list. The list is searched backwards, starting at the end
// and ending at the first element in the list. The elements of the list
// are compared to the given value using the Object.Equals method.
//
// This method uses the Array.LastIndexOf method to perform the
// search.
//
public int LastIndexOf(T item)
{
Contract.Ensures(Contract.Result<int>() >= -1);
Contract.Ensures(Contract.Result<int>() < Count);
if (_size == 0) { // Special case for empty list
return -1;
}
else {
return LastIndexOf(item, _size - 1, _size);
}
}
// Returns the index of the first occurrence of a given value in a range of
// this list. The list is searched forwards, starting at index
// index and upto count number of elements. The
// elements of the list are compared to the given value using the
// Object.Equals method.
//
// This method uses the Array.IndexOf method to perform the
// search.
//
public int IndexOf(T item, int index, int count) {
if (index > _size)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
if (count <0 || index > _size - count) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
Contract.Ensures(Contract.Result<int>() >= -1);
Contract.Ensures(Contract.Result<int>() < Count);
Contract.EndContractBlock();
return Array.IndexOf(_items, item, index, count);
}
If you remove these 300 lines of pointless comments, you still have 900 lines of code that is terribly space-inefficient. Everything is "pretty," but slow to read, because of the immense amount of whitespace, nesting, and lines longer than 76 chars. You cannot read long swathes of code in one screenful. You have to scroll vertically and horizontally, because for some reason a standard library needs to throw exceptions (exceptions aren't free; they negatively and noticeably impact performance).Seriously, you could just use an "out" errno/status. "But then we would have to always check to see if the operation succeeded!": exceptions make people lazy. Just because an exception wasn't thrown, doesn't mean you're doing things correctly.
----------------------------------------------------------------------------------------------------------------
Why does a List implement a search algorithm? Why binary search of all things -- because it's convenient? You know if I need a binary search, I can write one myself. Don't pollute my namespace.
// Searches a section of the list for a given element using a binary search
// algorithm. Elements of the list are compared to the search value using
// the given IComparer interface. If comparer is null, elements of
// the list are compared to the search value using the IComparable
// interface, which in that case must be implemented by all elements of the
// list and the given search value. This method assumes that the given
// section of the list is already sorted; if this is not the case, the
// result will be incorrect.
//
// The method returns the index of the given value in the list. If the
// list does not contain the given value, the method returns a negative
// integer. The bitwise complement operator (~) can be applied to a
// negative result to produce the index of the first element (if any) that
// is larger than the given search value. This is also the index at which
// the search value should be inserted into the list in order for the list
// to remain sorted.
//
// The method uses the Array.BinarySearch method to perform the
// search.
//
public int BinarySearch(int index, int count, T item, IComparer<T> comparer) {
if (index < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
if (_size - index < count)
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
Contract.Ensures(Contract.Result<int>() <= index + count);
Contract.EndContractBlock();
return Array.BinarySearch<T>(_items, index, count, item, comparer);
}
What if my list -- as is almost always the case -- is unsorted? The result will be incorrect? Looking through the chain of indirection, I cannot see any code checking to see that the list is sorted. Maybe it's there, but it's so much overhead trying to make sense of the List.BinarySearch -> Array.BinarySearch -> ArraySortHelper<T>.Default.BinarySearch -> arraysorthelper.BinarySearch -> arraysorthelper.InternalBinarySearch chain. So I'm going to silently get a wrong result, and the only way to know is to read the docstrings? Thanks.----------------------------------------------------------------------------------------------------------------
As far as I can tell, it's unoptimized. It's just plain, OO C# meant to be readable. I don't see any tricks or tweaks to get the IL to be more concise/performant. Maybe the compiler is aggressively optimized for the core lib (but I'm not holding my breath -- because I can't see it).
1. Optimization is primarily handled by the .Net JIT, not the C# compiler. That allows F#, C#, VB.Net and other runtime languages to share similar performance characteristics without duplicating effort.
2. Docstrings are used by the IDE to help the user. That avoids the need to read the source code itself for regular usage.
3. When comparing the .Net List<> implementation against any C++ std::vector implementation, the former looks quite tame in comparison...
With functional languages, it tries to enforce some explicit type signatures in the function arguments so things are cleaner within the functions themselves.
Const-ness is one of the things I really miss from C++. I could look at an object and be reasonably sure I wasn't mutating it by calling foo.length() for example.
Sometimes I want to add one little extra thing that gets mutated in an otherwise "const" method, e.g. for debugging purposes. If the compiler doesn't let me do that because I valued the ideal of const-correctness higher than practical concerns, I know I've done something wrong.
In the vast majority of cases it reduced my cognitive load significantly because I could just look at the method declaration and see that my code would be fine.
However, IME there is a big problem with const for more database-y, more stationary in-memory data. This is the kind of data that is almost always going to be mutated by at least some part of the code at some point in time. There is a fundamental problem of communication between mutating code and non-mutating code (the "strstr()" function, that has to apply a const-cast hack internally to implement its interface, is a trivial example here).
As said there are certainly situations where such "communication" isn't needed, but I'm anxious about precluding the possibility in the name of const-correctness.
I feel that instead of const (or whatever static formalized description of what a function is doing), good naming is most helpful to intuit broadly what that one function was doing again.
In C at least, I've ended up leaving const almost exclusively for the cases where the data is truly const - i.e. in the .ro section of the binary, and I know for sure it won't ever have to be modified, and basically I have to apply the const qualifier lest it puts the data in the wrong section / it needs an awful cast to remove the const. The majority of those are string literals typed as "const char *".
Structs have the issue you describe and they aren't really OOP.
Yes, if the first thing you do when you write a class is make a setter method for each field then you will have problems. That's not really a property of OOP.
All of these logical errors that are easy to commit are terrible because they are usually runtime bugs, not compile time.
As I think of it, I think a neat feature of OOP would be conditional methods that are only callable under specific circumstances. For example, the “Customer.SendPasswordResetEmail()” method couldn’t be called (or didn’t even exist) until I verify that the “Customer.IsEmailVerified” property is true.
Being able to add these type of annotations to methods for expected object state would help catch some logic bugs at compile time.
This strikes me as a sort of ... reverse of survivorship bias.
You look around and see all complex systems are in OO, then you conclude that it is OO that is the cause of the complexity.
Have you considered that the non-OO designs are deficient in some way that prevents them from being used for the type of systems that you find to be examples of OO being bad?
Not that I am defending OO, I just want to know how you are differentiating between "OO produces complex systems" and "OO is used for complex systems".
I don't think that people people are really considering Smalltalk's OOP style when they complain about Java OOP.
Having shipped both significant non-OO projects and significant OO projects, their drawbacks were usually related to low adoption. In terms of code and architectural complexity, they were either comparable to OO projects (in specific situations), or better.
That being said, in most situations, language/paradigm choice were not the main drivers of project success. At worst, a bad OO codebase is a drag, not a killer, and the same is true with non-OO projects.
> Not that I am defending OO, I just want to know how you are differentiating between "OO produces complex systems" and "OO is used for complex systems".
OO definitely produces complex systems. And, let me be clear, by OO I mean the social consensus in OO circles, not the paradigm itself or the technical tools. My take is that OO circles host a cottage industry of consultancies and gurus peddling a stream of design patterns, advices, etc. which end up layering in any long-lived OO codebase and create unnecessary complexity.
This right here. Every time I hear mid level dev bring up DDD I contemplate quitting and spending some time looking for a Rust or Clojure gig. Sometimes it gets so bad I think about biting the bullet and going to node.js
C# isn't a bad language, even the frameworks are taking a nice turn towards simplification (eg. ASP.NET Minimal APIs, EF direct SQL queries) but the culture it creates... LAYERS of bullshit :D
There is a whole industry peddling OO systems that are extremely abstracted for the benefit of filling chapters in a book, or producing extra pages of content in a website. I fell victim to both early on in my knowledge and even professional world, but somehow managed to follow what "felt right" and broke away from that to find an easy path forward that allowed me to use the tools I was given in the easiest way possible, and only introduce complexity when the solution was complex (not for the sake of complexity for complexity's sake).
Obviously it's not a direct apples-to-apples comparison; Clojure is an untyped language, and performance for it is admittedly generally a little more difficult to predict. But, and obviously this a sample size of one, but I do feel like my programs have less... "fluff" than the equivalent OOP languages.
The general anxiety of the movement towards functional or procedural programming in general might also be a feature of age: a young programmer eager to impress that they can juggle 8 balls effortlessly, but called upon to do the same 15 years later might admit 3 balls sufficed to begin with, and is closer to an attainable sustainable solution.
I'm sure this is just an example popping first out of your mind, but it seems like an oddly specific thing to mention. Specially since the answer is obvious if you know C#: the name of the method matches that of the type if and only if it is a constructor.
I won't comment on the rest of your post as my experience with F# is minimal; but I think I understand where you're coming from.
> Separating Data from Behavior manages complexity better
There's a sweet spot, and it varies. Sometimes it is difficult to find. API design can be difficult. Managing complexity is sometimes itself a complex process.
They would make a mess in Modula-2, or Standard ML as well, given how many need a network layer to write modular code.
Why? because Linq is basically just syntactic sugar for regular IEnumerable methods, while discriminated unions have no equivalent at all.
Even if you wanted to claim that those IEnumerable methods ARE linq, then it would still be possible to implement them with a library while discriminated unions have to be a compiler feature.
The C# compiler "duck types" LINQ so you can already (ab)use LINQ for general computation in C#. You can use nearly any Monad you want with LINQ syntax. It isn't always a strong fit for some types of Monads, but it is more capable than it seems. You might get some funny looks if you do, though.
(Similar with async/await: it is "duck typed" at compile time so you can write other Monads for that, if they make more sense in that form of transformer than LINQ. Or support both LINQ and async/await together.)
There's definitely some more interesting power in F#'s Computation Expressions that can't easily be done even with (ab)using the tools that already exist like that, but it is still interesting what can be done with the existing tools.
What do you mean by this?
"But remember that you need the TemplateExplicative and NullUnderstanding compiler extensions!"
brilliant! lol
And I would really like if OCaml would have had the possibility to add the needed PPXs names to the source file (like Haskell's compiler extensions). So as to not have to read the Dune (or whatever build system is used) file to get to know where `foo%bar` or `[@@foo]` is coming from and what is doing. But at least the usage of `ppxlib` nowadays should make PPXs "compose" aka. not stamping on each other's feet.
https://ocaml.org/docs/metaprogramming http://ocamlverse.net/content/metaprogramming.html
The syntax superficially looks a lot like OCaml, but it doesn't do the cool stuff with OCaml functors and modules; you write it a lot more like Haskell most of the time.
Don Syme began with a port of Haskell to .Net, but SPJ convinced him that this is a bad idea, so he did choose OCaml. ("The Decision to Create F#", Page 9)
[1] https://hackage.haskell.org/package/base-4.18.0.0/docs/Debug...
No need for extensions, just compile and run your program slightly differently. That's the power of declarative languages.
Also, a link to a useful library is not a bad thing for anyone curious about Haskell. Thus, there's a bit of education there.
If it looks like improv, I am here every evening till Friday.
I also object to that "everywhere". It is far easier to just dump an extra 'print' line somewhere inside a for-loop than into a `foldl (*) 1 $ map (+ 3) [17, 11, 19, 23]`. And that is an easy one...
I develop with Haskell professionally. That foldl example of yours is pretty rare and usually dealt with the QuickCheck [1], mother of all other quickchecks. Usually, the trace will be outside of the foldl application, but you can have it there in the foldl argument, of course.
So actually not everywhere. And QuickCheck does something else entirely.
The foldl example of yours should be split into a property checking and controlling for expected properties of the input. The first part is done via quickcheck and second part usually is done with assertions and/or traces.
But nothing preclude you from having your trace there, inside foldl argument. This is clearly wrong place to have it, but still you can have it there.
So I repeat, you can have your traceEvents everywhere.
The "correct" answer to `foldl` would be `scanl` and printing the result of it.
https://hackage.haskell.org/package/base-4.18.0.0/docs/Prelu...
In my opinion, eventlog traces make much better logging than almost anything I've seen.
Right now, developing with C++, I miss the power of Haskell's RTS.
Haskell's (GHC's) Eventlog is nice, but a binary format and not comparable to text output.
When I was at Jet and Walmart, I never really felt "limited" by F#. The the language was extremely pleasant to work with, and I think most importantly, it was opinionated. Yeah, you can write Java/C#-style OOP in F# if you really want, but it's not really encouraged by the language; the language encourages a much more Haskell/OCaml-style approach to writing software.
Even calling C# libraries wasn't too bad. MS honestly did a good job with the built-in .NET libraries, and most of them work without many (or any) issues with the native F# types. Even third-party libraries would generally "just work" without headache. .NET has some great tools for thread-safe work, and I'm particularly partial to the Concurrent collections (e.g. ConcurrentDictionary and ConcurrentBag),
I also think that F# has some of the best syntax for dealing with streams (particularly with the open source AsyncSeq package); by abusing the monadic workflow ("do notation" style) syntax, you can write code that really punches above its weight in terms of things it can handle.
Now, on the JVM side you something like Scala. Scala is fine, and there are plenty of things to love about it, but one thing I do not love about it is that it's not opinionated. This leads to a lot of "basically just Java" code in Scala, and people don't really utilize the cool features it has to offer (of which there are many!). When I've had to work with Scala, I'm always that weirdo using all the cool functional programming stuff, and everyone else on my team just writes Java without semicolons.
But the basic point of the article does make a reasonable point; part of the reason that Scala has gotten more traction is because Java is just such a frustrating language to work with. Scala isn't perfect but being "better than Java" is a pretty low bar in comparison.
C# is honestly not too bad of a language; probably my favorite of the "OOP-first" languages out there. The generics make sense, the .NET library (as stated before) is very good, lambdas work as expected instead of some bizarre spoof'd interface, there are some decent threading utils built into the language, and it's reasonably fast. Do I like F# more? Yeah, I think that the OCaml/Haskell style of programming is honestly jsut a better model, but I can totally sympathize with a .NET shop not wanting to bite the bullet on it.
https://github.com/scala/scala-xml/wiki/Getting-started
The "opinionated" Scala are the Typelevel and Zio stacks, which are very cool.
The problem with the "better Java" approach is that although it has helped Scala's growth a lot, it has also made it susceptible to Kotlin. The Scala code that doesn't use the advanced type magic can be straightforwardly rewritten in Kotlin instead. Kotlin also stops your bored developers from building neat type abstractions that no one else understands.
People who use Scala only has a "better Java" can now use Kotlin has a "better "better Java"".
People who are writing Clojure likely aren't looking at Kotlin as an "alternative"; while they superficially occupy a similar space, I don't think Clojure has any ambitions of being a "better Java", but rather a "pretty decent lisp that runs on the JVM with some cool native data structures and good concurrency tools". I do like it better than Java, but that's because I like FP and Lisp a lot; if I needed a "better Java" right now, I would unsurprisingly probably reach for Kotlin.
Kotlin's whole point is a "better Java", so it's going to grab people who went to Scala for a "better Java". Also Java actually has a sane roadmap and methodology to get better too, so there's that now too - with the preview/incubating JEPs, people can see what is coming down the pipeline.
Clojure makes no claims of being "Java++". It's a lisp first and foremost that focuses on embracing the host platform and being broadly compatible with existing libraries and strong concurrency protections.
My theory as an outsider: F# is strongly tied to the Windows world, the corporate world, where a conservative approach is always preferable, on your tech stack and if you need to hire peons coding all day. The corporate world isn't leaving OOP anytime soon, because it's what 95% of engineers focus on, the silent majority which do not frequent HN or play with functional languages in their weekends. The corporate world runs on Java and C#.
If F# had been released in the open-source, flashy and bustling world of Linux and macOS developers, it would have had a much greater success.
I know you can run F# on Linux, but just like running Swift, it feels like outsiders I wouldn't want to bet my business on if I were a Linux-only shop (which I am), however nice it feels. Also a decade ago when it had a chance to take root, Microsoft was still the Embrace Extend Extinguish company. It's not good enough to risk it, just like I'm not gonna use SQL Server for anything.
I think you are grossly overestimating the degree to which the programming language you choose to use to solve a business problem constitutes "betting your business on." How would your business fundamentally change if your first 10k lines of code was in F# as opposed to Go, or Java, or Python, or TypeScript? These are also all languages I've been paid to use, and have used in anger, and with the exception of Java were all learned on the job. This comment in general has big "M$ bad" vibes and if you take those pieces out I'm not sure what the actual criticism is (maybe there is none)?
F# looks wonderful, but unless you’re already in the MS ecosystem, dotnet just feels bad and out of place. And I guess if you are already in the MS ecosystem you’re using C#.
I think the situation is clear-cut: until recently, you couldn't really run .net on anything else than Windows, so the only people using it were those already invested in the ecosystem.
Among the people invested in the windows ecosystem, many (most ?) are large "non-tech" companies who hire people who mostly see their jobs as a meal ticket. These people don't have the inclination (for lack of curiosity, or time, or whatever reason, doesn't matter) to look into "interesting" things. They mostly handle whatever tickets they have and call it a day. Fiddling with some language that has a different paradigm wouldn't be seen as a good use of their time on the clock by corporate, or during their time off work by themselves, since they'd rather spend that time some other way.
Hence, F# never really got any traction.
It's pretty simple, really. I am a Linux engineer, and it is not a great investment of time and money for me to get into .NET. I knew F# was cool, but is it cool enough to want to feel a second class citizen, running it on the OS and platform it is not intended to run on? It makes no business sense at all.
I'm not a software engineer myself, nor a Windows person, so I don't know the specifics, but FWIW, my client runs production .Net code of the C# variety on Linux, connected to pgsql. It's some kind of web service for checking people's tickets (think airport gates where you scan your ticket to enter), so not exactly a toy, even though it's nowhere near unicorn-scale. It seems to work fine, in the sense that I've never heard anyone complain about anything related to this setup. No "works for me but is borken in prod" or "thingy is wonky on Linux but OK on Windows so have to build expensive workaround".
The devs run VisualStudio (non Code) on their Windows laptops. Code is then built and packaged by Azure Pipelines in a container and sent to AWS to be run on ECS.
But it never was a tier 1 platform during its growth. So most non-Windows devs put their focus on other platforms. There is nothing wrong with that.
I could learn .NET now, but I don't really have an interest to do so at this point; Also, the devs you talk about are on Windows, using their tier 1 IDE (Visual Studio) that only runs on Windows, which is my point exactly.
I tried to dip my toes into F# out of curiosity, and it worked by following some tutorial and VS Code. But it did seem somewhat bare bones. Although I'll admit I'm spoiled by Rust and IntelliJ.
As with almost all "vibes"-related comments, this doesn't hold up. There isn't any criticism; just a positing that the sort of corporate, process-heavy companies that will major on Microsoft programming languages will be the last ones to want to try functional programming languages.
As long as it has a base capability (libraries, maturity) and when people join they can be productive with it in a month or so then the risk is pretty low. For F# most .NET developers, even Node developers IMO will get used to F# relatively quickly. From my anecdotal experience with a number of languages its probably one of the easiest to onboard out of the FP langs balancing the FP methodology while trying to be practical/pragmatic. It has a large ecosystem via the .NET platform and supplements it with FP specific F# libraries where pragmatic to do so.
Your "career calculus" article has been top of mind for me recently as I've talked about it a bunch of people. Amusing how those core concepts don't change much.
I think with all the changes and extensions over time to add ever more features to C# so you can write whatever paradigm you want in it is a bad idea.
I would much have preferred to keep C# OO and use F# when you want to go functional.
Or just create G# which is the everything mashed together language that C# is close to being.
It seems that FP advocates overlook just how great LINQ is, even if it exists within the impure swamp of regular C#.
https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref... https://fsharp.github.io/fsharp-core-docs/reference/fsharp-l...
LINQ and async/await alone can be heady "gateway drugs" in C# to learning functional programming fundamentals (including the deep, advanced fundamentals like Monads), and C# only seems to continue to pick up little bits of FP here and there over time.
There are definitely lots of reasons why even some of the biggest FP fans often think "C# is good enough" without needing to reach for F# in 2023. (Which is why the post here, and the comments above in this same thread lament that F#'s biggest problem is the shadow that C# casts.)
[1] Mark Seemann did an entire series of blog post on coding common Monads in both F# and C# and the C# code is very interesting at times: https://blog.ploeh.dk/archive/
What's stopping you? You can mix and match C# and F# on the .Net platform, no?
I often think about C.
I learned C a long time ago, I still have the KR book, and if I look at C code I can get a good idea of what is happening, even though I haven't kept up with all the changes.
In C# now, two developers can write code that accomplishes the same but might not be able to read each other's code. I dont think that is healthy.
It is somewhat the Microsoft Office / Word approach to programming languages. Just Keep adding features on top of features.
It's... basically the same? Include library, create server object, tell server object what request to handle and what to return, start server object.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/m...
Much of the older overdesigned pain of C# is that it used to be tied to how IIS wanted things to work, NetCore initially pivoted more to DI stuff before pivoting again to these minimal API's but the DI stuff is definitely available(and used) still.
For pragmatic choices it's all there, you can start off with minimal API's and get far with it and once you start feeling pain-points as you want to re-implement things it might be time to add in the more "frameworky" parts.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();Your code snippet certainly has a lot of unnecessary ceremony. Why use a builder object at all? Why use a static class with a function to build the builder object?
var builder = createBuilder(args);
// etc...
Would be better. But var app = createWebApp(args);
// etc...
Is even better. No ceremony at all!Basically having the pivot efforts being sabotaged by the same company.
I don't think the pivot entirely failed, there's definitely a small niche for "data science, but it needs to run in .NET" and F# still to my understanding fills it well. It's a very small niche and I don't expect to hear a lot of data scientists directly training for it, but there's a lot of advantages in places that use the Azure stack, for instance, for faster/better/more integrated data science when done with F#.
F# would probably need a lot more investment in dynamic types to truly attract a lot of data scientist attention. (Though the .NET DLR still exists and could use some fresh, modern love.)
Relatedly, I appreciate a lot that Microsoft's polyglot approach helped standardize the ONNX runtime, and even if the data scientists I'm working with prefer Python or R, I can still take ONNX models they build and run them in a C# or F# library with very little sweat.
That's right. I mostly switched to writing "dumb records + service classes" code in C#, and while F# is terser, there's just not enough pain to cause me to switch. When DU's come to C#, the gap will get even narrower.
Maybe it doesn't need to thrive for everyone; maybe it just needs to continue being useful for the people who employ it and add value. That's probably OK. They could be just busy building stuff instead of blogging, especially if the community is mostly compromised of senior developers (10 years +).
C# will always be more popular because it easier to learn. Why? Because it looks familiar to most developers. Why would you learn this unfamiliar thing called F# if C# is right there and you basically already know it? On top of that, C# almost has feature parity with F#.
However, F# is a simpler language than C#. That is a fact. It has less concepts that you need to learn. I've found that onboarding someone in an F# codebase takes a lot less time compared to onboarding someone in a typescript,C#,... codebase. A lot less time. I've found that new people can start contributing after a single introduction. The things they build often just work.
I think that an F# code base costs a lot less money to maintain over longer periods of time. Can't prove it but I think that the difference is huge.
I've interviewed a lot of functional candidates in a decade-long stint of functional programming professionally. My interview approach is always the same. All practical exercises, no leetcode here. You can do the exercises in the language you're most comfortable in.
If I had to pick a language that predicted you'll do poorly on a practical interview exercise, I would pick F# every time. For some reason the candidates just do not do well. Now I can think of some confounders - maybe the types of people who would apply to a job with some dynamic programming requirements and people who are good at F# just have no overlap. I've thought about that.
But it seems like this line
Pragmatists don't make technology decisions on the basis of what is better. They prefer the safety of the herd.
Implies that if someone just saw some code in F# and realized what people can do in it, they would be super impressed. I have not found that to be the case. If that's a general problem and not just a quirk of my personal experience, that has to be fixed first.However technical capability and efficiency of the tool isn't the only concern picking up the comments in this thread so far. The herd effect sadly can make people nervous creating a barrier for people to try it especially if career is in it. What other people think of it, and their preconceived notion can matter.
It's unfortunate that they decided to pick F# on such problems when they didn't have the (mental) toolkit to tackle them, but I think it speaks more to people being really eager to communicate their enthusiasm for FP. I wouldn't try to ascribe any higher meaning to it.
Depends on what you mean by messy? Non-conforming JSON? A custom FParsec parser might be able to sensibly extract the data. If it is conforming to JSON then you'd use normal F# code to work with the standard JSON parsers.
But tbh, from what little I know, I'd be expecting you to expect me to solve the issue from first principles. So using a "and then magic" technique might be something interviewees shy from.
Normally now I ask Chat GPT to create an F# record from the JSON. Then go through and check everything line by line, redo the types to something sane and use that.
But if it were a quick script to do a one off then a type provider will work well.
Is F# too approachable due to its closeness to C#, popular with C# professionals, and therefore not approached as a functional programming language as much as a C# extension?
> I've interviewed a lot of functional candidates in a decade-long stint of functional programming professionally. My interview approach is always the same. All practical exercises, no leetcode here.
Does this mean that you're measuring, like, how fast someone can deploy a CRUD webapp in the given language? I can imagine F# would do poorly on that kind of metric; it's optimized for maintainability and doesn't take unprincipled shortcuts, whereas something like Ruby lets you type one line and it will blat out a bunch of defaults via unmaintainable magic.
> Implies that if someone just saw some code in F# and realized what people can do in it, they would be super impressed. I have not found that to be the case. If that's a general problem and not just a quirk of my personal experience, that has to be fixed first.
I don't think it's a general problem; almost everyone who spends any significant time working in F# returns to C# reluctantly if at all. It's already a "better" language in the sense of how nice it is to program in and how productive a programmer feels when doing so. But those aren't the metrics that matter.
They may not be built into Ruby-the-language but they're very much part of Ruby-the-development-community.
> I’ve seen and worked on some very maintainable rails apps
I've seen far more upgrade breakages in apps built on rails than any other stack.
React Native would like to argue with you on that point
Not trying to nitpick, your comment just piqued my curiosity because you made the point of distinguishing your exercises from leetcode and also stated that those who chose F# were generally poor performers.
Your observation about F# may be valid, but this does appear to be a test of one specific use case for a language, not how productive people are when building entire applications with them.
Scala developers were referred to a Java refugees, Swift developers to Objective C refugees, and F# as C# refugees. A weird side effect of Microsoft doing a better job with C# is that there less of a push to F#. Plus F# by virtue of being in Dev Div had the core value proposition (Ocaml on .Net) undermined by the Win vs Dev Div internal battles that tried and failed to kill .Net.
My guess is that interviewees wishing to return to the typed world they are comfortable with would first try to type the JSON they are working with. Given that the JSON is messy this could be an unbounded amount of work that is unlikely to pay-off within the span of an interview.
In the context of JSON you can work on it without types from a typed language. It’s just that as a force of habit coders may chose to spend time adding types to things when they shouldn’t.
It has absolutely nothing to do with dynamically typed programming. It's also a really terrible name for what is essentially caching.
There is much naïveté among the strongly typed herd.
Is exactly the reverse also true? Let me try: "There is much naïveté among the weakly typed herd." For every person who thinks Python or Ruby can be used for everything, there is another person who thinks the same for C++ or Rust.Also, the example that you gave is incredibly specific:
When tasked to create glue code translating between two opposing type systems, which is a very common data engineering task
Can you provide a concrete example? And what is "data engineering"? I never heard that term before this post.I'm not sure why using a static language would make translating data types difficult, but I add as many typehints as possible to my Python so I rarely do anything with dynamic types. I guess they're saying for small tasks where you're working with lots of types, when using a static language most of your code will be type definitions, so a dynamic language will let you focus on writing the transformation code.
Lots of people assume static typing means creating domain types for the semantics of every single thing, and then complain that those types contain far more information than they need. Well, stop doing that. Create types that actually contain the information you need. Or use the existing ones. If you're deserializing JSON data, it turns out that the deserialization library already has a type for arbitrary JSON. Just use it, if all you're doing is translating that data to another format. Saying "this data is JSON I didn't bother to understand the internal content of" is a perfectly fine level to work at.
Stop inventing work for yourself.
But yes, you can have a JsonNode type, which is still better than having every type being “object”.
Another technique: While the JVM is running with a debugger attached, it is possible to inject a new version of a class. IDEs usually make this seamless. It also works when remote debugging. Do you consider this monkeypatching also?
There's no theoretical reason you can't. No languages that I know of provide that combination features, because monkey-patching is a terrible idea for software engineering... But there's no theoretical reason you couldn't make it happen.
I think you've conflated static typing with a static language. They're not the same thing and can be analyzed separately.
But monkey-patching is also often used to alter behaviors of existing things, and that could be done without needing new types.
In another dimension, you could use that feature in combination with something rather like Ruby's metaclasses to change definitions globally in a statically-typed language.
I can't think of a language that works this way currently out there, but there's nothing impossible about the design. It's just that no one wants it.
Given that, a sketch of a statically-typed system would be something like... At the time a definition is added to the environment, you type check against known definitions. Future code can change implementations, as long as types remain compatible. (Probably invariantly, unless you want to include covariant/invariant annotations in your type system...)
This doesn't change that much about a correct program in a dynamic language, except that it may provide some additional ordering requirements in code execution - all the various method definitions must be loaded before code using them is loaded. That's a bit more strict than the current requirement they the methods must be loaded before code using them is run. But the difference would be pretty tractable to code around.
And in exchange, you'd get immediate feedback on typos. Or even more complex cases, like failing to generate some method you had expected to create dynamically.
Ok, I can actually see some appeal here, though it's got nothing to do with monkey-patching.
[{
name:”ex obj”
value:”a sample value”
extraProperty:”I’m important”
}]
And the code class myDTO{
string name;
string value;
}
var myObjs= DerserializeFromFile<myDTO>(filepath)
SerializeToFile(myObjs, filePath2)
filepath2 would end up with without the extraProperty field.You can also write code like
function PrintFullname(person) {
WriteLine(person.FirstName + “ “ + person.LastName)
}
And it will just work so long as the object has those properties. In a statically typed language, you’d have to have a version for each object type or be sure to have a thoughtful common interface between them, which is hard.All that bring said, I generally prefer type safe static languages because that type system has saved my bacon on numerous occasions (it’s great at telling me I just changed something I use elsewhere).
You can write code in a statically typed language that treats the data as strings. The domain modelling is optional, just choose the level of detail that you need:
1. String
2. JSON
3. MyDTO
If you do choose 3, then you can avoid serde errors using property based testing
I am surprised that you are using Python for data transformation. Isn't it too slow for huge data sets? (If you are using C/C++ libraries like Pandas/NumPy, then ignore this question.) When I have huge amounts of data, I always want to use something like C/C++/Rust/C#/Java do the heavy lifting because it is so much faster than Python.
I find Python is fast enough for small to medium datasets. I've normally worked with data that needs to be loaded each morning or sometimes hourly, so whether the transformation takes 1 minute or 10 minutes it doesn't matter. The better way is of course to dump the data into a data warehouse as soon as possible and then use SQL for everything, so I only use Python for things that SQL isn't suited for, like making HTTP requests.
In retrospect, Marz's hope that specter might get incorporated in clj core was wildly optimistic (even if the core team wasn't hostile to outsider contributions), because it feels like he built it to his own satisfaction, and never got around to removing the sharp edges that newcomers cut themselves on.
It's a shame, because I think specter is a cool idea, and would love to see a language based on its ideas.
Relevant blog post is Parse don't Verify
"Dynamic programming" traditionally has nothing to do with dynamic languages but is instead a class of algorithms that are "dynamic" in the sense that they represent time.[0] This might be what your parent was referring to because these algorithms lend themselves well to Haskell's lazy evaluation, and they reference F# as being eager.
That said, they also talk about F# as being type safe, so they could also be referring to dynamic programming languages. The grandparent was definitely referring to this one, but "dynamic programming techniques" sounds much more like the algorithmic meaning.
That's the type bias. If you look at a non-typed codebase, it always feels like it will be better with types. But if you had a chance to go back in time and start the same codebase in Typescript, it would actually come out way worse than what you have today.
Types can be great when used sparingly, but with Typescript everyone seems to fall into a trap of constantly creating and then solving "type puzzles" instead of building what matters. If you're doing Typescript, your chances of becoming a product engineer are slim.
One would expect that by now, out-of-process COM would be supported across all OS extension points, instead it is still pretty much in-process COM, with the related .NET restrictions.
Then there is the whole issue that since Longhorn, most OS APIs are based on COM (or WinRT), not always with .NET bindings, and even VB 6 had better ways to use COM than .NET on its current state (.NET Core lost COM tooling).
In case you aren't aware, WinRT basically marks the turning point started with Longhorn ideas being rewriten into COM.
With WinRT, they basically went back to the drawing board of Ext-VOS, a COM based runtime for all Microsoft languages, hence why .NET on WinRT/UWP isn't quite the same as classical .NET and is AOT compiled, with classes being mapped into WinRT types (which is basically COM, with IInspectable in addition to IUnknown and .NET metadata instead of COM type libraries).
Mostly driven by Steven Sinofsky and his point of view on managed code.
This didn't turned out as expected, but still the idea going forward is to make WinRT additions to classical COM usable in Win32 outside UWP application identity model.
"Turning to the past to power Windows’ future: An in-depth look at WinRT"
https://arstechnica.com/features/2012/10/windows-8-and-winrt...
Naturally since DevDiv also has something to say, .NET isn't going anywhere.
And since nowadays "Azure OS" matters more than Windows, WinDev point of view is mostly relevant for Windows Server and Hyper-V workloads.
Today's post-UWP C#/WinRT bindings don't require AOT either.
Beyond WinRT, .NET (Core) has supported all the raw COM, including COM component hosting since at least .NET Core 3.0. It's Windows-Only, of course, to use that, but that should go without saying. It's mostly backwards compatible with the old .NET Fx 1.0 ways of doing COM and a lot of the old code still "just works". .NET has proven that with .NET 5+ and all the many ways it (again) supports raw Win32 fun in the classic WinForms ways. (And all the ways that even .NET Fx 1.0 code has a compatibility path into .NET 5.)
It would have been nice if Windows had stronger embraced .NET, but WinRT is still closer to .NET in spirit than old raw COM anyway.
.NET Core doesn't do COM type libraries like the Framework does, you are supposed to manually write IDL files like in the old days.
Additionally CCW/RCW infrastructure is considered outdated and you should use the new, more boilerplate based COM APIs introduced for COM and CsWinRT support.
Lots of changes, with more work, for little value.
F# is a slice through the language design space that optimizes for developer productivity, other languages with different design choices are optimized and are indeed better for other things.
I think there is a benefit to learning learning ‘bad’ languages as they teach you about the different design trade offs that are available. A person with dynamic programming language experience would have known that the given JSON task was tractable without types and not started the task with a ‘type all the things’ mentality.
Maybe I am mistaken here, but it seems to me that the line you are quoting from the essay implies the opposite of what you said.
As someone that has just spent a while learning F# and really enjoys it, this makes me sad. I hope that I don't have some trait that drew me to F# that also causes me to be less competent.
I'm sure you could find a person who would claim this same exact thing for any language if you searched around a bit.
To derive a useful conclusion you would need to control for all of the factors involved in the interviews and objectively measure their performance compared to other people's, anything less than that is completely useless at best.
The most important thing is to always be learning. You’ll never be done, embrace that and rejoice in it. Most people more or less stop learning in early adulthood: don’t do that, and you’ll eventually be ahead of the pack.
Can be a great Java programmer, now go do something in assembly, doesn't reflect on the Java skills.
I.e. social proof
I feel like F# deceptively presents itself as simple, when in reality it is closer to C# in complexity. I've written in languages that are actually simple (tcl) and it is a joy. I've written in languages that are unashamedly complex (Wolfram language) and that is also fun. But F# occupies that weird middle ground where it seems easy to do what you want, but for some reason you trip over your own shoelaces every time you take a step.
Pure syntax wise, its pretty nice, though having a single pass compiler makes it a bit dated feeling compared to other languages.
though having a single pass compiler makes it a bit dated feeling compared to other languages
Can you please share how F# could improve with a multi-pass compiler?I'm not particularly attached to order of definitions within a module, but I definitely like the linear ordering of modules. Without the compiler enforcing that you always end up with cross dependencies between modules, which I think makes it much harder for humans to read the code.
I've had similar conversations about variable shadowing. I find it natural and it bothers me when I can't do it, especially in functional languages, but I have a friend who really doesn't like it and finds it jarring. I think his dislike stems from having learned Erlang before any language that has shadowing, because in Erlang when you reuse a variable name it'll be checking for equality with the value you're "assigning" it to.
In codebases that allow out of order definitions, things get wild real quick.
As I understand, Java allows this. You can effectively have circular dependencies. If true, I have worked on multiple million+ lines Java projects in my career; none of them were "wild real quick". Also, C++ has forward type declarations to effectively allow the same. Again, there are ginormous projects like Google Chrome and Firefox written in C++.Not an issue in C# at all. Get wild in what way?
I'm not sure a project gets into that state "real quick", but it remains something that projects can do over time, sometimes without realizing it, especially when a DI abstraction makes it even less obvious how circular a project's dependencies really are.
[1] https://en.wikipedia.org/wiki/Cyclomatic_complexity
[2] https://en.wikipedia.org/wiki/Software_Peter_principleF# always came across as very weird in the syntax area
In my experience, the worst performance is by C++ candidates.
Why? Not because C++ is bad, or attracts bad programmers.
Rather, because if that's the language you reach for a quick solve, you probably don't know very much, and that says something.
I would consider most popular languages, like C#, Java, and Python decidely unpragmatic as languages. There's way too many hoops to jump through to concisely describe the problem domain. In F#, I define some types that describe the domain, write some functions, and move on. It's that easy.
Like in an interview setting, even if they say you can choose any language, it is probably a safer bet to go with something the interviewer has probably seen before.
I'm not a great fan of f# (it's not rational - but every time I've tried to dip my toes, something in the syntax has felt... Tedious? In a way that for example StandardML does not). But it still seems like an eminently powerful and pragmatic language, so I'm surprised by your experience.
Ed: I see this was addressed downthread:
https://news.ycombinator.com/item?id=36133560
https://news.ycombinator.com/item?id=36133578
Fascinating that fsharpers trip up on ad- hoc json wrangling - to be fair i used to agonize over nested lists/association lists in lisp - rather than doing the "professional" thing and YOLO assume that three levels down, behind :user - there's a fifth element :email...
Some languages don't really intrude on one another's territory — e.g. not that many people are rewriting Ruby programs in Rust — but some very directly compete.
So if you want to convince someone to use F#, you have to convince them it's significantly better than some other closely-related language. And that's hard!
I have a strong suspicion that the next decade will see a reduction in programming language diversity. JavaScript/TypeScript and Python will become even more popular, to the detriment of everything but Go, C++, and Rust.
Platform-specific languages like Swift will persevere, as will Java, which is unkillable, but the vast array of languages will become less vast.
Why do you see the popularity of C# waning?
if the tooling would be on par
I am betting person, and I say: "It never will be." It is easy to overlook the importance of better developer tooling. It directly translates into higher developer productivity. Every time I am forced to use a language with worse developer tooling, I am reminded of this performance hit. This is why Microsoft spends so much time and efforts on its developer tools. Also: Better tooling means below average developers can level-up to average. This is most of corporate computing, so a win for the bean counters.Also: There is not single, commercial, controlling force behind Rust, like Java and DotNet.
Don't interpret this post as anything against Rust. I think it is a brilliant language.
I feel this daily as a Clojure programmer. I've been playing with Copilot, and it's astonishing how much worse Copilot is at generating Clojure code, than say, Js. The difference is probably due to training volume, and if AI-assisted coding is worth it at all, the benefits will primarily accrue to the largest languages.
Personally, I find the JetBrains IDEs overly complicated. More so than Visual Studio, but that may be familiarity. (Also, I use VSCode for .NET these days so full IDEs feel heavy to me anyway.)
See ONYX, DirectML and ML.NET announcements at BUILD.
Neither ONNX, nor DirectML, nor ML.NET excel at deep learning. TorchSharp is the closest to what is needed for the modern ML work, and there is almost no investment in it - I believe there's just one person working on it, and even that is part time.
The only thing that keeps me on C# is lack of Rust debuggers that would be able to evaluate arbitrary Rust expressions including full support for traits.
Python ... maybe? It's so different than the rest, I can see it going the way of Ruby once AI bindings improve in other languages. I can also see Julia dominating the AI space.
The "better than X" languages though? C#, F#, Scala? Done. I don't even hear about Kotlin that much anymore, Java is starting to adopt the good stuff from them.
There's also Odin[1] that looks promising.
I don't think C# is going anywhere, F# on the other hand :shrug: is at the mercy of MS - they always seem to be on the fence about it.
[0] - https://www.modular.com/mojo
[1] - http://odin-lang.org/
I would say that in the domain of game development C++ (which is where I'm guessing that most new C++ development is done) has such a moat that Rust in it's current form will not be able to displace it.
and even more so for beginners, most languages only show their differences at a much deeper level.
Just an anecdote (and definitely not "many") but one thing we've found is that ruby and rust actually fit fairly well together. The way I express something in Ruby (often FP stuff like `map` and `filter`) translates very well in Rust. Much better than it translates into (as an example) golang.
Beyond expression, the tooling in rust (cargo) feels very familiar and intuitive. Ruby -> Rust in that regard has felt much more natural than Ruby -> JVM/golang/C++.
Don't get me wrong, it's not a trivial jump, but I've been pleasantly surprised by much of the developer experience.
Even if your suspicion is right - since so many new programmers enter the field, absolute community sizes can still easily grow and hence we get more real-world viable languages.
Personally I think we've witnessed an acceleration in the coming and going of languages. Go, Rust, TypeScript, Clojure, Elixir, Zig, etc. And C++ is being dethroned in many more application areas than seemed likely only few years ago. And the GPU realm is still nearly entirely untouched territory. Plus the AI craze may yet stir the soup significantly.
Google branding, and yet another language from UNIX creators.
None of its direct influences, Oberon-2 and Limbo, achieved market success when ETHZ or Bell Labs were pushing them.
Docker and Kubernetes successes made Go unavoidable on DevOps space.
Functional programing is different. I don't think it's hard but it's not what most developers have spent years practicing. So there is a learning curve to get over.
Critical mass is a problem for all new languages and often it's not the best language that will win critical mass. It's the one that gets adopted for reasons that aren't always merit (see javascript).
I pick F# because it's easier for me to avoid silly mistakes. I find my code is easier to change and it's harder to write some classes of bugs. It's also very pleasant to use.
But C# is pretty good at all of these things, F# is just a little bit nicer. But that little bit comes at a big cost of being very different.
I think the problem with this line of thinking is that some people insist on using functional tactics EVERYWHERE, even when it doesn't make sense. I get that its ugly to mix functional and imperative code, and maybe some languages dont even allow that. but they should. thats why I like Go, because sometimes a simple for/while loop IS the correct answer.
But somethings take a bit of ramping up. Like the option type, it is great and works well with pattern matching. If you don't know why exhaustive pattern matching with union types and options instead of nulls is amazing then it just feels like coding with your hands tied.
It’s difficult to relate to this without specific examples (I don’t think I agree with the sentiment on balance, but I could also imagine all kinds of obviously-bad things), but FP really does transcend other programming paradigms. Languages such as Java and C++ code definitely do benefit from FP approaches, even if these languages are perhaps not the best at facilitating the approach.
An individual may have the same values as an early adopter, while having to make the decision to used tried and true systems for various reasons out of their control - one of which might be that you depend heavily on the continued development & support of surrounding ecosystems. Or that large code-bases can't simply switch to a new technology without it being a massive multi-year undertaking which is difficult to justify.
So while the article makes some reasonable points, I think it doesn't fully take into account that pragmatists often use what they use for better reasons than "following the herd".
I'm also "following the herd" when choosing a platform that will make it easier/cheaper to hire (there is an existing "herd" of developpers), or has wide support by multiple vendors etc .
While I like the F# language, I don't like the (understandably) abandonware of the small community. I've ported most of my F# stuff to C#, and it's not that much different in that you can program in a functional style in C#. Probably the nicest thing in F# that C# doesn't have is computation expressions.
for instance, as an example for this https://fsharpforfunandprofit.com/posts/designing-for-correc... where a C# solution is suggested and done with DUs in F#, I wrote the equivalent DU solution in C#... https://gist.github.com/keithn/ceeeed5f7eb567e1b2333747065d1...
Only thing you don't have is exhaustive checking, which I personally don't think is much of a problem pragmatically.
There's a secondary benefit though if they allow DUs of Structs where you can have it be on the stack as a single value based on the size of the largest subtype that could be incredibly useful when you are worried about performance. Inheritance does not do that.
But even more often, I find myself wishing C# blocks were expressions.
In F#, I could do this:
let x =
let z = whatever
...
value
and z is inside a local scope.So in C#, I want to do:
var x =
{
var z = whatever;
...
value
};
Maybe it's just me. var x = (() =>
{
var z = whatever;
...
return value;
})();
It is not the most performant way to write that code and if the contents start to get long, refactoring to its own method starts to feel more likely, but in one-off cases, it seems fine for how ugly it is.I think the abandonware thing is expected. But I think this is the cost of a language that is a child's to a main ecosystem and doesn't just apply to F#. Unless its specific to FP/the F# domain it will be written in C# or whatever the common language is for that platform. Ironically the language with the least features is the one to write in (LCD) so it targets the most people. Why would I just target one language on the platform when I can target all languages? Especially if I'm paying a team to write the library. Most bang for buck so to speak.
Hence most things in these side languages are either wrappers to make them more idiomatic and/or pattern libraries that are specific to the extra value add the language has and usually don't need to change much once implemented. I think that's fine and pragmatic, why reinvent something that works?
* two async models present: task (C#) & async (f#)
* horrible async performance: perhaps fixed by native task & FSM transformation
* two error handling models: Result (monadic but without do notation) & C# style Exceptions
* not rich in community libraries, like a desert & abandonware
You have access to the entire .net ecosystem?
So using .NET libraries directly is frowned upon, while some of them like GUI frameworks, expect C# features (like Roslyn and Code Generators) not available in F#.
Just to test how much I would actually benefit from the .NET ecosystem, I chose to use a C# wrapper for the Telegram API. Suddenly I had to handle null references and exceptions everywhere (it's in this regard even worse than Rust, where I'm guaranteed to get Options and Results).
I know proper support for C# T? type interop is in the works (https://github.com/dotnet/fsharp/pull/15181), but really it seemed to me that too few cared about F# to get it landed in a reasonable timeframe.
I'd really love to see a pragmatic FP language with a reasonable amount of libs to choose from (both natively and via interop without much effort), but F# was a bit far from that description IMO.
Agree that exceptions are frustrating that they exist, sadly there's little option due to C# exception model. Same thing in OCaml though less excuse over there.
I basically use C# libraries directly, and it hasn't been annoying at all.
The stigma of the MS touch and the fear of getting bitten in the future is a strong deterrent.
You can easily migrate from github or vscode, but not your programming language.
Mostly what I have seen happening is people really loving F# convert to using C#. The tooling being the main reason.
What this article isn’t saying is that F# is awful at interoping with C# and windows APIs in general. Mostly that F# functions cannot be used as Func<> objects. So even in a C# environment, bringing some F# is a pain. Definitely didn’t help the adoption.
> Because C# is really, really good.
The first language I learned was VB.net because I thought "Visual Basic" sounded easy. I quickly moved to C# because it was more popular. I've always been curious about F# but never invested the time to learn it exactly because C# never caused me enough pain.
Good advice, I think the useful idea is: An excellent product in competition with a very good incumbent is a bad idea. A mediocre product in competition with an absent or terrible incumbent is a great idea
F# had Jet as the one large success story of a full f# shop, but outside of that there weren't many large companies using a lot of F# (except microsoft internally, and they didn't blog about it _too_ much).
Kotlin on the other hand is seeing server-side adoption in quite a few large companies. Google is recommending it as the server-side JVM language for new services going forward. Airbnb and a handful of other large companies blog about having kotlin services in their architecture.
There's a few reasons I think Kotlin is succeeding where F# hasn't been able to:
* JVM + tooling is a proven and great ecosystem. First-class support for most of your cloud tooling and databases (AWS, Kafka, ElasticSearch, Cassandra, etc).
* It has fantastic editor tooling (given that an IDE company created and maintains it). F# for quite a while has had less-than-average tooling (it's gotten better now with newer versions of FSAC, Ionide, and Rider, but it was _rough_ for a while). It's hard to move to something new that's supposed to be safer when your language server has memory leaks or breaks every X hours. I remember stretches of time where I was just writing F# in vim without syntax highlighting because any editor I tried would constantly throw errors in the middle of me coding. That hasn't happened to me with Kotlin.
* It's close-enough looking to a scripting language with nice features that it's more approachable. Classes and functions look similar to other languages. It even has block parameters that are inviting for rubyists (although the implementation is different). It also has some syntactic sugar to make some things very ergonomic to work with.
* All of the above just works. No big fiddling necessary. The pain points in working with the Java ecosystem are written about and known (largest that come to my mind are coroutines interacting with java threading, exceptions in mixed coroutine/threading environments, and null/optional which Kotlin has some great features built-in to work around).
There's also things like arrow-kt and http4k for those desiring the more functional side of things.
The one thing that continues to baffle me is how little I see in people talking about kotlin in various communities (slack, discord, reddit, etc) compared to how many people are writing it (both for android and for server-side concerns).
The one upside I'll give is that fable has been much better IME than my experience with Kotlin/js. I also prefer F# from a purely language standpoint, but Kotlin is just better to work with.
This is in contrast to Java which for a significant time wasn't showing any evolution which sparked many alt langs on that platform. Sometimes things succeed but silently, and sometimes things that should succeed just don't and maybe there isn't a real explanation. In fact I think the latter across most products is actually the most common outcome, not just in programming languages.
TL;DR I agree with the article, it suffers from an adoption chasm. Pain and herd safety are important especially when people are considering their career progression.
To your dot points:
* I think F# has .NET which is a "proven" and good enough ecosystem as well. You won't get stuck by picking F# even if it means using a C# library. Have to do the same in Scala/Kotlin and the like typically as well.
* Editor has caught up, my surprise is that you can write with Vim and kinda get away with it - only find that works in terser/scripting like languages anyway unlike C#. It has wrappers and libraries for doing things in a more F# FP manner if you wish although when I was doing F# I avoided them (i.e. I avoid things like ScalaX most of the time too).
* F# has a good scripting story; I've seen F# scripts used quite often by people even if coding in C# to check things and REPL program.
* These days F# just works too mostly. `dotnet new webapi/console/etc -lang F#` and you have a webapi for example.
* There are F# libraries around for similar things (Http, Collections, Async seq, etc). They are probably maintained to the level they realistically need to be, I would argue they still do their singular job quite well. At some point a library just does what it needs to do.
There's things I prefer in the .NET ecosystem as well - value types, inlining, etc. Having said that I seen quite a few people use it professionally to good success but they often don't brag about it and are usually quite senior and reserved about their language choice often citing other worries/risks to a project's success.
On the JVM is as much as F# on the CLR, as any google trend analytics and similar will show.
Why F# evangelism isn't working - https://news.ycombinator.com/item?id=8841534 - Jan 2015 (5 comments)