Part I
Part II
Part III
Part IV
Part V
Part VI
In Part I we ended up with a small module containing functions that allowed a user to select polylines and print the sum of the lengths of those polylines to the command line. In the workhorse function, GetTotalPolylineLength, we took a good look at a tail recursive loop and how to use an accumulator in a recursive loop. Then we marveled at how good the F# compiler was at inferring type data.
In this second part we’re going to clean up the code a bit, introduce another F# data structure, the tuple, and see how all that magical and marvelous type inference is more than just hype and how it can save time when developing and modifying code.
The first thing is to clean up are those mutable identifiers, activeDoc and currentEditor. I replaced them with a pair of functions and replaced all calls to activeDoc and currentEditor with GetActiveDoc() and GetCurrentEditor(). This is a much safer practice and in the future I promise to try to stay away from mutable variables as much as possible. Another benefit is that these library type functions can be placed in a module and reused. For the purpose of this blog post however, I’m going to leave them where they are.
1: //Declare some shared variables for the functions in the module
2: //let mutable activeDoc = Application.DocumentManager.MdiActiveDocument
3: //let mutable currentEditor = activeDoc.Editor
4:
5: let GetActiveDoc () =
6: Application.DocumentManager.MdiActiveDocument
7:
8: let GetCurrentEditor () =
9: GetActiveDoc().Editor
The next thing is to modify the GetTotalPolylineLength function to return the sum of the areas as well as the lengths. I avoided this at first because I wasn’t sure how to return both pieces of data from one function. I found one very easy way to do it with a tuple. A tuple is simply an ordered list of values. But unlike F# lists, the values in a tuple do not have to be the same type.
Here is an update to the GetTotalPolylineLength function. I renamed it to better reflect its purpose. I then modified the loop to accept and return a tuple with two values, totalLength and totalArea. You declare a tuple by simply placing identifiers, or actual values, in parentheses with each value separated by a comma. This is where things get interesting.
The tuple itself is pretty straight forward. The cool thing is that I didn’t have to change any of the other functions, even though this function changed from returning an integer to returning a tuple. This is because F# allowed us to create every function and every identifier as generic, aside from our single cast to the Polyline type on line 9 in the listing below, and the compiler was able to infer all other type information. I don’t anticipate changing function return values always having zero impact on surrounding code, but I do anticipate this being a real time saver when creating a group of interrelated functions where you don’t know up front the value types that they’ll be passing around; or when you’re going back to modify existing code to add functionality. We’ll put this ability to further use as we finalize this module.
1: let SumLengthsAndAreas inputList =
2: use trans = GetActiveDoc().TransactionManager.StartTransaction()
3:
4: //Recursive loop to accumulate total length
5: let rec loop (totalLength, totalArea) inputList =
6: match inputList with
7: | [] -> (totalLength, totalArea)
8: | hd::tl ->
9: let polyline = trans.GetObject(hd, OpenMode.ForRead)> Polyline
10: loop (totalLength + polyline.Length, totalArea + polyline.Area) tl
11:
12: //Start the loop
13: loop (0.0, 0.0) inputList
OK, now we’re on a role and our function is returning area as well as length. I was going to save the next bit of functionality for a future post, but decided to implemented it now, considering how easy adding the last bit of functionality turned out to be. One of the requirements for this app is to give the total length of all selected polylines, but only calculate area for closed polylines. It also needs to show the user the open polylines so they can determine if it’s an error or if they’re open by design.
To accomplish this we check the polyline.Closed property and if it’s closed, total its length and area. If it’s not closed, however, just total the length, not the area, and store the ObjectId in a list, openPolys, that we added to the end of the tuple.
1: //Recursive loop
2: let rec Loop (totalLength, totalArea, openPolys) inputList =
3: match inputList with
4: | [] -> (totalLength, totalArea, openPolys)
5: | hd::tl ->
6: let polyline = trans.GetObject(hd, OpenMode.ForRead)> Polyline
7:
8: //If closed, then get its area, else add it to the openPolys list
9: match polyline.Closed with
10: | true -> (totalLength + polyline.Length, totalArea + polyline.Area, openPolys)
11: | _ -> (totalLength + polyline.Length, totalArea, hd::openPolys)
12:
13: |> (fun polyInfo -> Loop polyInfo tl)
14:
15: //Start the loop
16: Loop (0.0, 0.0, []) inputList
On the first pass I coded this with an if..then expression. I then started thinking about creating further conditionals to filter the polylines and perform different actions based on those filters. For instance, maybe we only want to count length for polys, open or closed, on layer A-Trim, without adding the open ones to the open poly list, count area and length for closed polys on layer A-Areas, and count length only for open polys on layer A-Areas. Whew, that’s difficult to follow and it would result in a mess of confusing boolean comparisons, or even worse, nested if’s, if it was coded using imperative techniques. Pattern matching, however, expresses it clearly and with very little code.
1: match polyline.Closed, polyline.Layer with
2: | _, "A-Trim" -> (totalLength + polyline.Length, totalArea, openPolys)
3: | true, "A-Areas" -> (totalLength + polyline.Length, totalArea + polyline.Area, openPolys)
4: | false, "A-Areas" -> (totalLength + polyline.Length, totalArea, hd::openPolys)
5: | _ -> (totalLength, totalArea, openPolys)
Of course that was all hypothetical and our function doesn’t need all that functionality, but who knows, it might one day, and if it does, it will be very easy to add.
There were two things that I learned in this round of coding, keep your code generic to ensure it’s flexible and that it’s easier to change and maintain, and that pattern matching rocks! In Part III, we will finalize the code and create a GUI, specifically a UserControl that we’ll host on a palette in AutoCAD.