Tackling HackerRank with F#
- Authors
- Name
- Darren Hickling
Learning F# was a personal goal for last year. As much as I did learn, I was determined not to let it slip again. I had the determination, but not a plan, although as you'll see, I ultimately stumbled across a method of learning which I wholeheartedly recommend.
Why F#?
First and foremost, I wanted to better understand functional programming. A while ago, I decided to review my programming skills, and realised that this was one area of which I had little knowledge. Sure, I've used aggregation and projection in several languages, and tried to reduce state where possible, but surely there was more to it than that?
Initially, I didn't have a language in mind, but stumbled across the excellent blog by ploeh, and after testing F# interactively, decided to attempt to learn it. The immediate draws were:
- Enforcing business logic as code: using lightweight types, discriminate unions, pattern matching and other approaches.
- Concise, clear style: using structural and duck typing, whitespace, and features like the forward pipe operator.
- Native .NET libraries access: this was also a factor in choosing IronPython for my dissertation.
- Multi-paradigm design: switching between functional, imperative and object-oriented approaches when required.
- Great language features: such as immutable types, parallelisation, partial application and units of measure.
First Step: Read, Reread, Read Some More
It became fairly obvious from the interactive F# demo that there were plenty of new concepts for me to learn. One of the first sites I found — for which I am so thankful and continue to browse today — is F# for fun and profit, which has a fantastic set of series, such as functional design and porting from C#. I started racing through the site, enthusiastically absorbing as information much as possible, excited to soon test as much as possible...
... except that I didn't, because I couldn't! There was too much new material in one go, too much that I had not considered before, and quite a significant change in approach to the OO way I had used since uni. So I went back to the start, and read through the material again. This time, I spent less time trying to understand the nuances of the language, and could quicker absorb the higher-level principles. I could also start to see how concepts such as discriminated unions and pattern matching could be used together, and more importantly, why it would be preferred over imperative and OO approaches.
It was then that I made the decision to start putting the theory into practice, but with one condition: always using the functional approach, even if that meant rewriting code.
The Hacking Begins
Before this experience, and given a new piece of tech, I would have tried writing a small application. However, this approach has usually fallen flat fairly quickly, due to limited scope or unwillingness to continue, due to uncertainty over whether I would be happy to use that tech for an extended period of time.
I knew I needed small, concise challenges that — when accumulated — would test a variety of skills. I had been sent code challenges in the past from prospective employers, and hated every site they used, so thought it was time to investigate similar products that could actually help. To be fair, I discovered HackerRank almost immediately, and had completed several tasks before realising I really enjoyed it.
As you can see, HackerRank groups challenges into sections and subsections, and caters for a huge variety of programming languages. Naturally, it gamifies the process with scores, badges and competitions, and I'm happy to say that I have a functional programming badge!
Learning to Love F#
At first, it was incredibly difficult to see how a functional approach could be applied to every situation.
For at least the first ten challenges, I started with a for
loop or an if
statement, before replacing
them with functional equivalents. However, by sticking to the functional paradigm, I began to develop
a set of small, concise approaches that could be used in place of imperative patterns. Now, I mostly
battle the problem itself, rather than F#.
Let's take a look at a small example: weird numbers.
open System
type State = Weird | NotWeird
let isOdd n = n &&& 1 = 1
[<EntryPoint>]
let main argv =
let result =
match Console.ReadLine() |> int with
| isOdd -> Weird
| x when {2..5} |> Seq.contains x -> NotWeird
| x when {6..20} |> Seq.contains x -> Weird
| _ -> NotWeird
match result with
| Weird -> printfn "Weird"
| NotWeird -> printfn "Not Weird"
0
Even if you aren't familiar with F# or functional programming, I would like to think that the code above is straightforward to understand, albeit given a little more time than usual to read! As you will hopefully agree, the logic is relatively uncluttered compared to an OO language, but you may not have immediately appreciated:
- The type of the
n
argument of theisOdd
function has been inferred from its usage. - Thanks to type safety, only a
State
value can be returned from the firstmatch
. - All match scenarios must be met, hence the last
_
match; a little like adefault
catch all in aswitch
statement. - The
Console.ReadLine
function is called from the importedSystem
.NET library, as one would from C#.
[Oh, huge thanks to the Fira Code font] for the beautiful and incredibly useful monospace ligatures, which bring the forward pipe and lambda arrow operators to life!]
Thinking back to the time I first saw F# code examples, the one above might take a while to sink in. That's fine! But believe me, if you have no functional programming experience, and this interests you even a little, I thoroughly recommend that you make studying it a priority. Trust me: when you start to reach a point where you are comfortable programming in F#, you'll want to use it everywhere, and constantly miss the design features that make it so useful.
Finally: Some Advice
F#
Weirdly, the differences between the main types of collection only really became apparent after using and abusing them! I can't remember seeing it explained this quickly, so in a nutshell: lists are preferred in F#, and sequences are lazily-evaluated.
let list = [1; 2]
let array = [|1; 2|]
let sequence = {1..1000}
// Convert a string input to an array, then list, of integers, to allow destructuring.
match Console.ReadLine().Split(' ') |> Array.map(int) |> Array.toList with
| number::max::tail -> number * max
| _ -> 0
F# is a multi-paradigm language, and borrows from the best, as evolution absolutely should. With that in mind, use its multitude of features to create readable code, and avoid duplication.
// Forward pipe to not to inverse a boolean.
let stringHasValue s = s |> String.IsNullOrWhiteSpace |> not
// Partially-apply a format argument to the print function.
let printString s = printfn "%s" s
// Explicitly ignore a value, rather than implicitly with a compiler warning :)
Console.ReadLine() |> ignore
// Instead of a while loop, try this pattern.
Seq.initInfinite (fun i -> Console.ReadLine())
|> Seq.takeWhile stringHasValue
|> Seq.iter printString
Finally, beware of unnecessary projection. Often, I would take an input and it via Seq.map
, then suddenly run out of memory
because one of the HackerRank test cases provided a data set far larger than I had anticipated! My first instinct was to
parallelise with functions under the Array.Parallel
namespace, but to no avail. However, I didn't really need any of the projections,
so instead aggregated where possible with Seq.fold
and Seq.reduce
.
Code Challenges
This experience has given me the confidence, and enough patterns and knowledge, to tackle larger problems in F#. I do attribute much of this to the challenges from HackerRank, and absolutely recommend the learning and hacking approach. However, please do read as much you can before starting, from a variety of sources. For F#, in addition to ploeh and F# for fun and profit mentioned previously, the following really helped me:
- Examining the F# Programming Language: quick and to the point.
- A C# Developer's Guide to F#: arrived late on in my initial studying, but a brilliant, in-depth guide from a C# perspective.
- Microsoft Docs: RTFM! Microsoft have improved their documentation of late, and there is always a surprising amount of detail both newcomers and experts can learn from (re)visiting documentation. In this case, try starting from the symbol and operator reference, and follow the links.
As for the source of the challenges, or code kata, there are plenty of products in addition to HackerRank, and would love to hear your thoughts on these and others:
Persevere
As ever, keep going! Particularly when faced with something completely out of your comfort zone: don't give up. There were plenty of times that I swore at a problem, walked away, then returned with fresh eyes to conquer it with great satisfaction. Don't forget to source control your work and review it periodically, to see what you can improve with your new skills. Good luck, and I hope you find it as useful as I did.