Fighting Complexity in Software Development
github.com
github.com
I will argue that the complexity of software development is not because of OOP vs Functional. Tooling, documentation, quality of libraries and people are what matter most. Ruby was a massive success is largely attributed to above. We are humans, we can understand and deal with a fixed amount of complexity if I can offload some of it to a framework, library or tool I will have more time to work on my problem.
Every time I try to play with anything Functional I got hit by a bus of undocumented frameworks(Erlang), multiple standard libraries (ocaml), competing half finished implementations (lisp), arcane tooling (scala), no tooling (Haskell) and broken tooling (F# on Linux).
And to be fair, most of the premature optimization and bloated abstractions are just your architects sitting around bored for 6 months while the project sponsors argue over reporting minutiae (but won’t approve the designs until their pet features are pulled in on the roadmap). They know the entire feature list is going to get changed at the last minute as the political winds shift, so they’re designing an overbuilt architecture to CYA for whatever random bullshit someone will pull a week before a deadline.
Cost only goes up and up when engineers go overboard with "clever" Ruby magic... which is human error, don't blame the tool.
I've only used a few languages in production for my non-developer job, but I've noticed very different frequencies of "clever" code between them.
R: This is my primary language. I love it, but it almost encourages clever coding. There are 10 ways to do any task without even touching third-party packages.
Python: My secondary language at work. Clever coding is definitely possible in Python, but the language's syntax makes it easy to spot. Detection isn't as good as prevention but it's better than nothing.
(I also grudgingly use SAS, but won't waste my time looking for ways it encourages clear coding.)
I'll grant that it is easier in more stable API environments. But we are our own worst enemies in that race.
In Java? Not really. Spring has been around since 2003 or so.
I honestly can't compare to a rails app, as I don't have the experience. I can say that modern dev practices worry me with the constant upgrade train on api standards. On the one side, progress is quite nice. On the flipside, what you describe in "all your dependencies have completely changed their APIs" is just as annoying in Java as it is in any language. More so if someone has helpfully split your codebase into as many supposedly independent parts as they can.
More than that, it is amusing that many of the refactoring tools that you reference actually began in more dynamic languages. I've seen that folks that rely on their IDE to be able to refactor have a habit of making codebases that they have to use an IDE to refactor. I grant it could just be confirmation bias.
But yes, complexity is also a major problem.
I'd argue that this is the weakness with (the traditional) OO languages. An experienced developer with plenty of time can write good software with almost any tool. But that's not what's interesting. I want to see tools that not just lets but rather guides inexperienced developers into making maintainable software.
Things like "no nulls" or "immutable by default" in Rust and most functional languages are two examples of such designs. OO itself doesn't necessarily mean the developers get trapped in poor code, but the traditional 3 (C++, Java, C#) sure do give developers lots of guns to shoot at their feet. Perhaps not mainly because they are OO, but because they inherited some poor fundamental decisions about mutability and nulls (from C) and about inheritance as a default method of abstraction (from C++) etc.
In many languages they make it harder to write bad code but they do so at the expense of delivering functionality quickly. There's a clear trade off there - one which is often very project dependent (some projects primarily need to develop functionality quickly; others prioritize stability).
The problem of slowing down development becomes particularly pronounced when developing something where the biggest risk isn't building the thing wrong, but building the wrong thing. It's extremely expensive to use a very strict language if you're developing code where requirements are highly in flux and cannot be discerned up front.
Ideally general purpose languages should enable a smooth transition from coding up a prototype/MVP all the way to production-hardened, spotlessly clean code.
I like rust a lot but I would only use it to write very low level code where the requirements are cast in stone and speed and stability is of the utmost importance. It's a lovely language but it's very slow to build stuff in, and in almost all cases it should be used to rewrite something existing rather than to build it anew.
Using a "stiff" set of tools might slow down the first stages of prototyping, but it also makes the prototype easier to modify as requirements change. The question I suppose is simply where the equilibrium occurs. I.e. does Rust or F# make a thing that is easier to modify (because of less coupling) already after one or two months, or only after one or two years?
This isn't invariable at all. More often what you throw together gets thrown away. I'd estimate this happens to more (working) code I've written over my career than not. The hardest thing to get right is often getting the contours of a tool right and ascertaining what it should do - not making it work right after it has proven itself.
>Using a "stiff" set of tools might slow down the first stages of prototyping, but it also makes the prototype easier to modify as requirements change.
This is only really the case where the prototype was fundamentally solving the right problem in the right way to begin with and the subsequent changes are incremental in nature. If the design of, say, a microcomponent is flawed from the outset or it did the wrong thing and you have to re-do it, those "stiff" tools slow you down.
Using an extremely strict set of tools in a prototyping environment also invariably means a lot of extra up front work dealing with the tools' attempts to protect you from bugs which have an extremely low probability of occurring and/or an extremely low cost when they do occur.
If F# or rust or haskell really was quicker and more effective in a prototyping environment as well as when writing production hardened code, programmers would likely eventually converge on only using them. That isn't what is happening.
This does not seem to me to be a safe assumption to make. The market is irrational. There is no reason to believe popularity has any significant correlation to the effectiveness of a tool.
To some degree. But programmers are not, in general, easily herded, even by other programmers.
> There is no reason to believe popularity has any significant correlation to the effectiveness of a tool.
Programmers are not stupid. They are not totally ineffective at pushing management to use sane tools. One or both of those would have to be true for your statement to be true.
Well, you may say, it's a slow process. But Haskell has been out there for more than 30 years. F# is 14 years old, and supported by Microsoft. These languages have had plenty of time for programmers to notice that they were actually more effective in the real world.
This. So, so, so, so much this.
I absolutely agree that getting the contours of the tool right (nice analogy there) is (one of) the hardest parts. The main issue I find when using strict, type-driven FP langs for "first draft" style implementations is that I waste time with the unavoidable ceremony that most of these languages require, when what I really want to be doing is probing around to discover the rough edges. The "my last name is Curry" style of FP almost requires you to declare these edges up front as you code.
I actually find that TDD is even more useful in FP contexts than in procedural - mainly because a) it's a good way to help think about the shape of the tool before building it, and b) the FP implementations are often more mentally complex with recursion, pattern matching, and other such things that (IMO) require more brainpower to grok than simple procedures, and to be honest I just find myself needing the tests so I don't go mad trying to be a human compiler. I think even the most staunch TDD fanatics wouldn't try to argue that it's a fast way to prototype.
These days I find myself reaching for dynamic languages with gradual typing for prototypes that might hit production (JS+TS, Python, even PHP). When I'm satisfied with the general shape, I'll add a few type hints here and there to make my IDE friendlier. After a while, usually at the point where there are large additions to requirements, I'll find myself rewriting at least a chunk of the original in a completely different manner, usually in a more type-driven manner, usually with a more FP slant, and often in a different language with more strictness.
I would like to see more mainstream languages support both type-driven FP and simple procedural code without using Haskell-inspired syntaxes or turning into the incomprehensible mess that is Scala. Java is slowly morphing that way, but it still lacks some of the FP fundamentals for when you do want to go full-zealot.
But they get the ballance completely wrong. In Python, I can just write code that returns some object or None (Python equivalent of null). In Java/C/C++ I can't - I have to appease the type system (i.e. declare the return type), but there is also no way to even later tell the compiler that it can't be null!
What complicates assessment is that for many attributes, it’s impossible to assess the tool and the user in isolation. This is not unique to programming languages.
As well as one of the most complete specifications in ANSI Common Lisp.
In the end multi-paradim languages will win.
What many FP advocates fail to acknowledge is that all FP languages that got some kind of mainstream adoption, might be FP first, but they are actually multi-paradigm.
I confess I don't "get" the benefits of FP for the type of applications I work on. Most examples are for a domain completely different, make unrealistic assumptions about domain patterns of change that I actually see, or fill in for weaknesses of a given language's OOP model.
All of them also expose OOP concepts.
Even the initial c# version was over complicated. The complex fluent interface with lambdas and callbacks could be done with a few if statements that would be simpler, faster and require no knowledge of the FluentValidation library. Unnecessary getters and setters to satisfy the encapsulation gods.
If you want to fight complexity got back to basics, you can have a static method returning a validation result with code like this:
if (!string.IsNullOrEmpty(card.CardNumber) && CardNumberRegex.IsMatch(card.CardNumber))
validations.add("Oh my");
Converting if statements to more elaborate constructs is creating complexity not fighting it.You then want each field to be validated individually. So you get an error for each field which is wrong.
So you have if statements for each field creating a localised validation error object then placing in a list.
You have 8 fields coming in on your request. It's starting to look like a big method now with 8 if statements creating these localised validation objects.
You also want to share your validation rules between different use cases.
FluentValidation makes that quite quick and terse to achieve compared to simple if statements.
So the above example would become something like this:
if (!string.IsNullOrEmpty(card.CardNumber) && CardNumberRegex.IsMatch(card.CardNumber))
validationContext.add("CardNumber", Localizer.MessageFor("InvalidCCNumber"));
if (x.ExpirationMonth < 1 || x.ExpirationMonth > 12)
validationContext.add("ExpirationMonth", Localizer.MessageFor("InvalidCCExpiration"));
Throw in some lambda's for the property name and static strings for the message names if you really need to be type safe. Also I'm not sure if FluentValidator handles this, but you need somewhere for root level errors, not all errors map neatly to a property.> You have 8 fields coming in on your request. It's starting to look like a big method now with 8 if statements creating these localised validation objects.
There's no local state, the errors are stored in a glorified dictionary, it's a simple imperative series of if statements that anyone who's gone beyond hello world in any language can understand. Big methods are not bad just because they're big (not that 8 if statements is big), they're bad when there is a lot of mutable state that the programmer has to track in their head, validation logic rarely has this problem. It would be fine if there were 1000 properties because the complexity is flat.
> You also want to share your validation rules between different use cases.
So you make a function. It doesn't look like FluentValidator offers any improvement here, it seems like custom rules with this library basically just wrap a function call: https://fluentvalidation.net/start#including-rules or you create a "function" at runtime with rulesets: https://fluentvalidation.net/start#including-rules
> FluentValidation makes that quite quick and terse to achieve compared to simple if statements.
From the examples I'm not sure it's any quicker or more terse after you include the extra boilerplate setup. All it seems to do is turn if statements into where/must calls, for loops into RuleForEach calls and functions into custom RuleSets. It also adds the complexity of using a library.
Next problem, rename a field on the object using a refactoring tool. You now have to change validation code to change the field name. You may forget about the validation code if your not looking at it. You have good tests though so you would probably would catch it. But you want it to be automatic. Maybe nameof?
The class is getting a lot responsibility, and you want to seperate validation out into its own class responsible for that. Maybe extract to a validation object which operates on a request/command class?
Might point is you eventually you end up building something like fluent validation. With own set of default rules, validation classes etc. Maybe fluent validation is overly complicated but I'd rather get the speed boost of using a well tested library that I already know instead of gradually refactoring into something custom.
I'm building a simple composite data structure, outside of NPM this is not remotely a library. With a bit of luck not even that, I'm a bit rusty but I think the MVC framework had one built in to handle this.
> Next problem
I already addressed it, a lambda function instead of a string that can extract the property name, but your right that nameof might be a better option these days.
> The class is getting a lot responsibility, and you want to seperate validation out into its own class responsible for that. Maybe extract to a validation object which operates on a request/command class?
I already have, the actual validation is in a static method somewhere, so it has only one responsibility and the validation context is mostly a simple data structure, that's not too much responsibility.
> Might point is you eventually you end up building something like fluent validation. With own set of default rules, validation classes etc. Maybe fluent validation is overly complicated but I'd rather get the speed boost of using a well tested library that I already know instead of gradually refactoring into something custom.
It's not an unbounded problem with lot's of gotchas down the line, it's a well known and simple to solve problem that practically everyone has seen before. What you're ignoring is the complexity of adding a dependency in general and the complexity of this library in particular. Adding a dependency is not a free lunch, it has a cost in time, mental overhead and maintenance. This particular dependency increases the complexity of your code and delivers practically nothing. As for the speed boost, assuming it's true does not mean it reduces complexity, faster (initial) dev time very often comes at the cost of creating more complexity.
Then there is unnecessary complexity from doing incomplete refactorings and rewrites. If some code cannot handle the addition of a new requirement, it should be replaced. Otherwise you add complexity that roughly takes the logical (if not actual) form if (these cases) { new code } else { old code }. And there is overlap! new code has taken over requirements for which old code still exists, but because of some lingering requirements that only the old code handled, all of it is still there (due to laziness, dependencies or whatever). It's not obvious that some of that code is never used; someone diving into it faces the complexity of figuring out what is the real payload in production now and what is the historic decoy.
The main source of complexity is how we write software, not that the software has requirements.
It's a systemic problem that results when the entire leadership stack isn't aware of how good software is created. Because of the limited amount of time and resources given, quite often it's a business/management problem.
And that's not to say that it isn't also a software dev problem. We've all seen some horrific things. But I've also seen horrific things because there was no one senior there because they wouldn't pay enough for it.
it's all intertwined, there's not a simple explanation. But changing requirements is definitely a source of complexity.
Fact is they could build better software in the old language as well, assuming they started from scratch.
It's a repeating pattern in this society, fools with advanced gear doing what fools do best.
Consider using a hammer against a pneumatic tool. A newbie will hose you with better tools, they're wielding that condensed knowledge at their hands, their sum overshadow yours with only a hammer.
Most of the difference seem to be in motivation.
When I studied at university many years ago, my course had a reputation for being tough. Before the course started properly, there was a three week intensive Java course with an exam at the end. They suggested that if you didn't pass the exam, then it probably wasn't the subject for you. A couple of people failed that exam and continued with the rest of the year long course anyway. Those people did struggle and I don't think any of them passed.
And these abstractions will be often be overlooked or misused by developers who have not used them in languages where they're native; making them a net negative instead of an obvious benefit.
These things just make bugs disappear.
When it comes to IDE experience, the parts that I use often are mostly the same between Rust and our language.
Edit: I'd say it's both, in a multiplicative manner. You need experience and a good set of tools (the language itself being the most important tool) to write good code fast.
Is this homegrown? In my experience this alone has a major impact in productivity because it is generally hard to create a good implementation and a new language and/or implementation only pays when the existing solution is too bad. (Source: I have made a Lua type checker at work. It worked, but fell into disuse as I moved on and the entire org abandoned Lua in spite of my work.)
This seems like an example of a language effectively abstracting common complexities and pain points, which were probably discovered in earlier languages...
World operates in spirals. We branch off trying things and then go back to old forgotten ideas, etc etc
Most often, people start down this path bright-eyed and bushy-tailed, and end up realizing after about 4 months that actually all that complication was doing something pretty useful. People need to be careful before they dismiss real working-in-the-wild code.
Replacing a system is also not really solving a problem that the business cares about. So this enevitably leads to feature creep, "If you're rebuilding it, can you add X, Y, Z...". This then leads raises the bar even higher...
The better alternative is to modularize the system somehow and replace seperate chunks... but that's easier said than done
These are just some of the big corps who use Clarion. https://en.wikipedia.org/wiki/LexisNexis https://en.wikipedia.org/wiki/DBT_Online_Inc. https://en.wikipedia.org/wiki/Experian
Various banks and other stock market listed companies. Even various military use it for their own top secret work.
The key to its success is the template language, which enables the programmers to work at a higher level of abstraction which for some reason just doesnt seem popular amongst many programmers. You can use the templates to write code in other languages, including Java, PHP, ASP.net, javescript and more.
Its safe to say, that everyone in the Western world will have some of their details stored in a Clarion built database, and its not just limited to building databases, its even been used to build highly scalable webservers. Theres also C/C++, Assembler and Modula-2 built into the compiler, so you can get right down to low level coding if required, and there's a Clarion.net version which is mainly like C# but has some of the data handling benefits of F#.
The simpler solution is often hard to see. We get attached to the wrong details or suffer sunk cost fallacies.
When you switch languages the cost of porting is higher, so it shouldn’t be a surprise that you end up with something much simpler. And if the target language attracted you because it makes some part of the problem simpler, that’s important but maybe not the dominant contributing factor to the experience.
1. Our way of implementing DDD helps us organize code into infrastructure and domain. Domain objects typically aren’t allowed to have external dependencies. Infrastructure code is primarily for data access and mapping. Our API code (controllers and event handlers) ties the two together.
2. Given the above we are able to write a) very clear and concise unit tests around domain objects and API endpoints and b) integration tests that don’t have to bother with anything but data and other external dependencies.
The result is that when we go to ask, “How does the system respond given X?” we can either point to a test we have already written or else add a new test or test case to cover that scenario.
We can even snapshot live data in some critical areas that we can then drive through our high level domain processes (we process payroll so it’s a lot of data to consider). If someone wants to know how a particular pay run would play out, they can just craft the data snapshot and write some assertions about the results.
We also use FluentValidation (on API objects only) and test those as well (but only if the rules are non-trivial).
It’s a breath if fresh air from the sadly too common (IMO) flavor of the month new tech promotion.
Functional programming is basically my goto when there is complicated business logic involved now.
It's fine if you want to choose C#, and there're better ways of addressing the approaches to validation in OOP than were provided in the examples. Value objects are a nice way to ensure strong immutable types like credit cards can be created and passed around without requiring separate validation classes or wild abstract base classes.
I like exceptions in C# - when I used to code that I'd make a lot of domain/business exceptions that the code would throw anytime there was a violation. Here I think Java is a lot stronger in that you are forced to declare what types of errors can be thrown from a function so you have a chance of handling them. In C#, Typescript, I'm finding myself having to lean on codedoc "@throws" to do the same thing (though not as reliably).
That said, I generally am fine for most exceptions to not be handled and instead bubble up "globally". If it happened because of an API request? Let middleware map it back to a 400 Bad Request with the error body. If it happened because of a message handled? Log it, retry the message until it gets dumped to the DLQ. If it's not a violation, then it may not be an exception in the first place, in which case it can be returned with a compensating action performed.
I really like F#, but I struggled to find the actual benefit of it in this article from a DDD perspective.
In c# you need to create a lot of value object classes for that. Or have some kind of property inside the value object which indicates current state of the email.
Even then you won't be able to exhaustive pattern matching on it to guarantee each situation is handled.
Once you realise this it's fine, you just don't use those features when it doesn't make sense.
I find that imperative OO style naturally leads to complexity. Passing references to mutable data all over the place creates tight coupling across your entire application. This makes it impossible to guarantee that any change you make is local without considering every other place that references the data. Meanwhile, objects are opaque state machines and programs are structured by creating many interdependent objects.
These aspects make it pretty much impossible to tell what any particular piece of code is doing just by reading it in large applications. The only option is to fire up the debugger, get the app in a particular state and look at the data. However, there are typically many ways to get into any particular state, and it's really hard to know that you've accounted for them all. So, a debugger is a heuristic at best.
FP and immutability tackle both of these problems head on. Immutable data directly leads to the ability to do local reasoning about your code, and allows you to write pure functions that can be reasoned about independently. Meanwhile, data is not being abstracted inside opaque state machines that provide ad hoc DSLs as their API. Instead, it's explicitly passed through function pipelines to transform it.
So beginners could solve simple bugs and add simple features to FP applications.
Yet it's not a common thing.
Why ?
For example, the menus and navigation can almost all be tracked and managed in the RDBMS. It's easier to query and study the structure that way because I can sort, search, group, and filter it by any way --I-- please for any given need; I don't want to be stuck with YOUR single grouping; I want to be the Grouping God when studying the app. File-centric code can't do that (at least not without an IDE that reinvents a database). Therefore, don't do it. Use code where code is best, and RDBMS where RDBMS is best.
Code sucks at the big-picture and FP won't change that.
I tend to feel that unexpected, unrecoverable exceptions are best treated as just that -- exceptions. Applying a C-style function return value check seems backwards.
And the "Interpreter"?? Proper purpose of Interpreter or DSL is for dynamic (configurable or user-input) code, not to implement basic sequential flow and the 'if' statement which the underlying language already provides.
File-centric code forces a hierarchical big picture structure, but many relationships are either not hierarchical, or need additional non-hierarchical ways to view/group them. Relational is more powerful and more flexible than file systems. (It has some rough areas, but they can be worked around or fixed.)
Start backward next time and think how you would LIKE your code organized, forgetting about frameworks you know. If you do this often enough, you'll realize RDBMS-based code management is where we should be heading. About 90% of validation and field management could also be attribute-driven: data-dictionaries would do most of the grunt work.
With OOP and FP you are forced into choices such as "should this be its own class, or an object instance, or a group of classes for composition?" etc. etc. When tablizing your event & validation snippets, you are not forced to choose. They are grouped "by" anything you want, and by multiple groupings at the same time: multiverse. I agree that FP is probably more flexible than OOP, but it's also less disciplined: large FP systems look like the spaghetti-pointer databases that preceded RDBMS. Hierarchical, logical, and pointer-based DB's thrived for a while, but relational won, for a reason.
Your actual code would be "dumber" and event-specific such that paradigm differences matter less. Complex associations are managed via the RDBMS so that code rarely has to manage them.
I should make a distinction between framework coding and application coding. I won't say which paradigm is "better" for the first; I'm mostly focusing on the application-side coding here.
I’d add DAL should be shelved for a repository pattern and business layer shelved for root aggregates and value objects.
It’s all in Eric Evans’ Domain Driven Design book that still carries enormous weight.
On the current code base I work on I swear every single one of my pull requests contains large amounts of variable and method renaming to make it easier for the next person because if it takes me half a day to compute an understanding of what "var result = apiTotal - total" is actually doing then it isn't named nearly clearly enough.
One thing I want to point out -- if at all possible do not use decimals for money:
> We could use decimal (and we will, but no directly), but decimal is less descriptive. Besides, it can be used for representation of other things than money, and we don't want it to be mixed up. So we use custom type type [<Struct>] Money = Money of decimal .
The custom type is a great idea (try to write code in languages that make this concept easy, I suggest Haskell & type/newtype). The problem here is that decimal is the wrong type for storing money[0]. Your first IEEE754 floating point bug teaches you this, but in general trying to write code around manipulating decimals can get very messy really quickly when precision is involved in any case. Another example is JSON, JSON numerics are actually all floats under the covers, so this means if you store more precision or a bigger number than it can handle, things can get wacky if you're not careful -- this is one of the places where being "stringly typed" (and defining your own unpacking to go with your domain types) can be very helpful.
Libraries like dinero.js[1] exist because of how surprisingly hard this problem is, kind of like how moment[2] exists due to how hard dealing with time can be.
[0]: https://stackoverflow.com/questions/3730019/why-not-use-doub...
[1]: https://github.com/sarahdayan/dinero.js
[2]: https://momentjs.com
Note that the code here is .NET, where decimal is a type explicitly for use in financial calculations[0]. It is still floating point, which means you still need to really watch what you are doing, but it's very high precision.
In general though, if your application allows for it, you should store money using integers representing cents (or the relevant smallest unit for the currency).
[0]: https://docs.microsoft.com/en-us/dotnet/csharp/language-refe... (this is a page for C#, but it is more useful than the general page for Decimal)
The key difference between a decimal type (in any language) (regardless whether it's fixed-point or floating-point) is that it's not BINARY floating point. Yes, you need to watch what you are doing (shouldn't you always?)... but you can limit analysis to the appropriate precision, without worrying about binary conversion artifacts like 0.3 isn't 0.3.
> In general though, if your application allows for it, you should store money using integers representing cents.
What you're describing is a poor man's fixed point. Much better to just use a fixed point decimal type so you don't need to remember to apply the scale factor everywhere.
In any case, you can't get around the need to determine a ceiling in your necessary precision.
I've been thinking a lot about these terms and have been defining these roles internally as:
junior: can build relatively simple solutions, still gaining experience.
mid-level: can build complex systems, but the solution is going to be complicated.
senior: builds simple solutions to solve complicated problems.
I mentor internal devs to help shift their focus from learning more complicated technologies toward learning how to produce much cleaner and simpler solutions. This is the career path that I think helps them the most and also helps our company scale.
1. I now have two different languages for client and server, and can't share e.g. validation code.
2. Dealing with 2nd class linux support (no REPL)
3. No library that combines the maturity and simplicity of express.js. Yes I am aware of ASP.NET Web API and no I do not think that compares.
So as much as I love pipes and partial application and concise syntax, F# would create an explosion of complexity, not fight it.
EDIT: This also follows the common trend I see of starry eyed functional programmers who - to put it bluntly - don't seem to know what they are criticizing.
Of course some things from here we can do in C#. We can create CardNumber class which will throw ValidationException in there too.
A Result datatype with all the bells and whistles is what, a few hundred lines in C#? I agree that it sucks that there isn't one built in, but if you like them and you're stuck in C#, code one up and forget about the 'issue' ever again.
But that trick with CardAccountInfo can't be done in C# in easy way
That example looks trivially translatable to an abstract class with two concrete sub-classes to me.
EDIT 2:
The F# docs are awful. Struggling to find the API for the Result module. There's a guide on how to use it, but when you look at the core namespace it's missing.
> 1. I now have two different languages for client and server, and can't share e.g. validation code.
You could use Fable to transpile F# to Javascript, keeping a single language
> 2. Dealing with 2nd class linux support (no REPL)
FSI is available under Linux, but I admit it's got some hairs on it. (FWIW, FSI on Windows needs a lot more TLC too).
> 3. No library that combines the maturity and simplicity of express.js. Yes I am aware of ASP.NET Web API and no I do not think that compares.
This is purely a matter of taste. While express.js is very accessible, I don't think it's any more so than Giraffe (a wrapper around ASP.NET Core).
I'm all-in on F# now - once you're over the (mild) initial learning curve, you see huge dividends from the smaller codebase, static typing, fewer null-checks and drastically less testing required.
That would only solve your runtime issues. You would still need to write the same code twice.
You could use Fable to transpile F# to Javascript, keeping a single language
That seems like another explosion of complexity. Source maps, mapping F# to a JS constructs when debugging, niche tooling, wrapping other JS libraries in a nicely typed F# package... IME it's best to avoid transpiling anything more exotic than typescript.
FSI is available under Linux, but I admit it's got some hairs on it. (FWIW, FSI on Windows needs a lot more TLC too).
I appear to be mistaken - I remember I was excited when the dotnet tool came out, but then they took away the REPL. FSI is mono only, right?
This is purely a matter of taste. While express.js is very accessible, I don't think it's any more so than Giraffe (a wrapper around ASP.NET Core).
Yes it's very subjective, we can agree to disagree here.
I'm all-in on F# now - once you're over the (mild) initial learning curve, you see huge dividends from the smaller codebase, static typing, fewer null-checks and drastically less testing required.
I love F#, I used it professionally and it was a great experience. And you are right, the conciseness is hard to beat. But the static typing/fewer null checks thing is easily solved in JS land with the typescript compiler.
Yes, you do lose source maps by transpiling, but in my (albeit fairly limited experience), the type checking means I have drastically less debugging to do in the first place. You're right, though, the tooling leaves a lot to be desired (and this is also on the assumption you have TS definitions for third party libraries - I agree it becomes a lot more difficult if you don't).
FSI is available in both Mono and .NET Core 3 (which is still in preview).
Haha, no thank you
>OOP languages for modern applications gives you a lot of troubles, because they were designed for a different purposes
For which purposes if not software development exactly?
Well, if you recognize only 1 category "software development", I can't help you. C was designed for software development as well, but you wouldn't chose it to do web development, I hope? It's a good tool when you need to develop something small and resource efficient. OOP fits good when you don't have much of concurrency, but you manage complex states in memory (which you do with help of objects and inheritance). And functional programming fits well when you need to manage data flow applications.
And truth is in a big complex system you need a decent support of both FP and OOP. Point is that languages like C# decently support only OOP.