Yesterday, I spent a few hours rewriting a little JavaScript command line utility into F#. I used VSCode with the excellent Ionide plugins.
Here are some takeaways in no particular order.
F# is terse but readable. The program ended up being a little more than 1/2 the size of the JavaScript version. I never had to specify a type, yet I still got excellent intellisense, and accurate errors displayed to me in the editor. In fact, whenever I ridded my code of the little red underlines, it always compiled, and it always worked.
People in other statically typed languagesβ including ML derivativesβ often think your code is clearer and more maintainable if you at least annotate your function definitions. F# seems to disregard that notion, and does it successfully. I think it gets away with this partly because it is a language that is almost always used with an IDE or a good IDE-like plugin (Ionide). So, if you really canβt understand how to call a function, you can hover your mouse over the function and see its inferred argument and return types. Also, my experience so far is that F# somehow really encourages teeny tiny functions. My biggest function was the βprintHelpβ function which was 5 lines of code and thoroughly understandable at a glance. Tiny functions are generally understandable (unless theyβre tiny due to weird APL-like syntax), and understandable functions donβt need to be littered with type information to improve understandability.
That said, I do think that if I were to author an F# library or something like that, Iβd annotate make my public functions. It just seems like the right thing.
There were a handful of hiccups on the way.
Iβm using a Mac (alas, itβs pretty much required for my day job), so Iβm a 2nd class citizen in the F# world. It took a while for me to get everything set up and working. The F# toolchain outside of Windows seems solid, but poorly documented. It wasnβt rocket science to get up and running, but it was definitely not as smooth as, say, something like Node. (Iβll publish a step-by-step in another post.)
Terms can be overwritten without any warning at all. Hereβs an actual example of how this gave me a bit of a snag.
I had some code that looks like this:
open Fake Target "Clean" (fun _ -> β¦ )
All was well in the world. Then, I needed to pull in a little compiler tool, so I added this line:
open Fake open Fake.FscHelper Target "Clean" (fun _ -> β¦ )
Crash and burn. It turns out that Fake defines Target but so does Fake.FscHelper. And the two definitions clash. But rather than get a useful warning or error like βTarget is defined in Fake and in Fake.FscHelper. You need to disambiguate.β I just got a weird type error indicating that I was calling Target with the wrong kinds of parameters.
Code that had been working was suddenly not working and the reason wasnβt very obvious. I honestly donβt know why F# allows you to redefine a term like this.
Fixing it was a pain, too. As far as I can tell, thereβs no way to exclude terms from being imported with an open statement. Thereβs no equivalent to the ES6 import syntax:
import {foo} from βbarβ
I think this is a pretty glaring limitation. I wound up having to reorder my open statements and then fully qualified or aliased my calls to Fake.FscHelper.Target. Far from ideal.
This is a big deal. I wanted to do something like this:
let exec [] = printfn βA term is requiredβ let exec [hd] = doSomething hd let exec [hd::rest] = doSomethingElse hd rest
No can do. In this case, I was able to pretty easily work around the limitation by using pattern matching. That said, in a statically typed functional language, the ability to overload a function seems pretty important and pattern matching canβt always be the solution.
For instance, I would like to be able to call a toJson function and pass it any type, and have it call the appropriate, type-specific, function if available, and fall back to some generic one if no specific one has been written.
F# does have member functions (or maybe theyβre really methods), which allows you to kind of work around this, but it feels like Iβm dropping out of the functional world and back into OO. This makes me sad.
Evidently, itβs rare for a statically typed functional language to allow default arguments. It makes currying harder to implement. But OCaml can do it, so F# should be able to, too.
Not a big deal, but I did have to work around it in one context. The workaround was pretty elegant, thanks to pattern matching, so maybe this gripe isnβt a big deal. Still, thought Iβd jot it down.
Itβs too early to say, but I did really enjoy the experience of writing and reading F#. The tools on Mac are surprisingly good. The language is highly expressive, terse, and intelligibleβ¦ A winning combination. If I continue down the F# road, itβll be interesting to see how active the community is, how robust the ecosystem is, etc. But for now, I have to say tentatively that Iβm a fan.