Recently Dr. James McCaffery, posted Why he doesn't like the F# language. Quite a few of his points are subjective. People have different preferences and it seems like F# and more generally functional programming takes him outside of this comfort zone. This is fine, and I have absolutly no objections about views like this. I have a similar feeling when I'm in C# or Java. I don't feel safe, or comfortable, again it is just a preference thing.
However, there are a few points raised in the blog post that I don't really agree with. I'll tackle each one seperately not to loose any context.
- F# has a tiny user base.
I did a quick search on the day I wrote this post at a job aggregation site and found 109 job listings that mentioned F#. There were over 34,000 job listings that mentioned C#. And at MSDN Magazine, where I'm the Senior Contributing Editor, our F# articles get very few reads. The tiny user base means there is relatively weak community technical support on sites like Stack Overflow, compared to mainstream languages. Additionally, unlike other languages with relatively few users (such as R), there`s no real motivation for me to adopt F# from a career point of view, because of the very limited job opportunities.
While I somewhat agree, that F# adoption in industry has been slow. I think alot of this is to do with the fact that in the early days F# wasn't pushed as a general purpose programming language. This was obviously a marketing decision made in Microsoft, for reasons that are unknown to me. This decision caused an elitist view of F# in the early days with the preception that you need a advanced degree in a mathematical subject to use it, categorising it as only being good for Data Science, Finance, Mathematics and Research. Thus these areas were the early adoptors. In fact a quick browse of the testimonials page on FSharp.org backs this up. With testimonails coming from one of these areas. There are of course some exceptions most notably the design of Louvre in Abu Dhabi and it's use at GameSys.
However this metric is only one dimension and just because there are currently only a few jobs in a language doesn't mean you should not learn it. I'm currently learning langauges like Haskell, Coq and Idris. For the latter I doubt there is a single role in this country (although I'm willing to be proved wrong on this). Why do I do this? I hear you ask. Well I believe by learning different langauges and paradigms pushes me slightly out of my comfort zone and makes me a better programmer, in which-ever language I ultimately end up coding a commercial product in.
With the commercial prospects aside a conslusion is drawn that a small user base => a weak technical community. I don't know about other languages but I can categorically say that with F# this is simply not true. In fact, as I started writing this blog post, I raised an issue in the Paket project on github and within an hour, I had a fix presented too me.
For other sites like Stack Overflow, I can't really comment on the experience as I don't tend to use it much myself. However we can use F# to do some data munging to see how the community it doing. i.e. What is the average time for questions with an accepted answer to have got that answer?
To acheive this we can download the first 10000 questions with the F# tag, and write the result of each request out to a set of files.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
let baseUri = "https://api.stackexchange.com/2.2/" let [<Literal>] dataPath = __SOURCE_DIRECTORY__ + "/data/stackoverflow/" let dataDir = let path = new DirectoryInfo(dataPath) if not(path.Exists) then path.Create() path let getQuestions(page) = let outputPath = new FileInfo(Path.Combine(dataDir.FullName, sprintf "questions_%d.questions" page)) if(not <| outputPath.Exists) then let results = Http.RequestString(baseUri + sprintf "search?page=%d" page + "&pagesize=100&order=desc&sort=creation&tagged=f%23&site=stackoverflow") File.WriteAllText(outputPath.FullName, results) let writeQuestions() = [1 .. 100] |> List.iter getQuestions |
Next we can merge all of these questions using the Json type provider into a single list,
1: 2: 3: 4: 5: 6: 7: 8: |
let [<Literal>] questionPath = dataPath + "questions.json" type Questions = JsonProvider<questionPath> let questions = [ for file in dataDir.EnumerateFiles("*.questions") do yield! Questions.Load(file.FullName).Items ] |
Next up is getting the accepted answers. Firstly we build a map of the accepted answersId against the questions so we can relate them again later, then we use getAcceptedAnswers
to chunk the requests and write the results out to a file. Once we have the results we again use the Json type provider to merge the results up into a single list.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: |
let questionAnswerMap = questions |> Seq.fold (fun state question -> match question.AcceptedAnswerId with | Some answerId -> (answerId, question) :: state | None -> state ) [] |> Map.ofSeq let getAcceptedAnswers() = let answerIds = questionAnswerMap |> Map.toSeq |> Seq.map (fun (answerId,_) -> answerId.ToString()) let counter = ref 1 for answers in chunkBySize 100 answerIds do let outputPath = new FileInfo(Path.Combine(dataDir.FullName, sprintf "answers_%d.answers" !counter)) if (not <| outputPath.Exists) then let answersStr = String.Join(";", answers) let answers = Http.RequestString( baseUri + sprintf "answers/%s?order=desc&sort=creation&site=stackoverflow" answersStr ) printfn "Writing answers %s" outputPath.FullName File.WriteAllText(outputPath.FullName, answers) incr(counter) let [<Literal>] answersPath = dataPath + "answers.json" type Answers = JsonProvider<answersPath> let answers = [ for file in dataDir.EnumerateFiles("*.answers") do yield! Answers.Load(file.FullName).Items ] |
Next up we pair the questions with the accepted answers.
1: 2: 3: 4: 5: 6: 7: |
let mergeQuestionAnswers = [ for answer in answers do match questionAnswerMap.TryFind answer.AnswerId with | Some question -> yield question, answer | None -> () ] |
And we are now at a point where we can compute some statistics around the questions.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
let getTimeToClose (question : Questions.Item, answer : Answers.Item) = (unixToDateTime answer.CreationDate).Subtract(unixToDateTime question.CreationDate).TotalHours let statsByYear = mergeQuestionAnswers |> Seq.groupBy (fun (q,a) -> (unixToDateTime q.CreationDate).Year) |> Seq.map (fun (year, data) -> let timeToClose = data |> Seq.map getTimeToClose let average = timeToClose |> Seq.average let median = timeToClose |> median year, average, median ) |> Seq.sortBy (fun (y, _, _) -> y) |> Seq.toArray |
This gives the following results.
|
Which we can then plot using
1: 2: 3: 4: |
(Chart.Combine [ Chart.Line(statsByYear |> Seq.map (fun (y, a, _) -> y, a), Name = "Average") Chart.Line(statsByYear |> Seq.map (fun (y, _, m) -> y, m), Name = "Median") ]).WithLegend(true) |
And actually we see that in 2008 when FSharp first appeared it took a long time for questions to get closed. This is the year F# was introduced and I suspect there was only a handful of people outside of Microsoft Research that actually where able to answer these questions. However as time has progressed we see an exponential improvement it the time for questions to get answered, which typically bottoms out with an average of 25 hours and a median of about 30 mins. This is clearly a sign of a responsive community, that is indeed growing. Whats more, I still don't think that an average of 25 hours is actually representative. In my experience I rarely use Stack Overflow for F# questions, instead I direct my questions to the fsharp github repository previously on codeplex, the repository of the project I am using, or finally twitter with the #fsharp tag, and wait for the plethora of responses to come in from the help and very active community members. And in these domains the response time is typically around ~5 minutes.
In fact as I write this I'm wondering whether the comment
few irritatingly vocal people in the F# community implicitly try to guilt non-believers into F# adoption.
has been spurred by the willingness to help in the community. Yes there is a certain amount of advertisment that goes on for features specific to F#, but in general it is just sound fundamental programming advice. I'm fairly sure every single one of those people would offer examples in C#, Haskell or VB if asked. Anyway I digress.
The second comment that stood out for me in the post was,
- F# has no compelling technical advantage. Some overly zealous F# fans claim that F# can do things that general purpose languages like Perl and C# can't. This all depends on how you define features of languages. But more reasonable F# proponents usually state, correctly, that F# isn't intended to replace languages like C# and that F# doesn't have any unique, magic capabilities. So, there's no technical reason for me to use F#, and the cost of context switching between the primarily procedural C# and the primarily functional F# is a huge price to pay.
I think you only have to look at type providers, which are used to analyse the Stack Overflow questions above are certainly a nice and almost unique feature. That to my knowledge only one other language has Idris. Sure you can do the analysis I have done above in C# but there will be alot more typing and additionally a lot less safety, since you will ultimately loose the strongly typed data access, that type providers offer. Moreover it is this safety that F# and statically typed functional programming languages in general offers you and makes it worth the context switch.
Since I adopted F# and functional programming it has completely changed the way I think about coding problems in all of the other languages I use, most notable C#. It has made me a better developer.