Why is F# code robust and reliable?
devblogs.microsoft.com
devblogs.microsoft.com
The long answer - https://bartoszmilewski.com/2014/10/28/category-theory-for-p...
No worries, here's a slightly longer description.
Languages such as F# have static type systems which can catch logic errors during compilation. The kind of defects type systems can catch are often "low hanging fruit" issues such as logic errors and typo's.
In many ways, programming languages with a sufficiently mature type system provide the same benefit as pair programming. When they are used in conjunction with mathematically sound constructs, such as immutability, Functors, Monads, Monoids, etc., these languages can be a productivity/correctness force multiplier.
I never really got into F# or Haskell (more than some tutorials) so can't really comment on the type safety part.
In open-world domains, like business information systems, static typing is often an obstacle to fast adaptation.
Whereas immutability provides value in every domain, unless the performance requirement cannot be met.
The application domain is not relevant because you rarely know the domain up-front. Even if the domain is fully known by business stakeholders, it's not known by the application developers, and those domains can be vast. Application development is a constant process of learning the domain and of extending the existing functionality.
This is why all the talk about how LLMs are going to make it possible to replace programmers with people using prompts in English doesn't make much sense. Because the act of programming is primarily one of learning and translating requirements that are initially confusing and context dependent into a precise language. Programming is less about making the computer dance, and more about learning and clarifying requirements.
Static typing helps with refactoring, A LOT!
So when your understanding of the domain changes, YOU WANT static typing because you want to safely change already existing code. You want static typing precisely because it gives you “fast adaptation”.
It's the same argument for why someone would pick Clojure over other dynamic languages. Clojure gives you some guarantees due to the pervasive use of immutability, such that it gives you a clearer view of the API's contract and how you can change it. But statically typed FP goes even further.
I've been involved in projects using dynamic typing (PHP, Perl, Ruby, Python) and with no exception, the code became a mess due to the constant evolution. This is one reason for why we preferred an architecture of microservices because it forces you to think of clear boundaries between services, and then you can just throw away or rebuild services from scratch. Large monoliths are much more feasible in statically typed languages, due to the ability for refactoring. And no, while unit testing is always required, IMO, it isn't the same thing.
I, for one, prefer more static typing, rather than less. I prefer Scala, OCaml, F#, or Rust. And I've seen some difficult refactorings accomplished in Scala due to its expressive type system, although I can understand why it can be a turnoff.
The downside of having more static typing is a bigger learning curve, so you end up sacrificing horizontal scaling of software development (hiring juniors fast) over vertical scaling (doing more with fewer, more senior people).
Another downside is many times a slower compiler, which changes how you work. Once the code compiles, it may be correct, but then again, you end up doing less interactive development, so you work more in the abstract, instead of interactively playing with the code. I.e., Python's `pdb.set_trace()` is rarely available in static languages. I've always found this difference between dynamic and static languages quite interesting.
Why, then, don't you prefer languages with more static typing? Scala, Ocaml, F#, and Rust are middle of the road at best.
It seems you're echoing that the pragmatic choice for a business application is to stick to typing basics (within some margin of what is considered basic).
Haskell, for example, is harder to pick, and I wouldn't pick Idris even if I founded my own company.
It's supposed to be 'brittle', in the sense that the compiler verifies your code and if the logic changes, the compiler complains. Everything else is a bug.
> But, if you have business people tell you, you have to pass some additional information through your system without doing anything to it, and you answer, I have to refactor all my type definitions, then you have some explaining to do.
And the explanation is that this is software development, there is no "without doing anything to it". If there's a new requirement I need to adapt the code - take it or leave it, I'm not a wizard. And yes, I have done that already in some form and it was almost always received properly. 'Business people' sometimes have no understanding of software development.
The logic "code changes -> only dynamic typing" isn't valid, in my opinion.
The data model did change though. Adding an extra field, even if you don't use it, changes the shape of your data.
Ultimately your data is going to be typed with or without your approval. It's unavoidable, because eventually the data needs to be bits on a disk or on the wire. It's just a matter of how aware of it you want to be.
If you can reasonably keep the shape, and all possible shapes, in your head then fine. But I think you'll find this becomes less feasible as systems grow, and even less feasible in a corporate environment when teams come and go.
In contrast, Javascript is weakly typed. Yet, Clojurescript, which compiles to JS, retains Clojure's strong typing principles even in the Javascript (weakly typed) runtime. That (to certain degree) provides benefits that even Typescript cannot.
Typescript's type checking is limited to the program's boundaries and often doesn't cover third-party libraries or runtime values. Typescript relies on static type analysis and type inference to catch type errors during development. Once the code is compiled to JS, all type information is erased, and the JS engine treats all values as dynamic and untyped.
Clojurescript uses type inference, runtime checks, immutable data, and dispatch mechanisms, optimized by its compiler, to achieve strong typing for the code running in the JS engine.
Robust result type benefits give way more
Logic defined in terms of immutability can trivially support "undo" and "replay" functionality, amongst other more interesting workflows such as event streaming.
Logic defined in terms of mutable collaborations cannot do so with the same ease.
I was thinking of the benefit statically typed languages offer and not the difference between them and dynamically typed ones.
[0] https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
>In F#, all variables, functions, types and files can only depend on variables, functions, types and files defined earlier. The benefits of this are the fact that a circular dependency is not possible by default and extra clarity with “what depends on what”, which helps during code analysis and PR reviews
Ehm. So it's like C... with no forward declarations?
In C you can have a function using a global variable, and change to this global variable will affect the function behavior.
The XML is not part of the language. You could invoke FSC manually (again, with the files listed in the correct order).
It scales very well IME.
Would C# be better with circular library dependencies?
Here's an example of a worst-case scenario (GUI frameworks and the extensions have notoriously huge amount of code): https://github.com/fsprojects/Avalonia.FuncUI/blob/master/sr...
But realistically an average project would look closer to this instead: https://github.com/DiffSharp/DiffSharp/blob/dev/src/DiffShar...
Once you have enough files, it might be a good idea to factor out separate concerns into different projects.
Dealing with cycles in Python is a total pain full of cryptic error messages, while in Rust cycles are fine as all top level items "come to exist" at the same time so they can depend on each other without any issues, and it makes refactoring a breeze.
Sounds like a compiler limitation touted as a feature.
The bigger issues are non-technical where it's just very few companies use F# in production. As for the technical ones - there are not that many and most of them are known (like mentioned above - code generation related, so the criticism usually mentions the same limited set every time, like code-first model definition in EF Core).
Edit: or do you mean the comparative lack of libraries?
anything else?
- Immutability by default. Check
- Discriminated unions with exhaustive check. Check
- No nulls by default. Check
- No exceptions in the business logic. Check
- Strict dependency order. Rust doesn't have this
- Warnings on unused expression results. Check
- Typed primitives. The level of ergonomics this is implemented with in F# I'll say Rust doesn't have this
- Explicit conversions. Check
- Functional approach to concurrency. I think Rust's compile time safety against data races gives this a check. I've used channels for concurrent server processes, it's nice. Check
- Explicit dependency injection. I've never understood what this means
7/10
The article seems to mean what's described in this other article as "dependency parameterization" where the dependency is explicitly passed to the function (and every function it calls that also needs that same dependency). This is as opposed to, in OO languages, setting the dependency (typically) during construction (however the object is constructed). Or it's otherwise set in some larger scope than the functions which make use of it (global, module, object instance, whatever is appropriate to the language and task).
IMO the first and foremost principle of Functional Programming languages is that they are optimised around building programs in terms of function composition. And anyone who had to work with borrow checker and closures for 5sec knows, that this is not the case for Rust.
https://www.geeksforgeeks.org/broadcasting-across-arrays-in-...
I also like the constraint programming support combined with performance in implicit parallelism:
sudo apt-get install minizinc-ide minizinc libgecode-dev
curl -fsSL https://install.julialang.org | sh -s -- --default-channel=lts --add-to-path=yes --startup-selfupdate=3600
>julia
import Pkg
Pkg.add("CUDA")
Pkg.add("MiniZinc")
using CUDA
CUDA.versioninfo()
Enjoy the fun =3
Constraint programming is not Julia specific =)
A great feature that Golang couldn't get right years later.
As a newcomer to Go, I find myself struggling to express business logic all the time. Zero-valued types make the situation even worse. You’re constantly constructing invalid values of types (nil/zero-valued), by design. Go is “so easy”, but when you inevitably run into one of the footguns it’s “yeah just don’t do that”.
One of the primary reasons for people to dislike Python is its dynamic typing. When Go came out, that was totally fair. But since then, Python has evolved and improved massively. It/mypy now supports type-safe structural pattern matching, for example. It’s very expressive, and safely so.
Meanwhile Go barely evolved. Generics landed just recently. They’re only now experimenting with iteration, lifting it from a purely magic, compiler intrinsic concept. And still, no enums of course. The “type system” are structs, or very leaky type wrappers (nowhere near the safety of Rust new types, for example). People are obsessed with primitives.
I can see the appeal of a simple, stable platform, but Go really ran too far with that idea.
func main() {
var p *int
*p = 1
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x466462]
That's memory safe. It's also common and terrible. As is Java's NullPointerException, C#'s NullReferenceException, Javascript's "is not a non-null object" and all the other, unnecessary and awful, yet memory safe manifestations of the same design mistake plaguing contemporary software.Understand that when Hoare coined the phrase "billion-dollar mistake," he was referring to his addition of null references to ALGOL W. ALGOL W references are not C pointers that allow willy-nilly pointer arithmetic and all the UB that come's with that. An explicit design goal of ALGOL W was to prevent unsafe dereferences. A dereference of a null reference in ALGOL W is defined behavior and yields a runtime error.
NULL OR UNDEFINED REFERENCE
An attempt has been made to access a record field using a null or
never initialized reference.
That said, ALGOL W isn't memory safe either: it attempts to mitigate memory violations, but this is not comprehensive. var bytes = (byte[]?)null;
Console.WriteLine(bytes.Length);
That and, well, all variables must be assigned before use. You'd be right to point out it's a band-aid, a convenient one but still.At the end of the day, scenarios like a library spawning a Goroutine which tries to dereference a null where it doesn't expect and experiencing an unhandled panic, crashing the application, are practically impossible in .NET. It is not necessarily watertight, especially around JSON serialization, but it certainly evokes a "can't believe you guys still struggle with nulls/nils" kind of reaction.
I'm just amazed that, given that golang's inception is relatively recent, somehow this old lesson had not been learned by the designers. Accomplished and learned people created this language. How did that happen?