Skip to content

Instantly share code, notes, and snippets.

@ninjarobot
Last active June 2, 2020 09:21
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ninjarobot/6ccb762a990b563c46ddd3d932ec1c56 to your computer and use it in GitHub Desktop.
Save ninjarobot/6ccb762a990b563c46ddd3d932ec1c56 to your computer and use it in GitHub Desktop.
How I ended up as an F# developer.

My Road to F#

I came from the OOP world, most of my professional work was in Java or C#. The languages were similar, and I could use them interchangably as I built software in each of them for several years. With enough experience in both, I was comfortable with either ecosystem and could generally be productive in either. Sometimes the job required one or the other. Sometimes the target OS constrained my choice to Java, which frustrated me a bit, because the languages were so similar, why would the runtime try to restrict a choice? I enjoyed writing server applications on Unix in college, and Linux afterwards, because the machines were so easy to configure compared to Windows machines that took a few hours to install and configure to get my software running. On Linux, I could type a handful of commands into a newly installed system and be up and running.

Sometime around 2005, Java had stagnated while C# was flourishing. They both released generics, but then C# came out with new features like LINQ and Tasks, both of which I sorely missed when I worked in Java. C# really became my language of choice, but it often didn't fit with my OS of choice. I came across the mono project, which was in 2.6 at the time. It worked on some simple projects, crashed on others, but I got to know what did and didn't work well. The Base Class Libraries were pretty nice, but classes and components from Windows Forms or ASP.NET didn't tend to work so well.

I worked on a few systems that required embedded scripting, and this is how I was introduced to Python. When embedding a scripting engine, there is a runtime host, then it's usually possible to make some features from the hosting application available to the script. To make it all work nicely for the people writing script and to test the hosting bridge, I found myself writing quite a bit of Python. The language was nice and terse, and didn't require all the ceremony that C# and Java did just to create normal objects and functions. This also made it much nicer for people to express their application logic without needing to spend a lot of time and rigor learning language keywords and programming techniques.

While I appreciated the succinctness and approachability of Python, I soon found that the larger effort came on the testing side. There is no compiler reminding you of spelling or scoping errors as you type. At best, there is a linter, but you really don't know if anything will work until you run it, which means testing everything. And I mean everything - when working in a compiled language, you can take it for granted that the compiler will make sure everything fits. If something expects a number, the compiler will make sure it gets a number rather than a string of characters. The more you can represent to the compiler, the more it can check for you, and there is no point in testing something the compiler will check anyway. In Python, all of these things have to be checked by actually executing the code in tests.

Tests are certainly an essential part of every developer's toolchain, making up for what a compiler can't check or verifying behaviors that can't be reasonably expressed in the language. But there are a couple of problems with them. First, writing tests is very subjective, making it easy to neglect testing part of the code. Test coverage can be analyzed and, adding more rigor, meeting certain testing metrics can be a requirement before shipping code. Another problem is that writing all these tests requires a significant amount of code, both for the tests themselves and to make sure that the code being tested is written in such a way that it can be tested in isolation. Abstraction layers are often made entirely so mock implementations can be used in testing, when there is no other reason for the abstraction. That can lead to unnecessarily complex dependency injection to compose the various implementations, and even these compositions need to be tested.

All the complexity and effort that comes with testing are often rationalized as "part of the process" or "a necessary evil," but it's hard not to think of them as baggage. Any change to the application logic also comes with changes to tests. Evaluating the impact of a change is difficult because there is the change to the application code as well as all the tests that may use it, and until the code change is in place and tests begin failing, it's often not possible to assess the scale of a change. This is where Python started to fall apart for me. All the time saved by the expressive language was lost in the amount of testing required, to the point it is incredibly difficult to maintain a large Python codebase because the cascading effect of every change becomes impossible to comprehend. My adversity to change became greater than ever because of the fear that I'd miss something and ship bugs or be unable to reliably estimate delivery and either fall behind on schedules or become overworked trying to recover. It's a problem in C# and Java, although the compilers can check quite a bit, so the problem doesn't emerge until the codebase is a lot larger than Python.

This led me to stick to C# and Java for larger systems, because I have a consistent, repeatable machine - the compiler - doing a lot of the work for me. There is no subjective decision about whether or not the types fit together at compile time, and that helps keep the testing surface reasonable.

Around this time, F# came into the picture. A technical lead in my company who knew I was a bit of a "languages nerd" had read about it and asked me if I had considered it. I didn't know much about it, but I saw it had a REPL, it inferred types, had module system, and had list comprehensions. It looked a lot like Python, but had some other strange syntax with things like "match expressions" and "discriminated unions." While I was interested, I had this concern in my head that if it was this much like Python, it was also not going to scale to large projects, and if I wanted to deal with that, I'd just write something in Python anyway. I dismissed it.

Instead, I was much more focused on moving our C# applications to Linux. Python was heavily considered, but there weren't SDK's available for the software we were working with. Java SDK's were available, but the port was going to be huge, basically a total rewrite, and there were licensing concerns around distribution. Mono had matured over the years and was finally to the point, around the 3.0 release, that it was a reliable runtime, so we went with it. There were parts of the application that needed to be rewritten - the SDK we were using had some parts that only worked on the Windows .NET Framework - but most of the application logic itself, as well as the development team that built and maintained it, could move right over to mono.

Most of the challenges with mono came down to tooling at the time. The packaging was not very well maintained, so we had to build our own. Most of the developers were using Visual Studio on Windows, so some project file adjustments needed to be added during builds, and a few mono libraries like Mono.Security needed to be copied over to Windows. Those weren't insurmountable problems, and they kept getting better over the years. But mono had two weak spots - HTTP services and threading. Threading in mono is quite different, and the threadpool is implemented much different from Windows. It took longer for mono to add threads for handling TPL tasks, and longer to clean them up. There were some deadlock issues, some fixes that only worked with the boehm GC and others that required sgen GC. We mostly got past those, but the killer was issues with HttpListener.

The HttpListener class was the basis for practically every web framework in the .NET world, but there were some issues with the asynchronous callbacks that resulted in occasional deadlocks. Like all concurrency issues, they are hard to reproduce so quite difficult to know if a patch fixed them until running at scale. It proved to be more resilient to have monitors that would restart hung services, but even still, the problems are there and talk of moving the services off .NET and mono began to look like the only option.

One afternoon, I saw a post from Scott Hanselman on an F# Web Server. It looked intereting, so I took a look at the web framework. This Suave server could apparently run on mono, which piqued my interest. I tried a little "hello world" type application, and it worked on mono just fine. I had some tests that I had used to bombard our HttpListener on mono and lock it up, basically if you create enough HTTP connections and drop them while they are returning their response, the listener would lock up. I fumbled through enough F# to create the same type of server in F# and hit it with my tests. I almost couldn't believe it when IT WORKED!

The first thing I did was look for HttpListener to see what it was doing to avoid the locking, hoping I could apply the same technique to my other applications. This is where I was really shocked. There is no HttpListener in Suave. Somebody wrote a web server from the sockets up. I didn't understand the syntax at all really, but I could appreciate that in a handful of source files, there was a complete web server that performed quite well and overcame the platform limitations inherent to HttpListener. This was going to save our codebase from a rewrite that would certainly impact the whole team! I just needed to learn enough F# that I could replace our HTTP service layer with Suave listeners, and we could reuse everything else.

It wasn't that simple to replace the HTTP services, some stayed in C#, some moved to F#, but this breathed a new life into cross platform .NET development a few years before the cross platform .NET Core became viable. F#, Suave, and mono turned out to be a gateway into building reliable and scalable applications in .NET on Linux. Along the way, I learned my first impression that F# would hit the same testing scalability issues as Python, were completely wrong because everything gets checked by a compiler.

It quickly spread from the service layer into the full stack of the application, where I learned about how much simpler it is to express application logic in F#. Behaviors that I would implement in C# or Java with lots of interfaces and strategy classes turned into smaller discriminated unions and match expressions. Structures that I would implement as classes became records which gave them structural equality comparisons. This simplified the semantics of what would otherwise be complex comparison logic to visit all the data in properties and subproperties. There was a lot of fighting with the compiler at first, but by the completion of the first major project (replacing a 25,000 line C# application), I had learned how to leverage it.

I realized all the places I wasn't leveraging the C# compiler for type checking because it had been too much work to do so. Implmementing structural equality in C# is non-trivial. There are ways to do it, but it's subjective, developers can certainly cut that corner and make a class without it. But then one day when you realize you need it, then these subjective implementation decisions make for a lot of unexpected refactoring. It's work that the F# compiler will just do for you. Kind of like how the C# compiler added auto-implemented properties, saving some work for the developers and getting a consistent implementation, only F#'s compiler does a whole lot more of it. The same can be said for behaviors, which could be defined as an interface, but in F# they are often modeled more simply as OneType -> AnotherType, where those two types represent the input and output, or initial state and final state.

Eventually I became proficient enough that I do most of my day to day development in F#, and the consistency in implementation that I get from the compiler leads to a consistency in delivery. I can stay on schedule, and the corners I need to cut are hardly ever in feature functionality. Tradeoffs are usually in accepting a little less compile time safety in favor of a little more testing. Tooling has gotten better and better, as has the cross platform support. The ecosystem is growing as well, and I feel proud enough of my work that I feel I can be a constructive part of it. It's not an easy language to jump in to, like everyone else, I needed some incentives to push me along (thanks for Suave's mono support!), but I'm very happy to be able to develop F# code every day.

@brettrowberry
Copy link

Excellent story

@ITSecMedia
Copy link

Thank you for sharing your story!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment