returning lambda from a conversion function

Discussion in 'AutoCAD' started by Kyle Wakefield, May 1, 2004.

  1. I'm trying to write a function that takes a source unit and a target unit
    and returns a function that takes a single value and converts from->to
    units. The code says it better than I can, so...


    (defun conversion (from to)
    (cond
    ((= from to) '((x) x))
    ((= from "mi") '((x) ((conversion "mm" to) (* x 1609344))))
    ((= from "yd") '((x) ((conversion "mm" to) (* x 914.4))))
    ((= from "ft") '((x) ((conversion "mm" to) (* x 304.8))))
    ((= from "in") '((x) ((conversion "mm" to) (* x 25.4))))
    ((= from "cm") '((x) ((conversion "mm" to) (* x 10))))
    ((= from "m") '((x) ((conversion "mm" to) (* x 1000))))
    ((= from "km") '((x) ((conversion "mm" to) (* x 1000000))))
    ((and (= from "mm") (= to "mi")) '((x) (/ x 1609344)))
    ((and (= from "mm") (= to "yd")) '((x) (/ x 914.4)))
    ((and (= from "mm") (= to "ft")) '((x) (/ x 304.8)))
    ((and (= from "mm") (= to "in")) '((x) (/ x 25.4)))
    ((and (= from "mm") (= to "cm")) '((x) (/ x 10)))
    ((and (= from "mm") (= to "m")) '((x) (/ x 1000)))
    ((and (= from "mm") (= to "km")) '((x) (/ x 1000000)))
    (t (*error* "unable to convert units."))))

    (setq convert (conversion "ft" "cm"))
    (setq value 666)
    (setq converted (convert value))


    The problem I'm having is that I don't understand how to 'inline' the
    recursive call to conversion when returning a conversion
    function that converts from anything other the (mm). Basically, conversion
    ends up returning an function like this:

    '((x) ((conversion "mm" to) (* x 304.8)))

    Which makes sense because that exactly what I wrote. But, I want
    conversion to actually return something like this:

    '((x) ((/ (* x 304.8) 10)))
    ;;I want the lambda that '(conversion "mm" to)' returns and not
    literally the expression '(conversion "mm" to)'

    so that conversion doesn't get called again every time the returned
    function is invoked. How do I do this?

    Thank you.
     
    Kyle Wakefield, May 1, 2004
    #1
  2. Kyle,

    Not sure I completely understand what you are
    trying to do but, have you looked at the cvunit
    function in the AutoLISP reference?


    --
    Autodesk Discussion Group Facilitator


    <snip>
     
    Jason Piercey, May 1, 2004
    #2
  3. "have you looked at the cvunit" (Jason Piercey)

    No, I hadn't; that's exactlly what the function I'm writing does. Thanks.
    : )

    But, that's really only half of what I'm trying to accomplish.
    I'm actually more insterested in better understanding the Lisp in general.
    Converting units in of itself seems trivial and could be programmed using
    many different methods.
    I could easily change the same function around to return a scaler instead of
    another function:

    (defun conversion-scaler (from to)
    (cond
    ((= from to) 1.0)
    ((= from "mi") (* (conversion-scaler "mm" to) 1609344))
    ((= from "yd") (* (conversion-scaler "mm" to) 914.4))
    ((= from "ft") (* (conversion-scaler "mm" to) 304.8))
    ((= from "in") (* (conversion-scaler "mm" to) 25.4))
    ((= from "cm") (* (conversion-scaler "mm" to) 10))
    ((= from "m") (* (conversion-scaler "mm" to) 1000))
    ((= from "km") (* (conversion-scaler "mm" to) 1000000))
    ((and (= from "mm") (= to "mi")) (/ 1.0 1609344))
    ((and (= from "mm") (= to "yd")) (/ 1.0 914.4))
    ((and (= from "mm") (= to "ft")) (/ 1.0 304.8))
    ((and (= from "mm") (= to "in")) (/ 1.0 25.4))
    ((and (= from "mm") (= to "cm")) (/ 1.0 10))
    ((and (= from "mm") (= to "m")) (/ 1.0 1000))
    ((and (= from "mm") (= to "km")) (/ 1.0 1000000))
    (t (*error* "unable to convert units."))))

    (setq scaler (conversion-scaler "ft" "cm"))
    (setq value 666)
    (setq converted (* scaler value))

    Which does have better performace characteristics than the prevoius version,
    but then I would still lack understanding as to why I couldn't figure out
    the last one.

    Thank you for pointing out 'cvunit' though.
    I'm sure that I'll actually use it instead of the one I've witten ('cvunit'
    certainly has more features).
     
    Kyle Wakefield, May 2, 2004
    #3
  4. (defun conversion-scaler (from to)
    Disregarding the purpose of this code, one thing I'll say
    about this approach is that it is not nearly as efficient
    as it can be. One main reason is because it performs many
    needless, redundant comparison tests using the (=) function.

    Remember that with (cond), in order for any condition to
    succeed, all preceding conditions must fail. In this case,
    each of them has a redundant, duplicate expression (namely,
    the (= from "mm") test).

    To help illustrate, put a trace on the (=) function, and
    call your function with the following arguments. Look at
    what happens:

    Command: (trace =)
    =
    Command: (conversion-scaler "mm" "km")
    Entering (= "mm" "km")
    Result: nil
    Entering (= "mm" "mi")
    Result: nil
    Entering (= "mm" "yd")
    Result: nil
    Entering (= "mm" "ft")
    Result: nil
    Entering (= "mm" "in")
    Result: nil
    Entering (= "mm" "cm")
    Result: nil
    Entering (= "mm" "m")
    Result: nil
    Entering (= "mm" "km")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "mi")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "yd")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "ft")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "in")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "cm")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "m")
    Result: nil
    Entering (= "mm" "mm")
    Result: T
    Entering (= "km" "km")
    Result: T
    1.0e-006

    Whoa! that's a lotta testing dude! And, it's largely
    redundant (notice how many times (= "mm" "mm") is called).

    Aside from the redundant testing, this seems to be a case
    of recursion for recursion's sake. I see no need for it.

    The following isn't intended to illustrate a better way to
    address the specific problem of unit conversion, but rather,
    only to demonstrate another approach to coding a more general
    problem in a more efficient way.

    ;; A global map that can be used to look up the
    ;; scalar for a given mm-to-unit conversion:

    (setq *mm-scalar-map*
    '( ("mm" . 1.0)
    ("mi" . 1609344.0)
    ("yd" . 914.4)
    ("ft" . 304.8)
    ("in" . 25.4)
    ("cm" . 10.0)
    ("m" . 1000)
    ("km" . 1000000)
    )
    )

    ;; Given a unit key, return
    ;; the scalar for mm conversion:

    (defun get-mm-scalar (units)
    (cdr (assoc (strcase units t) *mm-scalar-map*))
    )

    ;; Convert from a given unit of measure to the base (mm)

    (defun convert-to-mm (value units / scalar)
    (if (setq scalar (get-mm-scalar units))
    (* value scalar)
    (raise-error "Invalid units key")
    )
    )

    ;; Convert from the base (mm) to a given unit of measure

    (defun convert-from-mm (value units / scalar)
    (if (setq scalar (get-mm-scalar units)
    (/ value scalar)
    (raise-error "Invalid units key")
    )
    )

    ;; Convert from/to any unit of measure

    (defun convert (value from to)
    (setq from (strcase from t)
    to (strcase to t)
    )
    (cond
    ( (= from to) value)
    ( (= from "mm") (convert-from-mm value to))
    ( (= to "mm") (convert-to-mm value from))
    (t (convert-from-mm (convert-to-mm value from) to))
    )
    )

    ;; not intended for actual use. Your real
    ;; error handler should do cleanup, etc.
    ;; before stopping.

    (defun raise-error (msg)
    (if *debug* (vl-bt))
    (alert msg)
    ((lambda (/ *error*) (exit)))
    )

    Contrasting this to your original, a trace on
    the (=) function, yields this:

    Command: (trace =)
    =
    Command: (convert 1.0 "ft" "in")
    Entering (= "ft" "in")
    Result: nil
    Entering (= "ft" "mm")
    Result: nil
    Entering (= "in" "mm")
    Result: nil
    12.0

    Note that regardless of what arguments are passed,
    the number of comparisons that (=) is called to make,
    is never greater than 3.
     
    Tony Tanzillo, May 2, 2004
    #4
  5. Thank you for the excellent feedback, Tony.
    Using a map to find the conversion factors for units is a much better
    solution than the way I was approching the problem.
    I refactored your code examples and incorperated them in to a (probably
    unpractical : ) ) command called CHUNITS.
    CHUNITS simply iterates through a selection-set of 'TEXT' entities (assumed
    to be of valid format) performing a user specified unit conversion.

    Code:

    (defun C:CHUNITS ( / olderr sysvars setv unsetvars scalar conversion-scalar
    cdra
    getunits from to value ssforeach getformat format)
    (setq olderr *error*)
    (defun setv (name value / oldval)
    (setq oldval (getvar name))
    (if (not (assoc name sysvars))
    (setq sysvars (append sysvars (list (cons name oldval)))))
    (setvar name value)
    oldval)
    (defun unsetvars ( )
    (foreach v sysvars (setvar (car v) (cdr v))))
    (defun *error* (msg)
    (unsetvars)
    (setq *error* olderr)
    (if (and msg (/= msg "Function cancelled"))
    (prompt (strcat "Error: " msg))
    (princ)))
    (defun cdra (key assoc_list)
    (cdr (assoc key assoc_list)))
    (defun ssforeach (selection f / index entitiy)
    (setq index 0)
    (if selection
    (while (setq entitiy (ssname selection index))
    (f entitiy)
    (setq index (+ index 1)))))
    (defun getformat (msg)
    (initget 1 "Scientific Decimal Engineering Architectural Fractional")
    (setq units (getkword
    (strcat msg
    "[Scientific/Decimal/Engineering/Architectural/Fractional]: ")))
    (cond
    ((= units "Scientific") 1)
    ((= units "Decimal") 2)
    ((= units "Engineering") 3)
    ((= units "Architectural") 4)
    ((= units "Fractional") 5)))
    (defun getunits (msg)
    (initget 1 "mi yd ft in km m cm mm")
    (setq to (getkword (strcat msg "[mi/yd/ft/in/km/m/cm/mm]: "))))
    (defun conversion-scalar (from to / map getscalar)
    (setq map
    '(("mm" . 1.0)
    ("mi" . 1609344.0)
    ("yd" . 914.4)
    ("ft" . 304.8)
    ("in" . 25.4)
    ("cm" . 10.0)
    ("m" . 1000)
    ("km" . 1000000)))
    (defun getscalar (units / scalar)
    (if (not (setq scalar (cdra (strcase units t) map)))
    (*error* "Invalid units key."))
    scalar)
    (* (/ 1.0 (getscalar to)) (getscalar from)))
    (setv "cmdecho" 0)
    (command "UNDO" "BEGIN")
    (setq format (getformat "Enter source format "))
    (setq from (getunits "Enter source units "))
    (setq to (getunits "Enter conversion units "))
    (setq scalar (conversion-scalar from to))
    (ssforeach (ssget '((0 . "TEXT")))
    '((entity) (setq entity (entget entity))
    (setq value (* scalar (distof (cdra 1 entity) format)))
    (entmod (subst (cons 1 (rtos value)) (assoc 1 entity) entity))))
    (command "UNDO" "END")
    (*error* nil)
    (princ)
    )


    Any additional input will be much appreciated.

    Thanks.

    --
    Kyle Wakefield



    <..............................>
     
    Kyle Wakefield, May 2, 2004
    #5
  6. (setq olderr *error*)

    1. Unless you're using a really old version of AutoCAD (which
    I doubt, based on how you format the keyword list for initget),
    you don't have to save the existing *error* handler in a local,
    you just declare *error* as a local:

    (defun C:MYCOMMAND ( / *error* )
    (defun *error* (msg)
    ....
    )
    )

    This will automatically protect the global definition of *error*
    and restore it in any case.
    While its not a major issue, here is how one might
    consolidate some of the keyword handling and input:

    (setq UnitNames
    '( "Scientific"
    "Decimal"
    "Engineering"
    "Architectural"
    "Fractional"
    )
    )

    ;; (strlcat '("First" "Second" "Third") ",")
    ;; ---> "First,Second,Third"

    (defun strlcat (lst delim)
    (apply 'strcat
    (cons (car lst)
    (mapcar
    '(lambda (s) (strcat s delim))
    (cdr lst)
    )
    )
    )
    )

    ;; derive the initget string:

    (initget 1 (strlcat UnitNames " "))

    ;; derive the GetKWord string:

    (setq input
    (getkword
    (strcat "[" (strlcat UnitNames "/") "]: ")
    )
    )

    ;; The response converted to the positional
    ;; index of the correseponding string in the
    ;; UnitNames list:

    (setq units (vl-position input UnitNames))

    You might also want to use UNDO, End, U in your error
    handler if you want to rollback the entire command on
    an error.

    On a more general note, if you've not had much exposure
    to the ActiveX way of doing things, then you might want
    to begin exploring it. While there are some areas where
    it falls short, there are others where it clearly is the
    better choice (performance is one).
     
    Tony Tanzillo, May 3, 2004
    #6
  7. I now understand why I couldn't get the original idea of this to work
    correctly. I was trying to write a function that returns a closure. I now
    realize that closures are not implementable in languages with dynamic
    scoping, such as AutoLisp. In languages that have lexical scoping the
    returning function's variables would be bound their values at creation time.
    With dynamic scoping the returning function's variables are bound to values
    at the time of invocation. Which, in AutoLisp, will error about
    undefined/unbound symbols being used BY the returning function.

    An example, from cleaner code that the initial attempt, would (I think, I'm
    still learning) look sort-of like this in a language with lexical scoping:


    (defun conversion (from to / map getscalar scalar-from scalar-to convert)
    (setq map
    '(("mm" . 1.0)
    ("mi" . 1609344.0)
    ("yd" . 914.4)
    ("ft" . 304.8)
    ("in" . 25.4)
    ("cm" . 10.0)
    ("m" . 1000)
    ("km" . 1000000)))
    (defun getscalar (units / scalar)
    (if (not (setq scalar (cdra (strcase units t) map)))
    (*error* "Invalid units key."))
    scalar)
    (setq scalar-from (getscalar from))
    (setq scalar-to (getscalar to))
    (defun convert (value)
    (* (/ value scalar-to) scalar-from))
    convert)

    (setq convert (conversion "ft" "mm"))
    (setq value 666)
    (setq converted-value (convert value))


    :)
     
    Kyle Wakefield, May 4, 2004
    #7
  8. Thank you for reading through my code and giving me helpful pointers and
    tips.
    It's nice of you to spend time doing such things, and I appreciate it.

    --
    Kyle Wakefield


     
    Kyle Wakefield, May 4, 2004
    #8
Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.