This post has nothing whatsoever to do with Autodesk software: I just thought some of you might be interested in an old project I worked on during my University studies. I've already mentioned the project briefly in a couple of previous posts.
So, after dusting off the 3.5 floppies I found in the attic, and working out how to extract the code from the gzipped tarballs they contained (thankfully WinZIP took care of that), I started the work to port the code from Miranda to F#. Miranda is still available for many OS platforms, although it has apparently largely been succeeded by the open, committee-defined (originally, at least) functional language, Haskell. But the main point of this exercise was not as much to get the code working as it was for me to become familiar with the F# syntax, and what adjustments might be needed to my thinking for me to code with it.
Before I summarise the lessons learned from the porting exercise, a few words on the original project: I worked on this during 1994-5, with my project partner, Barry Kiernan, supervised by Dr. Steve Hill, from the University of Kent at Canterbury (UKC) Computing Laboratory. I've unfortunately lost contact with both Barry and Steve, so if either of you are reading this, please get in touch!
We adopted Miranda, as this was the functional programming language being taught at UKC at the time. I'm fairly sure that the original code would work with very little modification in Haskell, though, as Miranda is a simpler language and the two appear to have a similar syntax.
The project was to model the behaviour of a Motorola 6800 processor: a simple yet popular, 8-bit processor from the 1970s. The intent behind the project was to validate the use of purely functional programming languages when modelling hardware systems such as micro-processors. What was very interesting was our ability to adjust the level of abstraction: our first implementation used integers to hold op-codes, memory values, register contents, etc., but we later refined it to deal with individual bits of data, moving them around using buses. We also implemented an assembler using Miranda, which was both fun and helpful for testing. That's another strength of functional programming, generally: it is well-suited to language-oriented programming.
I have to admit many specifics of the project are now somewhat vague to me, but I was still able to migrate the code with relatively little effort: despite the fact we're talking about nearly 2,800 lines of source (including comments), it took me several hours, rather than days. I should also point out that I'm certain I haven't used F#'s capabilities optimally - I still consider myself to be a learner when it comes to F# - but I expect I'll come back to the code a tweak it, once in a while.
Here are some notes regarding the migration process:
- F#'s type inference was great: rather than having to define algebraic types for the various functions, these were inferred 100% correctly. The few times I added type information to force the system to understand what I'd done, it turned out to be a logic error I needed to fix.
- F# Interactive was very helpful, although when I first started out with the migration I didn't really use it (I've only since realised how useful a feature it is). I've now come to love the ease with which you can load and test F# code fragments within Visual Studio using F# Interactive.
- For now I've created one monolithic source file. In time I'll probably split this into separate files, but for now this was the simplest way to proceed.
The only big change needed to the code was to remove the use of multiple signatures to define a function's behaviour. With Miranda and Haskell it's standard practice to pattern match at the function signature level. For instance, here's the implementation of a function that performs a "two's complement negate" operation on a list of binary digits:
neg1 :: [num] -> (bool, [num])
neg1 [] = (False, [])
neg1 (1:t)
= (True, (0:comt)) , if inv
= (True, (1:comt)) , otherwise
where
(inv, comt) = neg1 t
neg1 (0:t)
= (True, (1:comt)) , if inv
= (False, (0:comt)) , otherwise
where
(inv, comt) = neg1 t
In F# the pattern matching is performed within the function:
let rec neg1 lst =
match lst with
| [] -> (false, [])
| 1 :: t ->
let (inv, comt) = neg1 t
if inv then
(true, 0::comt)
else
(true, 1::comt)
| 0 :: t ->
let (inv, comt) = neg1 t
if inv then
(true, 1::comt)
else
(false, 0::comt)
| _ -> failwith "neg1 problem!"
These changes were not especially hard to implement, but it did take some time for me to get used to the difference in approach. Note also the final wildcard match ('_') needed to prevent F# from warning me of an incomplete pattern match: this is presumably because the type included in the list was not officially constrained to be binary (0 or 1).
Alright - thanks for bearing with me... here's the F# source file, in case you're still interested. The simplest way to see it in action is to open the file inside Visual Studio (with F# installed, of course), select its entire contents and hit Alt-Enter. This will load it into F# Interactive, at which point you should see some automated test results displayed and be able to run the test assembly language program by typing the following line into the F# Interactive window:
run mult;;