The Early History of F# [pdf]
dl.acm.org
dl.acm.org
I want to share some insights of using F# with the community. We started from a C# codebase, and realized that a better language can help weed out most bugs in our system. And yes, it works. Pretty much all bugs we face these days are parts where the F# world touches something non-F#, like .NET and other libraries written for C#, where interaction (like nulls) is not well-defined. We've taken Scott Wlaschin's (fsharpforfunandprofit.com) teachings to heart, and we have a giant banner in our office that reads "Make illegal states unrepresentable".
It took a bit of learning for everyone to jump in, but our dev team has loved the experience as the language is a pleasure to use; when they need to go back to write C# or TypeScript code, a lot of these learnings transfer. People just become better programmers (as is true with learning any functional language).
To get all the benefits of F#, you must adopt the whole paradigm. While F# allows C#-like OOP, and while this can be an initial stepping stone in the path towards F#, you must go all the way. If you simply do OOP, the trade-offs aren't worth it, IMO, as F# is a functional-first language, and the OOP is mostly provided for interop with the rest of .NET.
IDE support has been janky in the past, but its improving. Latest VS 2019 is pretty good, and JetBrains Rider works pretty well on the Mac.
OOP is fine with functional if you make it immutable too, so I don't see the problem (I've not really got a problem with mutable OOP, or state generally, if it's done carefully).
This depends on your data model, of course. But for a list of integers you could do the following:
1. Have a unique data type that is the list of Deltas, plus the initial element (So for instance the list [5, 3, 6] would be encoded as (3, [2, 1]).
2. Provide a sort function that creates such a sorted list.
3. Make the sorted list your input parameter.
This is obviously oversimplified, but I hope you get the idea.
A cheaper alternative is to
1. Make an opaque datatype "sorted list", together with a function that translates this type to a normal list. Implemen this type just as a list, but keep the implementation private.
2. Provide a function sort, that is the only function that can yield that type.
3. Demand that type as input.
Things like that require runtime logic, there’s no way around that.
In Reason/OCaml, you can create a module interface (.rei or .mli) which makes the type opaque to functions outside the module. So instead of saying `type t('a) = list('a)`, it'll just say `type t`.
The compiler relies on this type information to decide whether functions are well-typed. So if an external module presumes to know the structure of the type and does an operation on it, it becomes a type error because the compiler simply doesn't know about the actual type. This is similar to encapsulation in OO where we don't expose a field to the outside world, but here it is done by virtue of the type signature itself, which I found to be a more powerful guarantee of encapsulation.
The module can then expose functions like `append` which would be the only way you can manipulate the list. This function in-turn can ensure the postcondition, guaranteeing that the list stays sorted. At the point in which we want to use the list for functions not supported by our SortedList module, we can turn it into a regular list with a `to_list` function. Since the underlying type is already a list, the operation is virtually free. The function would look like `let to_list = (xs: t('a)) => (xs: list('a))`. It is an identity function, with just the type changed for the compiler.
Similar rules about `append` applies to the constructor: we can create a new `SortedList` only with a `SortedList.make` which can ensure the postcondition. There will be no other way, thanks to the type being hidden, to create a value of the `SortedList.t` type.
I'd be less charitable and say that OOP in F# is a tedious, verbose mess. Functional programming in F# is extremely concise, at the expense of OOP. I wrote a parser/lexer in F#, to be used from C#, and ended up rewriting it in C# instead of dealing with the interop. I'm hoping that some day, C# will eventually have F#'s pattern matching power. They're slowly introducing more and more features to do it, and there's no major technical reason why C# wouldn't be able to.
...unless you are using a language where types are not part of the contract and not used in dispatching, such as Elixir.
I have mixed feelings about that because, on the other hand, in runs on the BEAM VM and as long as lack of typing leads to a proper crash, you may be OK as long as your code is written correctly.
I ended-up using a similar trick (with different syntax) in my own language, Empirical. I needed a way to infer a Dataframe from a file while in a REPL. It wasn't until I read the MSR paper that I realized I could do it entirely in the compiler without creating separate logic for the interactive users.
It’s not perfect though. When I tried to use them I ended up with problem so I parsed the data in more traditional way.
CsvProvider<"trades.csv">.Load("trades.csv")
I personally didn't like having to list the file twice, so in Empirical[1] it's just: load$("trades.csv")
My current version requires the dollar sign to tell the compiler that the parameter will be static (known at compile time). I'm planning on an updated version[2] that will eliminate the dollar from the function call: load("trades.csv")
Basically, it means that users can just load the file like in a dynamically typed language, except that Empirical is statically typed.[0] https://fsharp.github.io/FSharp.Data/library/CsvProvider.htm...
[1] https://www.empirical-soft.com
[2] https://github.com/empirical-soft/empirical-lang/issues/23
I use Javascript/Typescript professionally and I really like both of them. F# though has a minimalism in its design that is very appealing. Code I write in F# feels a lot less cluttered as opposed to the same function in Javascript/Typescript. This minimalism seems to be part of the language design with some following specific examples.
- `let` is used to declare variables and functions.
- The last statement in a function is implicitly the return value.
- The type safety/mechanism is great and seems to be extremely similar to Typescript's so its easy to pick up for me (I'm not sure which came first or if there's a relationship but it certainly wouldn't surprise me).
This along with some classic functional ideas that are built in like immutability by default, automatic currying of functions etc all allow for a language that seems be made to get out of the way. That's a very compelling idea for a language design to me that I previously haven't seen.
Because functions and variables are both declared with let, perceiving functions as data kind of finally clicked for me since I first heard that concept 3 or so years ago. Another feature was to not require a semicolon as a line terminator and instead use it for another meaning as a separator. While JS doesn't require semicolons at the end of a line, its reserved for that purpose.
Overall, I find F# to be a refreshing language (from javascript, python, java and even others like Rust or Go but I haven't used those as much so don't want to make bigger claims) and I'm glad to see a post like this on HN almost like validation of a community behind the language. I'd be curious to get a feel for some companies that actively use it as well.
I also love that there's a compiler for F# to Javascript so I can use it to build front end code as I learn the language. Its pretty incredible we can do cross compilation of high level languages like this and with others like Rust and Go to wasm etc
Edit: formatting of bullet points
[1] https://bucklescript.github.io/
[2] https://ocsigen.org/js_of_ocaml/3.6.0/manual/overview
[3] https://discuss.ocaml.org/t/state-of-ocaml-and-web-assembly/...
I really love the ML-family type systems. They're really great aides to programming, and makes planning really nice. F# has that, plus that usability from C# and TS. It's also, like you said, got a really beautiful syntax IMHO.
I keep coming back to Rust though, because Rust is more general purpose, more performant, and applies better to the stuff I like to work on for my hobby projects. I'd love to use F# more but can't find a reason to at the moment.
TypeScript, Python and Rust have become my comfort languages, but I like Rust the best, because it's got a lot of really nice features from ML languages and a powerful and safe type system.
More broadly there seems to be a big push for updated (perhaps even modern) versions of established languages and I think this is an incredible development to witness.
Rust, Go, Typescript, Scala, Kotlin are all the rage and seem to replace C/C++, javascript, and java. Others might exist for either side as well, this is just off the top of my head. From what I see and read as well functional programming seems to be a lot more popular and a lot of these languages are either built for it or with a good base of support for it. It'd be really interesting to have some hard data to know if this is accurate or just my perception.
My only other exposure to functional programming was an undergrad class required for a CS major (thank gosh it was a requirement too). It used Scheme for the language and Structure and Interpretation of Computer programs for the text book[1]. Looking back, this was the most useful class I took and the ideas have stuck and continue to inform me today. Its the one text book I've gone back to on occasion just to take a look and I've found some versions of it for JS and F# even on github which I use to help learn the language or review the concepts in languages I'm more familiar with.
[1] https://mitpress.mit.edu/sites/default/files/sicp/full-text/...
I saw in a different HN post a few weeks ago that jet.com heavily used F# but obviously that's no more.
If a company were using F# and I were looking to switch and all the other stuff was a good match, the language choice would be a big pull for me.
While I use and am learning F# for a side project, there's no substitute for working with one professionally and shipping production code with it. I wish it'd be possible to convince my engineering group to use it.
(If you're interested in quant finance in London, I work at G-Research which uses F#; I write almost solely in F# there. You can contact me privately if you like at patrick+gresearch@patrickstevens.co.uk.)
This works great with "makes illegal states unrepresentable" approach. It helps to reduce a need for boring unit tests and lets you focus more on expressing domain in code directly.
I kind of can do it for Strings by doing something like this
type ValidStrings = 'name' | age | 'dob';
The easy way around this is a function to determine this function greaterThan2(num) return num > 2;
but it'd be cool to express this level of dynamism as a type. (* GreaterThanTwo.fsi *)
type t
val make : int -> t option
val to_int : t -> int
(* GreaterThanTwo.fs *)
type t = Value of int
let make int = if int > 2 then Some (Value int) else None
let to_int (Value int) = int
Now, `GreaterThanTwo.make x` gives you a value of type `GreaterThanTwo.t option`, meaning it can be `Some` value or `None`. If it's `Some` value, that means it really does contain an int greater than two and you can get that int using `GreaterThanTwo.to_int`.Smart constructor technique works well with 'parse, don't validate' approach [0]. You can push type construction to the boundries of your system so that you can work on a domain code with more precise types. It's not always so rosy however as too much types can become a burden.
[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
On a slight tangent, you might enjoy the slides from Scott on a different topic - model checking with TLA+ - from NDC Oslo this week:
https://www.slideshare.net/ScottWlaschin/tla-for-programmers...
He does an incredible job conveying the ideas and intuition behind a lot of the terms and concepts for a given subject.
The three main things I find similar are the syntax, ability to compose new types from other types, and the ability to limit types to certain options. Both support | as an OR and support an AND operation. F# does a bit more allowing the product of types though.
Syntax Example:
Typescript
---------------
type Check = {
amount: number;
account: number;
...
}F#
--------------
type Check = {
account: int;
amount: int
}I think I need to become a bit more familiar though with F# to understand the differences better. I hadn't yet heard the term nominal to describe a type system before so I'll look into it further. Perhaps I'm focusing more on the syntax than is warranted...
Completely separately, how does one properly format on HN? Is it possible? Is there a doc somewhere? I'm fairly new to commenting...
Your example is actually a really good one for discussing these differences, and the syntax is indeed similar. That TypeScript code is introducing a "type alias," which is a special name for another type, rather than a new type of itself. (https://www.typescriptlang.org/docs/handbook/advanced-types....)
This is a concept that F# does not have, because F# doesn't support structural typing. In TypeScript, you could just as well specify that a function returns
{ amount: number; account: number; }
directly, and it would be equally compatible with anything expecting a "Check," because Check is just a shorthand for that object type. I can't easily declare that a function accepts { account: int; amount: int; } in F#, I have to declare that it accepts "Check." That is the main difference between TS and F# here, and incidentally also the most basic difference between structural and nominal type systems.
I'm not sure why the syntax is similar though, F#'s is based on OCaml's record syntax while TypeScript's is really just a JavaScript object literal but with semicolons instead of commas. I believe it supports commas too. It may be genuine coincidence, or the designers of TS may know of the ML family, which is highly influential among language designers and implementers.
That actually helps me understand some F# code I wrote last weekend as well regarding construction of objects of a certain type...
I didn't know about the difference between nominal vs structural types but your explanataion makes sense. It's the subtle distinction between a type vs a type alias that you pointed out.
I personally use type aliases in typescript over interfaces, I think they're a more natural fit for JS than interfaces but that's a separate conversation :)
Language design is really interesting especially the choices designers make. I feel like they must all pull at least in part from each other and would be curious to explore any relationships but if its total coincidence that's pretty incredible too.
The TypeScript docs recommend the opposite, preferring interfaces. Interfaces are also structurally typed in TypeScript though; the major differences are that interfaces can only be used to declare object types: https://microsoft.github.io/TypeScript-New-Handbook/everythi...
The fact that interfaces are limited to typing objects but type aliases can also be used for functions is an easy +1 to type aliases.
My view is that declaration merging is a bit of an anti pattern that could cause trouble if two different people write an interface with the same name but to represent different data... now they get merged incorrectly. A type alias forces more explicit behavior where composition must be done by creating a new type alias as the union of two or more other types
type Foo = Bar & Baz
Edit: found the page, its here https://www.typescriptlang.org/docs/handbook/advanced-types....Yeah, not sure I agree yet but I could be persuaded... I don't think type aliases prevent extension, they just force it to be explicitly declared.
And that’s an intersection you’re describing there, not a union. Interfaces are explicit about extension as well, they use the “extends” keyword.
The explicitness I was referencing is around the declaration merging. Its automatic so unless there's a conflict between two interfaces with the same name but with different properties, a dev wouldn't know that it was being done.
I agree with using what makes sense for the scenario. I prefer type aliases for functions and interfaces for classes and that seems to work out pretty well so far.
It takes the form
type newname = oldname
You can do things like
type LoadReceipt = RecordId -> Record
let sqlLoad: LoadReceipt = implementation
let noSqlLoad: LoadReceipt = implementation
You can use it like an interface, and accept loadReceipt functions for anyone who needs to load receipts... Then partially apply a SQL version, noSQL version etc.
I use this in my code.
I feel like it really has hit the sweet spot between FP and OOP. And while I do tend to prefer the more terse ML syntax of F#, Typescript is pretty close and, frankly, offers a more ergonomic experience in your editor (F# syntax highlighting in every editor I have used is woefully underwhelming).
Add to the above the structural typing vs nominal and Typescript really does offer an immensely rich and flexible paradigm. The only feature I'd really like to see better support for in Typescript is currying/partial application These can be approximated in TS using it's incredibly powerful type system (dependent types using "extends" combined with inference using "infer"), but newer versions will throw an exception when the compiler sees an infinitely recursive type.
[1] https://discuss.ocaml.org/t/multicore-ocaml-may-2020-update/...
and ocaml isn’t always faster
I wouldn't go so far as to say OCaml is more interesting. F# has things like computation expressions, async, units of measure, and type providers, none of which are trivial (or exist in OCaml), several of which are incredibly influential, and all of which are interesting. The long slog towards multicore for the OCaml community has been kind of embarrassing, with Haskell having offered arguably the best concurrent and parallel programming support in all of software development for years and years now.
I will also note that F# gets new features fairly frequently as well, is cross-platform, and is extremely fast by virtue of running on the CLR, which I would not underestimate: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
The features that it is missing from OCaml are admittedly pretty major, though. Row polymorphic records and the structurally typed object system are missed, not to mention rich modules and functors.
It definitely has async: https://github.com/ocsigen/lwt
It doesn't have units of measure but due to strong abstraction properties of modules we can fairly easily roll abstract types.
It doesn't have type providers but it does have plugins for type-directed derivation of JSON codecs, equality, comparison, printing, etc.
> The long slog towards multicore for the OCaml community has been kind of embarrassing
IMHO it's been refreshing to see the pushback against 'multicore at all costs'. There is a benefit to taking the time to do things the right way. OCaml is a 30+ year-old language with an even longer ML heritage. It's going to be around for a while; it doesn't need to rush into a sub-par implementation. Meanwhile, people are writing highly concurrent, multi-threaded applications with it right now, and using multiple processes just fine for parallel computing.
Type providers are a native feature, no plugins required. I have no animosity towards OCaml (in syntax and semantics, I prefer it!) but this is a bit of a stretch.
I don’t think anyone would say Haskell does multicore the “wrong” way, and OCaml as you say has had decades to solve these problems. I love the language but the community is very small and the language evolution has been extremely conservative for the most part.
Yes, units of measure are really cool, and I understand they support dimensional analysis out of the box. It's quite unique, and I fully agree that abstract types don't fully replace them. Just saying that that's what people do in OCaml.
> I don’t think anyone would say Haskell does multicore the “wrong” way
And it certainly is not something that I said.
> OCaml as you say has had decades to solve these problems.
The 'problems' being the lack of multicore support? As I said, this is not really stopping people from getting real work done in industry–see everyone using OCaml currently in highly concurrent and multi-process parallelized applications, or in fact even people using NodeJS, Python, Ruby.
> the community is very small
Correct, but it is definitely growing.
> and the language evolution has been extremely conservative for the most part.
Not so correct. There have been massive amounts of changes in the last few years: https://www.ocamlpro.com/2019/09/20/a-look-back-on-ocaml/
And this is not even taking into account the features which OCaml had before then beyond F#:
- Polymorphic variant types - Structural subtyped object types - Named arguments - Optional arguments - Functors
I’ve interviewed with Jane Street and gotten an offer on the basis of my OCaml and F# skills. I understand where these ecosystems differ. I rest my case.
> Named arguments are allowed only for methods, not for let-bound functions, function values, or lambda expressions.
> ...
> Optional parameters are permitted only on members, not on functions created by using let bindings.
The major pain points are the tools. I tried setting everything up two years ago, or so, and I couldn't do it. While the language might be good (modules) everything else isn't.
If that's the case, it's sad that Visual Studio's refactorings for F# are so meager.
Most of the investment in refactoring has been for Roslyn analyzers, which are C#/VB specific. F# has a completely difference compiler framework.
I don't know if the F# compiler service or Roslyn came about first, but they are not compatible with each other.
I have a small story around it. A couple of years back I had the nasty habit of enumerating sequences multiple times with LINQ. Resharper constantly complained about it. Programmed some F# for projecteuler and advent of code. My nasty habit of multiple enumeration went away even in C#, because F# design affordances made me rethink enumeration and sequences. So since then I haven't triggered multiple enumration in Resharper with my own code. It is always somebody elses.
F# and Scala are extremely similar languages: garbage collected, hybrid OO/FP guest languages hosted on a widely-used runtime. Even though the superficial syntax is very different, and the advanced language features they provide are pretty different, the core experience of modelling your data as "dumb" data structures and transforming collections of data using higher-order functions is almost identical.
Scala ended up winning out for me due to a broader ecosystem (Much more OSS Java than OSS C# out there), better tooling (Visual Studio's F# support was always disappointing...), and easier interop (F# <-> C# feels a lot more clunky than Scala <-> Java). But I can easily imagine an alternate universe where I'm happily writing F# all day, and almost nothing would be different from my current work writing Scala.
> Specifically, method constraints were added, introduced by a deliberately baroque syntax:
let inline (+) (x: ^T) (y: ^U) : ^V = ((^T or ^U):
(static member op_Addition : ^T * ^U -> ^V) (x, y))
> This definition says that any use of + is implemented via inlining a call to an appropriately-typed op_Addition method, defined on the type ˆT or ^U, i.e. the type of either the left-hand or right-hand argument. The ˆT notation for type variables indicates statically resolved type parameters (SRTP), i.e. type parameters which are resolved to a nominal type at compile-time.Why ^ and not using the existing : for seperating types from values? :T * :U -> :V would have looked like types, not pascal ptrs.
Equally interesting are ocaml warts like let x2 = 1.0 +. 2.0 (+ not overloaded or defined for floats), or haskell's decision to torpedo the "class" keyword.
Or ocaml hijacking ::
let xs = 1 :: xs, with :: being cons, which was initially the lisp dot notation '(1 . xs)
In retrospect perl's syntax is sane compared to this.
`::` is normal for cons in an ML, right? Of the ML family of languages, I'm only aware of Haskell that chooses differently (they use the single colon).
* actor-based approach to concurrency[1]. Very useful when you want to design your code lock-free. Treating instances as agents that read messages from a mailbox, frees the developer from using low-level threading primitives. First time I've used this approach in production with AKKA and Scala. In F# it is much cleaner because the language itself is built on a better platform.
* exhaustive pattern matching[2]. Writing in F# means using pattern matching a lot. Exhaustive matching gives you the confidence to refactor and maintain your code. The compiler will warn you when adding a new value in your type and not covering the execution path that uses it. It catches a lot of errors before you even push the changes to CI.
* obviously immutability, conciseness, currying, these have been mentioned so far by others.
One nitpick in F# are Exceptions [3]. They approached it similarly to how it is done in C#. You are allowed to define and throw an exception that will "jump" somewhere in the execution path. I prefer when a method returns Option-style types. This way, you know what to expect from a call, and pattern matches on the result without adding one more execution path in your code, which covers exceptions separately. It was added to support C# style error handling, though I very much prefer an error-code-based approach.
[1] https://fsharpforfunandprofit.com/posts/concurrency-actor-mo...
[2] https://fsharpforfunandprofit.com/posts/correctness-exhausti...