A simple taxonomy of programming languages

Someone asked me recently how I categorize different programming paradigms. I thought it was a very interesting question, so here's what I responded. Please bear in mind that this is very much the way I see things, and is neither an exhaustive nor a formally-ratified taxonomy.

One way to look at languages is whether they're declarative or imperative:

Declarative programming languages map the way things are by building up “truths”: this category includes functional programming languages (such as Miranda, Haskell and Erlang) which tend to be mathematical in nature (you define equations) and start with lambda calculus as a foundation. The other main set of declarative languages are logic programming languages (such as Prolog), which start with propositional calculus as a foundation (you declare axioms that build up to a system against which you can run queries). Declarative languages tend to focus on describing the problem to solve, rather than how to solve it.

Imperative programming languages, on the other hand, are lists of instructions of what to do: I tend to consider procedural programming languages (such as C, COBOL, Fortran and Pascal) as a sub-category which focus on the definition and execution of sub-routines, while some people treat the terms imperative and procedural as synonyms.

Considering these definitions, object-oriented programming languages (such as Smalltalk and Eiffel) should probably be considered declarative, as conceptually they map real-world objects, but the truth is that the most popular OO languages (such as C++) are impure, and so most OO systems combine big chunks of procedural (i.e. imperative) code. Many people who think they’re doing OOP are actually packaging up procedures.

Note that I've tried not to list multi-paradigm languages such as Ada, C++ and F# in the above categorisation. It's possible that some of the languages I've listed are also multi-paradigm, but anyway.

One other way to think about languages is whether they’re top-down or bottom-up:

Bottom-up languages are ultimately layered on how a processor works (from machine code to assembly language to C & C++), while top-down languages start from the world of mathematics and logic and add language features that allow them to be used for programming (i.e. declarative languages are therefore top-down). This latter set of languages are starting to see increased adoption, as they assume much less (even nothing) about the underlying machinery, in which big changes are occurring with multiple processing cores being introduced (which essentially invalidate the assumptions of previous generations of programmers, who have been conditioned to think in terms of the processor's ability to store and access state).

Many popular - or soon to be popular - programming environments are pragmatic in nature: C++ allows OOP but can also be used for procedural programming, VB.NET now allows you to define and access objects while coming from a long line of procedural languages, F# is multi-paradigm, combining OO with functional and imperative programming.

There are bound to be people with differing views on this subject (and many of them are no doubt more intelligent and experienced in these matters than I), but this is how I would answer the question of how to categorise programming languages.

For those of you with an interest in the future of programming languages, I can strongly recommend the following Channel 9 episodes. If you're not aware of Channel 9, then prepare to be impressed: Microsoft has given a fantastic gift to the development community with this resource.

Burton Smith: On General Purpose Super Computing and the History and Future of Parallelism
Erik Meijer: Functional Programming
Anders Hejlsberg, Herb Sutter, Erik Meijer, Brian Beckman: Software Composability and the Future of Languages
Brian Beckman: Don't fear the Monads
Joe Armstrong - On Erlang, OO, Concurrency, Shared State and the Future, Part 1
Joe Armstrong - On Erlang, OO, Concurrency, Shared State and the Future, Part 2

Enjoy! :-)

March 17, 2008 in Concurrent programming, F# | Permalink | Comments (2) | TrackBack

Parallelizing robotic AutoCAD hatching with F# and .NET

In the last post we saw some code combining F# with C# to create a random "hatch" - a polyline path that bounces around within a boundary.

This post extends the code to make use of Asynchronous Workflows in F# to parallelize the testing of points along a segment. In the initial design of the application I decided to test 10 points along each segment, to see whether it remained entirely within our boundary: the idea being that this granularity makes it very likely the segment will fail the test, should it happen to leave the boundary at any point. Not 100% guaranteed, but a high probability event. What this code does is take the 10 tests and queue them up for concurrent processing (where the system is capable of it).

Asynchronous Workflows - as suggested by the name - were intended to fire off and manage asynchronous tasks (ones that access network resources, for instance). The segment testing activity is actually very local and processor-bound, so it's not really what the mechanism was intended for, but I thought it would be interesting to try. One interesting point: while testing this code I noticed that it actually ran slower on a single processor machine, which is actually quite logical: only one core is available for processing, so the amount of sequential processing is not reduced but the overhead of synchronizing the various tasks is added. So it was fairly inevitable it would take longer. In the post I first talked about Asynchronous Workflows I showed a sample that queried multiple RSS sites for data: even on a single processor machine this was significantly quicker, as parallelizing the network latency led to a clear gain.

Anyway, as it was slower on this machine I decided only to enable the parallel version of the code in cases where the computer's NUMBER_OF_PROCESSORS environment variable is greater than 1. I checked quickly on a colleague's dual-core machine, and sure enough this variable is set to 2 on his system. I haven't, however, tested the code on a dual- or multi-core system, but I'll be getting a new system in a matter of weeks, which will give me the chance to test it out.

Here's the F# code, with the line numbers highlighting the changes in red:

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module BounceHatchAsync.Commands

    8

    9 // Import managed assemblies

   10

   11 #I @"C:\Program Files\Autodesk\AutoCAD 2008"

   12 #I @".\PointInCurve\bin\Debug"

   13

   14 #r "acdbmgd.dll"

   15 #r "acmgd.dll"

   16

   17 #R "PointInCurve.dll" // R = CopyFile is true

   18

   19 open System

   20 open Autodesk.AutoCAD.Runtime

   21 open Autodesk.AutoCAD.ApplicationServices

   22 open Autodesk.AutoCAD.DatabaseServices

   23 open Autodesk.AutoCAD.EditorInput

   24 open Autodesk.AutoCAD.Geometry

   25 open PointInCurve

   26

   27 // Get a random vector on a plane

   28

   29 let randomUnitVector pl =

   30

   31   // Create our random number generator

   32   let ran = new System.Random()

   33

   34   // First we get the absolute value

   35   // of our x and y coordinates

   36

   37   let absx = ran.NextDouble()

   38   let absy = ran.NextDouble()

   39

   40   // Then we negate them, half of the time

   41

   42   let x = if (ran.NextDouble() < 0.5) then -absx else absx

   43   let y = if (ran.NextDouble() < 0.5) then -absy else absy

   44

   45   // Create a 2D vector and return it as

   46   //  3D on our plane

   47

   48   let v2 = new Vector2d(x, y)

   49   new Vector3d(pl, v2)

   50

   51 type traceType =

   52   | Accepted

   53   | Rejected

   54   | Superseded

   55

   56 // Draw one of three types of trace vector

   57

   58 let traceSegment (start:Point3d) (endpt:Point3d) trace =

   59

   60   let ed =

   61     Application.DocumentManager.MdiActiveDocument.Editor

   62

   63   let vecCol =

   64     match trace with

   65     | Accepted -> 3

   66     | Rejected -> 1

   67     | Superseded -> 2

   68   let trans =

   69     ed.CurrentUserCoordinateSystem.Inverse()

   70   ed.DrawVector

   71     (start.TransformBy(trans),

   72     endpt.TransformBy(trans),

   73     vecCol,

   74     false)

   75

   76 // Test a segment to make sure it is within our boundary

   77

   78 let testSegment cur (start:Point3d) (vec:Vector3d) =

   79

   80   // (This is inefficient, but it's not a problem for

   81   //  this application. Some of the redundant overhead

   82   //  of firing rays for each iteration could be factored

   83   //  out, among other enhancements, I expect.)

   84

   85   let pts =

   86     [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]

   87

   88   // Call into our IsInsideCurve library function,

   89   // "and"-ing the results

   90

   91   let inside pt =

   92     PointInCurve.Fns.IsInsideCurve(cur, pt)

   93

   94   List.for_all inside pts

   95

   96 // Test a segment to make sure it is within our boundary

   97

   98 let testSegmentAsync cur (start:Point3d) (vec:Vector3d) =

   99

  100   let pts =

  101     [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]

  102

  103   // Call into our IsInsideCurve library function,

  104   // "and"-ing the results

  105

  106   let inside pt =

  107     async { return PointInCurve.Fns.IsInsideCurve(cur, pt) }

  108

  109   let tests =

  110     Async.Run

  111       (Async.Parallel

  112         [for pt in pts -> inside pt])

  113

  114   Array.for_all (fun x -> x) tests

  115

  116 // For a particular boundary, get the next vertex on the

  117 // curve, found by firing a ray in a random direction

  118

  119 let nextBoundaryPoint (cur:Curve)

  120     (start:Point3d) trace runAsync =

  121

  122   // Get the intersection points until we

  123   // have at least 2 returned

  124   // (will usually happen straightaway)

  125

  126   let rec getIntersect (cur:Curve)

  127     (start:Point3d) vec =

  128

  129     let plane = cur.GetPlane()

  130

  131     // Create and define our ray

  132

  133     let ray = new Ray()

  134     ray.BasePoint <- start

  135     ray.UnitDir <- vec

  136

  137     let pts = new Point3dCollection()

  138     cur.IntersectWith

  139       (ray,

  140       Intersect.OnBothOperands,

  141       pts,

  142       0, 0)

  143

  144     ray.Dispose()

  145

  146     if (pts.Count < 2) then

  147       let vec2 = randomUnitVector plane

  148       getIntersect cur start vec2

  149     else

  150       pts

  151

  152   // For each of the intersection points - which

  153   // are points elsewhere on the boundary - let's

  154   // check to make sure we don't have to leave the

  155   // area to reach them

  156

  157   let plane =

  158     cur.GetPlane()

  159   let pts =

  160     randomUnitVector plane |> getIntersect cur start

  161

  162   // Get the distance between two points

  163

  164   let getDist fst snd =

  165     let (vec:Vector3d) = fst - snd

  166     vec.Length

  167

  168   // Compare two (dist, pt) tuples to allow sorting

  169   // based on the distance parameter

  170

  171   let compDist fst snd =

  172     let (dist1, pt1) = fst

  173     let (dist2, pt2) = snd

  174     if dist1 = dist2 then

  175       0

  176     else if dist1 < dist2 then

  177       -1

  178     else // dist1 > dist2

  179       1

  180

  181   // From the list of points we create a list

  182   // of (dist, pt) pairs, which we then sort

  183

  184   let sorted =

  185     [ for pt in pts -> (getDist start pt, pt) ] |>

  186       List.sort compDist

  187

  188   // A test function to check whether a segment

  189   // is within our boundary. It draws the appropriate

  190   // trace vectors, depending on success

  191

  192   let testItem dist =

  193     let (distval, pt) = dist

  194     let vec = pt - start

  195     if (distval > Tolerance.Global.EqualVector) then

  196       let res =

  197         if runAsync then

  198           testSegmentAsync cur start vec

  199         else

  200           testSegment cur start vec

  201       if res then

  202         if trace then

  203           traceSegment start pt traceType.Accepted

  204         Some(dist)

  205       else

  206         if trace then

  207           traceSegment start pt traceType.Rejected

  208         None

  209     else

  210       None

  211

  212   // Get the first item - which means the shortest

  213   // non-zero segment, as the list is sorted on distance

  214   // - that satisifies our condition of being inside

  215   // the boundary

  216

  217   let ret = List.first testItem sorted

  218   match ret with

  219   | Some(d,p) -> p

  220   | None -> failwith "Could not get point"

  221

  222 // We're using a different command name, so we can compare

  223

  224 [<CommandMethod("fba")>]

  225 let bounceHatch() =

  226   let doc =

  227     Application.DocumentManager.MdiActiveDocument

  228   let db = doc.Database

  229   let ed = doc.Editor

  230

  231   // Get various bits of user input

  232

  233   let getInput =

  234     let peo =

  235       new PromptEntityOptions

  236         ("\nSelect point on closed loop: ")

  237     let per = ed.GetEntity(peo)

  238     if per.Status <> PromptStatus.OK then

  239       None

  240     else

  241       let pio =

  242         new PromptIntegerOptions

  243           ("\nEnter number of segments: ")

  244       pio.DefaultValue <- 500

  245       let pir = ed.GetInteger(pio)

  246       if pir.Status <> PromptStatus.OK then

  247         None

  248       else

  249         let pko =

  250           new PromptKeywordOptions

  251             ("\nDisplay segment trace: ")

  252         pko.Keywords.Add("Yes")

  253         pko.Keywords.Add("No")

  254         pko.Keywords.Default <- "Yes"

  255         let pkr = ed.GetKeywords(pko)

  256         if pkr.Status <> PromptStatus.OK then

  257           None

  258         else

  259           Some

  260             (per.ObjectId,

  261             per.PickedPoint,

  262             pir.Value,

  263             pkr.StringResult.Contains("Yes"))

  264

  265   match getInput with

  266   | None -> ignore()

  267   | Some(oid, picked, numBounces, doTrace) ->

  268

  269     // Capture the start time for performance

  270     // measurement

  271

  272     let starttime = System.DateTime.Now

  273

  274     // We'll run asynchronously only when we have

  275     // multiple processing cores

  276

  277     let runAsync =

  278       (Int32.of_string

  279         (Environment.GetEnvironmentVariable

  280           ("NUMBER_OF_PROCESSORS")))

  281             > 1

  282

  283     use tr =

  284       db.TransactionManager.StartTransaction()

  285

  286     // Check the selected object - make sure it's

  287     // a closed loop (could do some more checks here)

  288

  289     let obj =

  290       tr.GetObject(oid, OpenMode.ForRead)

  291

  292     match obj with

  293     | :? Curve ->

  294       let cur = obj :?> Curve

  295       if cur.Closed then

  296         let latest =

  297           picked.

  298             TransformBy(ed.CurrentUserCoordinateSystem).

  299               OrthoProject(cur.GetPlane())

  300

  301         // Create our polyline path, adding the

  302         // initial vertex

  303

  304         let path = new Polyline()

  305         path.Normal <- cur.GetPlane().Normal

  306

  307         path.AddVertexAt

  308           (0,

  309           latest.Convert2d(cur.GetPlane()),

  310           0.0, 0.0, 0.0)

  311

  312         // A recursive function to get the points

  313         // for our path

  314

  315         let rec definePath start times =

  316           if times <= 0 then

  317             []

  318           else

  319             try

  320               let pt =

  321                 nextBoundaryPoint cur start doTrace runAsync

  322               (pt :: definePath pt (times-1))

  323             with exn ->

  324               if exn.Message = "Could not get point" then

  325                 definePath start times

  326               else

  327                 failwith exn.Message

  328

  329         // Another recursive function to add the vertices

  330         // to the path

  331

  332         let rec addVertices (path:Polyline)

  333           index (pts:Point3d list) =

  334

  335           match pts with

  336           | [] -> []

  337           | a::[] ->

  338             path.AddVertexAt

  339               (index,

  340               a.Convert2d(cur.GetPlane()),

  341               0.0, 0.0, 0.0)

  342             []

  343           | a::b ->

  344             path.AddVertexAt

  345               (index,

  346               a.Convert2d(cur.GetPlane()),

  347               0.0, 0.0, 0.0)

  348             addVertices path (index+1) b

  349

  350         // Plug our two functions together, ignoring

  351         // the results

  352

  353         definePath picked numBounces |>

  354           addVertices path 1 |>

  355             ignore

  356

  357         // Now we'll add our polyline to the drawing

  358

  359         let bt =

  360             tr.GetObject

  361               (db.BlockTableId,

  362               OpenMode.ForRead) :?> BlockTable

  363         let btr =

  364             tr.GetObject

  365               (bt.[BlockTableRecord.ModelSpace],

  366               OpenMode.ForWrite) :?> BlockTableRecord

  367

  368         // We need to transform the path polyline so

  369         // that it's over our boundary

  370

  371         path.TransformBy

  372           (Matrix3d.Displacement

  373             (cur.StartPoint - Point3d.Origin))

  374

  375         // Add our path to the modelspace

  376

  377         btr.AppendEntity(path) |> ignore

  378         tr.AddNewlyCreatedDBObject(path, true)

  379

  380         // Commit, whether we added a path or not.

  381

  382         tr.Commit()

  383

  384         // Print how much time has elapsed

  385

  386         let elapsed =

  387           System.DateTime.op_Subtraction

  388             (System.DateTime.Now, starttime)

  389

  390         ed.WriteMessage

  391           ("\nElapsed time: " + elapsed.ToString())

  392

  393         // If we're tracing, pause for user input

  394         // before regenerating the graphics

  395

  396         if doTrace then

  397           let pko =

  398             new PromptKeywordOptions

  399               ("\nPress return to clear trace vectors: ")

  400           pko.AllowNone <- true

  401           pko.AllowArbitraryInput <- true

  402           let pkr = ed.GetKeywords(pko)

  403           ed.Regen()

  404

  405     | _ ->

  406       ed.WriteMessage("\nObject is not a curve.")

Here's the the complete project which defines both FB and FBA commands for simple side-by-side comparison.

February 21, 2008 in AutoCAD, AutoCAD .NET, Concurrent programming, F# | Permalink | Comments (2) | TrackBack

Using F# Asynchronous Workflows to simplify concurrent programming in AutoCAD

In the last post we saw some code that downloaded data - serially - from a number of websites via RSS and created AutoCAD entities linking to the various posts.

As promised, in today's post we take that code and enable it to query the same data in parallel by using Asynchronous Workflows in F#. Asynchronous Workflows are an easy-to-use yet powerful mechanism for enabling concurrent programming in F#.

Firstly, a little background as to why this type of technique is important. As many - if not all - of you are aware, the days of raw processor speed doubling every couple of years are over. The technical innovations that enabled Moore's Law to hold true for half a century are - at least in the area of silicon-based microprocessor design - hitting a wall (it's apparently called the Laws of Physics :-). Barring some disruptive technological development, the future gains in computing performance are to be found in the use of parallel processing, whether via multiple cores, processors or distributed clouds of computing resources.

Additionally, with an increasing focus on distributed computing and information resources, managing tasks asynchronously becomes more important, as information requests across a network inevitably introduce a latency that can be mitigated by the tasks being run in parallel.

The big problem is that concurrent programming is - for the most-part - extremely difficult to do, and even harder to retro-fit into existing applications. Traditional lock-based parallelism (where locks are used to control access to shared computing resources) is both unwieldy and prone to blocking. New technologies, such as Asynchronous Workflows and Software Transactional Memory, provide considerable hope (and this is a topic I have on my list to cover at some future point).

Today's post looks at a relatively simple scenario, in the sense that we want to perform a set of discrete tasks in parallel, harnessing those fancy multi-core systems for those of you lucky enough to have them (I'm hoping to get one when I next replace my notebook, sometime in March), but that these tasks are indeed independent: we want to wait until they are all complete, but we do not have the additional burden of them communicating amongst themselves or using shared resources (e.g. accessing shared memory) during their execution.

We are also going to be very careful only to run parallel tasks unrelated to AutoCAD. Any access made into AutoCAD's database, for instance, needs to be performed in series: AutoCAD is not thread-safe when it comes to the vast majority of its programmatically-accessible functionality. So we're going to run a set of asynchronous, parallel tasks to query our various RSS feeds, and combine the results before creating the corresponding geometry in AutoCAD. This all sounds very complex, but the good (actually great) news is that Asynchronous Workflows does all the heavy lifting. Phew.

Here's the modified F# code, with the modified/new lines coloured in red:

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module MyNamespace.MyApplication

    8

    9 // Import managed assemblies

   10

   11 #I @"C:\Program Files\Autodesk\AutoCAD 2008"

   12

   13 #r "acdbmgd.dll"

   14 #r "acmgd.dll"

   15

   16 open Autodesk.AutoCAD.Runtime

   17 open Autodesk.AutoCAD.ApplicationServices

   18 open Autodesk.AutoCAD.DatabaseServices

   19 open Autodesk.AutoCAD.Geometry

   20 open System.Xml

   21 open System.Collections

   22 open System.Collections.Generic

   23 open System.IO

   24 open System.Net

   25 open Microsoft.FSharp.Control.CommonExtensions

   26

   27 // The RSS feeds we wish to get. The first two values are

   28 // only used if our code is not able to parse the feed's XML

   29

   30 let feeds =

   31   [ ("Through the Interface",

   32     "http://blogs.autodesk.com/through-the-interface",

   33     "http://through-the-interface.typepad.com/through_the_interface/rss.xml");

   34

   35     ("Don Syme's F# blog",

   36     "http://blogs.msdn.com/dsyme/",

   37     "http://blogs.msdn.com/dsyme/rss.xml");

   38

   39     ("Shaan Hurley's Between the Lines",

   40     "http://autodesk.blogs.com/between_the_lines",

   41     "http://autodesk.blogs.com/between_the_lines/rss.xml");

   42

   43     ("Scott Sheppard's It's Alive in the Lab",

   44     "http://blogs.autodesk.com/labs",

   45     "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

   46

   47     ("Lynn Allen's Blog",

   48     "http://blogs.autodesk.com/lynn",

   49     "http://lynn.blogs.com/lynn_allens_blog/index.rdf");

   50

   51     ("Heidi Hewett's AutoCAD Insider",

   52     "http://blogs.autodesk.com/autocadinsider",

   53     "http://heidihewett.blogs.com/my_weblog/index.rdf") ]

   54

   55 // Fetch the contents of a web page, asynchronously

   56

   57 let httpAsync(url:string) =

   58   async { let req = WebRequest.Create(url)

   59           use! resp = req.GetResponseAsync()

   60           use stream = resp.GetResponseStream()

   61           use reader = new StreamReader(stream)

   62           return reader.ReadToEnd() }

   63

   64 // Load an RSS feed's contents into an XML document object

   65 // and use it to extract the titles and their links

   66 // Hopefully these always match (this could be coded more

   67 // defensively)

   68

   69 let titlesAndLinks (name, url, xml) =

   70   let xdoc = new XmlDocument()

   71   xdoc.LoadXml(xml)

   72

   73   let titles =

   74     [ for n in xdoc.SelectNodes("//*[name()='title']")

   75         -> n.InnerText ]

   76   let links =

   77     [ for n in xdoc.SelectNodes("//*[name()='link']") ->

   78         let inn = n.InnerText

   79         if  inn.Length > 0 then

   80           inn

   81         else

   82           let href = n.Attributes.GetNamedItem("href").Value

   83           let rel = n.Attributes.GetNamedItem("rel").Value

   84           if href.Contains("feedburner") then

   85               ""

   86           else

   87             href ]

   88

   89   let descs =

   90     [ for n in xdoc.SelectNodes

   91         ("//*[name()='description' or name()='content' or name()='subtitle']")

   92           -> n.InnerText ]

   93

   94   // A local function to filter out duplicate entries in

   95   // a list, maintaining their current order.

   96   // Another way would be to use:

   97   //    Set.of_list lst |> Set.to_list

   98   // but that results in a sorted (probably reordered) list.

   99

  100   let rec nub lst =

  101     match lst with

  102     | a::[] -> [a]

  103     | a::b ->

  104       if a = List.hd b then

  105         nub b

  106       else

  107         a::nub b

  108     | [] -> []

  109

  110   // Filter the links to get (hopefully) the same number

  111   // and order as the titles and descriptions

  112

  113   let real = List.filter (fun (x:string) -> x.Length > 0) 

  114   let lnks = real links |> nub

  115

  116   // Return a link to the overall blog, if we don't have

  117   // the same numbers of titles, links and descriptions

  118

  119   let lnum = List.length lnks

  120   let tnum = List.length titles

  121   let dnum = List.length descs

  122

  123   if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

  124     [(name,url,url)]

  125   else

  126     List.zip3 titles lnks descs

  127

  128 // For a particular (name,url) pair,

  129 // create an AutoCAD HyperLink object

  130

  131 let hyperlink (name,url,desc) =

  132   let hl = new HyperLink()

  133   hl.Name <- url

  134   hl.Description <- desc

  135   (name, hl)

  136

  137 // Use asynchronous workflows in F# to download

  138 // an RSS feed and return AutoCAD HyperLinks

  139 // corresponding to its posts

  140

  141 let hyperlinksAsync (name, url, feed) =

  142   async { let! xml = httpAsync feed

  143           let tl = titlesAndLinks (name, url, xml)

  144           return List.map hyperlink tl }

  145

  146 // Now we declare our command

  147

  148 [<CommandMethod("rss")>]

  149 let createHyperlinksFromRss() =

  150

  151   // Let's get the usual helpful AutoCAD objects

  152

  153   let doc =

  154     Application.DocumentManager.MdiActiveDocument

  155   let db = doc.Database

  156

  157   // "use" has the same effect as "using" in C#

  158

  159   use tr =

  160     db.TransactionManager.StartTransaction()

  161

  162   // Get appropriately-typed BlockTable and BTRs

  163

  164   let bt =

  165     tr.GetObject

  166       (db.BlockTableId,OpenMode.ForRead)

  167     :?> BlockTable

  168   let ms =

  169     tr.GetObject

  170       (bt.[BlockTableRecord.ModelSpace],

  171       OpenMode.ForWrite)

  172     :?> BlockTableRecord

  173

  174   // Add text objects linking to the provided list of

  175   // Hyp