Adventures in F#--Sweet Test-First Kung Fu
Jomo Fisher--Up until now, I've been avoiding using F# with the VS IDE. I've been using notepad.exe and fsc.exe because I wanted to build my own expectation for what the experience should be before I experienced what it actually was. I can tell you that I didn't expect the sweet experience of using the F# interactive window.
I've written in the past about Test Driven Development. Regardless of language, I really like the process of iteratively writing code and unittests tests for that code. For very small projects--simple algorithms and classes--I've found the process of getting going very tedious compared to the amount of actual code I want to write. In order to get started, you need a new project and solution for your code, a separate project for your tests and you need to reference your code project from your test project. All this means friction and tends to discourage firing up VS and freely inventing for very small projects.
Enter the F# Interactive window. Once you've installed F# and checked the Add-in box under Tools\Add-in Manager you'll see the F# interactive window. You can type code into this window and it will execute immediately. Importantly, you can select some text from your current source file and press alt-enter to execute it.
So here's my new process for these tiny one-off projects:
1) Ctrl-N to create a new text file (rename to mycode.fs)
2) Type in the skeleton of the function
3) Select function and press alt-enter
4) Write test, select it and press alt-enter (see failure)
5) Fix function to make test pass and goto 3
Notice that there's not a project or even a solution involved here--its just developer writing code. It's a really sweet experience. I only wish F# would install over C# Express 2008 since that's what I tend to use at home.
I like to show code in my posts when possible and I want to give you a visual idea of what I'm talking about. Here's a simple function I wrote--with tests--that takes a string in the form of A.B.C.D or vA.B.C.D and returns a tuple of 16-bit ints with values (A,B,C,D). The tests are at the end.
#light
let ParseVersion s =
// Build a list with each element of the version in reverse order.
let rec parse(s:string,pos,count,result) =
let versionSize = 4
if pos >= 0 then
if s.[pos] = 'v' && pos = 0 then parse(s,1,0,[])
else
let dotPos = s.IndexOf(".", pos)
if dotPos = -1 then parse(s, -1, count+1, int_of_string(s.Substring(pos))::result)
else parse(s, dotPos+1, count+1, int_of_string(s.Substring(pos, dotPos-pos))::result)
elif count < versionSize then parse(s, pos, count+1, 0::result)
else result
let l = parse(s,0,0,[])
(uint16(List.nth l 3), uint16(List.nth l 2), uint16(List.nth l 1), uint16(List.nth l 0))
ParseVersion "2.0.1"
ParseVersion "2.0"
ParseVersion "v2.0"
ParseVersion "v1.2.3.4"
ParseVersion "v1"
ParseVersion "1.2.3.4"
(Notice how I made it tail-recursive to take advantage of F#'s tail-call optimization?)
I wrote this using more-or-less test-first principles and I was very happy with the flow of the process.
This posting is provided "AS IS" with no warranties, and confers no rights.
Comments
Anonymous
October 02, 2007
PingBack from http://www.artofbam.com/wordpress/?p=4694Anonymous
October 02, 2007
The comment has been removedAnonymous
December 07, 2007
The comment has been removedAnonymous
June 16, 2010
Hi Jomo! I enjoy your blog a lot, being a C# 3.0 and LINQ fan. I just started having fun with F#, and your blog entries about its insides shed some light on it for me. However I find this snippet of your code misusing (or probably overusing) some functional concepts such as tail recursion or list pattern matching. I decided to rewrite it in a more eye-friendly way because this kind of thought purity is why I love F# for. I'm not sure if it is exactly as correct as yours but it looks sexier to me: open System let parseVersion (s:string) = s.Split [|'v'; '.'|] |> List.ofArray |> List.filter ((<>) "") |> List.map (Int32.Parse) let versions = ["1.0.0.1"; "v0.9"; "1.2.9"] let versions = strings |> List.map parseVersion List.iter2 (printfn "String '%s' has been parsed as %A") strings versions