Experiments with F#

A couple of customers have asked for a command-line tool to run Goanna over their Visual Studio projects, similar to the way the Linux command-line tool works. The difficult bit for such a tool is to translate the information in a project file to the appropriate arguments to the core Goanna executable, goannacc.exe on Windows. We already have code to do just that in the Visual Studio extension.

The Goanna VS extensions for VS2005/2008/2010 are written in C#, because there’s a wizard that generates a simple extension in C#. From that starting point, we (meaning I) built the current extensions. If there hadn’t been the wizard, I would have written the extensions in F#, because I prefer the functional style of programming. So I decided to write the command-line tool, that was an opportunity to try out F# in earnest. I’d written a wee bit of F# before; here was a chance to try it out on production code.

When writing the command-line tool, my main concern was, how easy would it be to pull in the C# code that does the project-to-command-line translation. It was very easy: just add an F# project reference to the .DLL containing the code, open the namespace, and I was good to go. I had to make some C# classes explicitly public for visibility, but that was the only change I needed to make.

Programming in F# is very much like programming in OCaml, a language I’ve used off and on for maybe 15 years. Nice thing: instead of the clumsy “delegate” syntax of C#, you can just pass a function argument to another function. Not so nice: in VS2010, the editor does not seem to auto-format F# code, the way it does with C# code (the Ctrl-E F magic). And the editor’s Intellisense feature does not appear to suggest variable names that are in scope. Also: although there are surely good reasons for it, the F# list type is distinct from the C#/.Net System.Collections list type, which is slightly maddening. Finally: I have to build the tool for the various VS versions in slightly different ways, and the conditional compilation facility works for that — but why are there no boolean operations allowed, as you have in C#?

Here’s an example of code that uses .Net lists instead of F# lists:

  let expandedProjs = new System.Collections.Generic.List() in
  while projsIter.MoveNext() do
    expandedProjs.AddRange(ProjectUtil.expandProject(projsIter.Current :?> EnvDTE.Project))
  done;

Ooof.

When the command-line tool starts, it fires up an instance of Visual Studio, no GUI. That way it can get information about solutions and projects from VS, like default include paths and configuration information, using code originally written for the extensions. Sometimes the calls to VS fail with COM retry errors, so those calls are done in a loop containing a try-with block. When the tool finishes, or the user hits Ctrl-C, it gracefully shuts down VS. I often run the tool from a Cygwin shell, and I haven’t yet found a way to trap Cygwin SIGKILL signals, so that VS is still running afterwards.

There’s still some work to do on the command-line tool, like deciding what kind of output it should produce, but it’s basically there. Let me know if you’d like to try it out before we make it generally available. The tool is tentatively called “GoRun”, and its syntax is:

  GoRun sln-file [projName ...] ...

That is, you supply one or more solution files, and for each solution file, zero or more project names. If you don’t supply project names, GoRun invokes Goanna on all the projects in the solution, otherwise only those specified.

A complaint: Soon after VS2010 was released, the MSDN site was updated with all-new documentation for the .Net libraries. But the only language there’s documentation for is C# (OK, sometimes J#) . In the type signatures, there are no hyperlinks for keywords (like public, final, etc.) and types. You can’t tell when a type is really a forall-quantified type variable. It wouldn’t be much harder to do these pages right.

One last comment: programming in F# doesn’t feel all that different than programming in C#, though the code is more concise. You definitely feel the presence of .Net every step of the way, and there’s statefulness lurking everywhere. Functional programming for the masses … sort of!

5 Comments
  • Passer By

    August 11, 2010 at 3:18 am

    Presumably you can wrap:

    let expandedProjs = new System.Collections.Generic.List() in
    while projsIter.MoveNext() do
    expandedProjs.AddRange(ProjectUtil.expandProject(projsIter.Current :?> EnvDTE.Project))
    done;

    in something prettier? Or, given that projsIter came from some x that is Enumerable, you could probably use Seq.iter on x. Then again, why bother making expandedProjs a .NET list in the first place, appending by side effect? Just Seq.collect on x?

    Maybe this doesn’t fit in the code well though, just a thought. Also, I rarely use “regular” .NET classes when writing F# code, so what I’ve written above might not work out of the box.

  • Paul

    August 11, 2010 at 1:06 pm

    Seq.iter works on F# collections, not .Net collections; it comes from Microsoft.FSharp.Collections.Seq. The projsIter comes from an instance of EnvDTE.Projects, which implements IEnumerable. Like I said … maddening.

    Certainly, if I were doing this sort of thing more than once, I’d create a function to confine the ugliness to one location.

  • Same Passer By

    August 14, 2010 at 2:39 am

    But “seq” is just the F# name for C# IEnumerable, right? For instance, the following works:

    Seq.iter ignore (new System.Collections.Generic.List())

    Since you apparently have only an IEnumerable, you will have to use something like:

    Seq.iter ignore (System.Linq.Enumerable.Cast(enumerable))

    So not perfect, but that should still be better than manually using the enumerator!

  • Same Passer By

    August 14, 2010 at 2:40 am

    And it ate my left and right angle brackets. You need to specify the type for cast:

    Seq.iter ignore (System.Linq.Enumerable.Cast<YourType>(enumerable))

  • Paul

    August 16, 2010 at 1:19 pm

    Yes, that approach works. Having to cast the list from the C# to the F# world is pretty clumsy, though. The resulting code is about the same length.

Post a Comment