Playing With F#
So I decided to use F# (basically OCaml) instead of scheme for the pure functional version of my puzzle solver. I wish I had started with F# in the first place. What a gorgeous language! Implementing in F# showed me what I was doing wrong in the C# version, and now I can implement the C# version without the yield keyword.
Here is the complete rotate routine in F#.
let cards = [ "K"; "Q"; "J"] let sides = [0; 1] let rec rotate outer inner result = match outer with | o::tail -> if tail = [] then [for i in inner -> (o, i)::result ] else [for i in inner for l in rotate tail inner ((o,i)::result) -> l] | [] -> [] do print_any (rotate cards sides [])
I was really surprised with how productive F# is. I thought I would get annoyed at its strong typing versus scheme, but it made dealing with nested lists a lot easier. And with the VS2008 integration, the editor shows you inferred types almost real-time, simply by hovering. Once I figured that out, I no longer needed fsi interpreter window open.
Amazingly, F# has yield functionality as well, but I stayed far away from it. Even funnier, F# has a “mutable” keyword, since nothing is mutable by default. There is nothing better than programming when everything is immutable.
Now, the whole reason I wanted to compare C# 3.0 functional versus scheme was to get a sense for how well the compiler can optimize the recursions. I think F# works as well as scheme for this purpose. Check out the following code sample:
let rec range a b = if a > b then [] else a :: range (a + 1) b let rec sum list = match list with | h::tail -> (sum tail) + h | [] -> 0 do print_any (sum (range 0 1000))
Even if you’re not familiar with Caml/F#, you can tell what this does. The range function returns a list of numbers inclusive from a to b, and the sum function adds a list of numbers together. Note that both functions are implemented recursively! So when we add up the numbers 1 to 1000, both functions trigger recursion 1000 levels deep.
Code like this would crash in C++ or C# because you can’t recurse that deep — especially if you’re passing everything immutably by value. But the sum returns instantly in F#, because the compiler doesn’t actually push the call stack 1000 times — it doesn’t implement things the way an imperative language would.
I was speculating that C# 3.0 might put C# recursion performance closer to par with scheme and F#, by heavy use of iterators and compiler advancements. So I found a great description of how iterators work over on Wes’s blog. He includes some perf charts and hints at a potential new “yield foreach” syntax that would have helped me out. It looks like iterators perform well.
C# version 3.0 is a huge advance, but still not as nice as F# for my preferred style. While the iterators perform and chain amazingly well, I can still get into trouble by recursing too deep. And C# (like Python) discourages you from passing around lots of big lists by value. List processing in C# has gotten a lot better, but still not as nice as F#.
December 30th, 2007 at 12:18 pm
Are you sure that your sum function will not blow up the stack? It’s not tail recursive:
let rec sum list =
match list with
| h::tail -> (sum tail) + h (* 0
something like
List.fold_left (fun acc e -> acc+e) 0 list
or
let rec sum =
let loop acc = function
x::xs -> loop (x+acc) xs
| [] -> acc in
loop 0
would be.
December 30th, 2007 at 12:25 pm
whoops, the last function should read
let sum =
let rec loop acc …
December 30th, 2007 at 8:39 pm
Janne: The range function isn’t tail recursive, either, but I thought it didn’t matter anymore. I actually tested before posting, and F# returns 10,000 deep almost instantly — so I assumed the compiler was doing some magic to implement tail recursion under the covers (it’s smart enough to infer all my types, so I thought “why not”), and 10,000 deep is an ungodly level of recursion.
Anyway, I just tested with bigger numbers and was able to blow the F# stack at 100,000 deep on my machine. So you’re right.
I also just tested the same routine in C#, and found that it actually does 10,000 deep on my machine (sum only, not range) as well. I am amazed at this; I would expect it to blow up after a few hundred. C# is a lot slower than F# at this level of recursion, though; and I can blow up C# at just 15,000 — where F# functions fine and near-instant at 15,000. It seems that F# perf doesn’t degrade as quickly as C# before blowing up.
For completeness sake, the range function is better implemented as:
let rec range2 a b = [for i in a to b -> i]