Avoiding nested ifs...

1,080 views
Skip to first unread message

John Szakmeister

unread,
May 26, 2016, 10:50:24 AM5/26/16
to clo...@googlegroups.com
I'm very much a fan of bailing out early in imperative
programming as it helps to avoid a bunch of nested if conditions
that are to follow and read. This typically comes up when
checking arguments to ensure that they're valid (in range, of
acceptable values, etc). One of the major stumbling blocks
I've had when writing Clojure is to find a good way to keep code
concise, but readable.

For instance, take a snippet like this:

def verify_position(pos, type):
# It's acceptable to have a None value--it just means don't
# change the position for the axis.
if pos is None:
return True

# Anything outside our limits is invalid.
if (pos > 90) or (pos < 0):
return False

if type == 'switched' and pos not in (0, 90):
# Switched axes only allow 0 and 90, and nothing in
# between.
return False

if type == 'dual':
# We can't control the value on this axis, so anything
# other than None is invalid.
return False

return True


I find this very readable in that along the way, I can start
throwing things off the raft: after the first condition, I don't
need to worry about None being present. After the second, I
know the value is within limits, etc. I have a hard time
translating the above into (what I believe) is readable Clojure.
Here's my stab at it:

(defn verify-pos [pos axis-type]
(if (nil? pos)
;; nil means don't move the axis.
true
(case axis-type
;; Only 0 and 90 are allowed on a switched axis.
"switched" (or (= pos 0)
(= pos 90))

;; Any non-nil value is disallowed on dual.
"dual" false

;; Otherwise, make sure we're within a valid range.
(and (<= pos 90)
(>= pos 0)))))

Now, this was a rather simple example, but you can see some of
the nesting starting. Add in another condition like
nil?--something that is somewhat global across the different
types--and you get another level of nesting in there.

I can break it up more:

(defn verify-range [pos axis-type]
(case axis-type
;; Only 0 and 90 are allowed on a switched axis.
"switched" (or (= pos 0)
(= pos 90))
;; Any non-nil value is disallowed on dual.
"dual" false
;; Otherwise, make sure we're within a valid range.
(and (<= pos 90)
(>= pos 0))))

(defn verify-pos [pos axis-type]
(or (nil? pos)
(verify-range pos axis-type)))

And this is a bit better, but you run up against another issue:
coming up with good names for each part of the whole so that you
can combine them. And, coming up with names that don't sound so
similar that folks have to dig into the implementation to know
which one is the correct one (I feel the above break up has this
problem).

In some cases, the error checking logic is really
complicated--because the thing being controlled has complex
restrictions that are out of my control--and the nesting of if
conditionals is maddening. Having to come up with names for
each of them would be more than twice as frustrating, as the
individual bits don't lend themselves to good names.

Then there's the issue of combining the verification and the
actual work into a useful function, where you need to verify and
then act on it:

(defn set-position [pos type]
(if (verify-pos pos type)
(send-position-to-equip pos)
(do-something-for-invalid-input)))

Again, this is a simple example, but more complicated cases have
more to check, and therefore more nesting of if statements,
where the early bail technique leaves the flow pretty readable.
I also realize pre- and post- conditions might be useful, but I
don't feel they're appropriate when validating user
input--especially not assertion errors, which we generally think
of programming errors, not user input errors.

I realize some of this may be me and shaping my mind more to
functional programming, though I do feel like I have a decent
grasp of it. I've been dabbling in Clojure for several years,
and have more recently incorporated ClojureScript into a project
for our web front-end. However, I feel like I might be missing
an important strategy for this kind of problem, and I'm hoping
that one of you can point me in a good direction. And please
don't get hung up on this example. I was trying to boil
something much more difficult down into something reasonable
that would help show the issue.

Thank you!

-John

Gary Trakhman

unread,
May 26, 2016, 10:55:47 AM5/26/16
to clo...@googlegroups.com
I think the idiomatic way to handle this in clojure is to do all your validation upfront on a single descriptive data structure, perhaps in a single function, then bail early.  That has the added advantage of being able to report multiple errors, instead of just the first, and is well supported by libs like Schema and the new clojure.spec.

Maybe if the target of the code is by nature imperative, it would make this more difficult.  You could probably abuse exceptions to avoid the nesting.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

John Szakmeister

unread,
May 26, 2016, 11:39:41 AM5/26/16
to clo...@googlegroups.com
On Thu, May 26, 2016 at 10:55 AM, Gary Trakhman <gary.t...@gmail.com> wrote:
> I think the idiomatic way to handle this in clojure is to do all your
> validation upfront on a single descriptive data structure, perhaps in a
> single function, then bail early. That has the added advantage of being
> able to report multiple errors, instead of just the first, and is well
> supported by libs like Schema and the new clojure.spec.

-ENOPARSE. :-) I think you're saying "describe the data you expect
with other data in Clojure", and then use that data to validate the
inputs--hence the reference to Schema.

I guess the problem I see here is that it's complicated. I really
dumbed things down in my example, but there are a number of properties
of the system that are dynamic: the limits of the axis, what
acceptable values are based on the type of axis, whether the
coordinate system is linear or compass-based, etc. Almost all of this
is determined at runtime when we link up with the equipment, but ends
up driving the kinds of validation we need to do. I'm not sure how
something like Schema would handle this, but...

I was trying to avoid using Schema, since it's rather large and I have
slow links to worry about (at least for the web side of things). And
I took clojure.spec as something not to be used in production, but to
help with testing--though I see the cross-over.

**update**: so I went and pulled in Schema, and it's not as bad as I
thought. So perhaps using Schema is more viable than I thought.

> Maybe if the target of the code is by nature imperative, it would make this
> more difficult. You could probably abuse exceptions to avoid the nesting.

I'm not sure I can say it's imperative by nature--it's my desire to
reduce nesting and the number of edges I have to keep in my brain. I
just haven't found a way that I'm happy to deal with it in a
functional manner.

As I mentioned, I was trying to avoid exceptions, but you're right--it
may be the only real way forward, but I'm looking for other options
first. :-)

-John

Gary Trakhman

unread,
May 26, 2016, 11:45:25 AM5/26/16
to clo...@googlegroups.com
Ah, yea, having a 'data spec' specifically for validation is above and beyond what I meant, which was having your domain contained within a single data structure that can be validated using a number of possible techniques. That validation would be separate from the code that's pulling it apart down the line and actually doing the work on known good inputs.

Nathan Davis

unread,
May 26, 2016, 1:43:46 PM5/26/16
to Clojure, jo...@szakmeister.net
First off, I should say that you should first consider alternative approaches before considering the options below.  For example, cond seems well-suited for this particular case:


(defn verify-position[pos type]
 (cond (nil? pos)     true
       (or (> pos 90)
           
(< pos 0)) false

       
.
       
.
       
.

       
:else true))


However, sometimes you really do want imperative-style control flow.  Two possibilities come to mind here:
  1. Monads (e.g., algo.monads)
  2. Continuations (e.g., pulley.cps)
In fact, monads and (delimited) continuations are isomorphic.  The main difference is one of view-point -- the essence of monads is the composition of computations; the essence of continuations is control flow.

For a monadic approach, I would start with something like the "Maybe" monad, but add a monadic 'bail' operator (to use your terminology and avoid conflicting with the common use of 'return' as one of the fundamental operators on monads).  Essentially, 'bail' would be constructed to abort the "current computation".  Then if you implement a function in terms of the 'bailing' monad (and encapsulate the effect to that function), you can use 'bail' to effectively return a value from the function at any point in the computation.

With continuations, all you need is an "abortive" continuation back to the call-site of the function.  With pulley.cps, this is easily accomplished using call-cc / let-cc.  For example, here's your first example in terms of let-cc:


;; Note:  This must be wrapped in an`cps` form,
;;        or use `def` along with `cps-fn`
(defn verify-position[pos type]
 (let-cc [return]
   (when(nil? pos)
     (return true))

    (when (or (> pos 90)
             
(< pos 0))
     
(return false))

   
.
   
.
   
.

   
true))


Basically, we just wrap the function's body in let-cc.  This gives us access to the current continuation of function (i.e., the place where the function was called).  In this case, we bind that continuation to the name return.  By invoking return, we abort the current continuation at that point and restore the captured continuation.  I've used when instead of if to check for conditions that return, because it more clearly conveys that effects are used.  But in any case, it's a pretty straight-forward conversion from the python code.

Using continuations you can build more complex control flows as well.  For instances, you can "return" not just from a function but from any point in the computation -- all you need to do is have access to the continuation at that point.  And since pulley.cps makes continuations first-class objects, you can bind them to variables (including dynamic vars), pass/return them to/from functions, etc.

Hopefully this helps.  Like I said, explore other options first, but continuations and/or monads are there if you decide to use them.

Nathan Davis

Sean Corfield

unread,
May 26, 2016, 1:48:18 PM5/26/16
to Clojure Mailing List
On 5/26/16, 7:50 AM, "John Szakmeister" <jszakm...@gmail.com on behalf of jo...@szakmeister.net> wrote:
>def verify_position(pos, type):
> # It's acceptable to have a None value--it just means don't
> # change the position for the axis.
> if pos is None:
> return True
>
> # Anything outside our limits is invalid.
> if (pos > 90) or (pos < 0):
> return False
>
> if type == 'switched' and pos not in (0, 90):
> # Switched axes only allow 0 and 90, and nothing in
> # between.
> return False
>
> if type == 'dual':
> # We can't control the value on this axis, so anything
> # other than None is invalid.
> return False
>
> return True

(defn verify-position [pos type]
(cond (nil? pos) true
(or (> pos 90) (< pos 0)) false
(and (= type “switched”)
(not (in-range pos 0 90))) false
(= type “dual”) false
:else true))

(or :else (do-stuff-to-verified-position pos type) if that branch is more complex?)

A pattern that I’ve also started using is something like this:

(defn guarded-process [data]
(some-> data
first-guard
second-guard
(another-guard :with “parameters”)
(process-the-data 1 2 3 4)))

That works well for a process that operates on non-nil data and then you write the guards to return the data if it’s valid or nil if it isn’t.

Sean Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/

"If you're not annoying somebody, you're not really alive."
-- Margaret Atwood





Timothy Baldridge

unread,
May 26, 2016, 2:13:00 PM5/26/16
to clo...@googlegroups.com
I would suggest reviewing your data model a bit. One of the problems  you are experiencing is that you are overloading the inputs to your function. And since they are overloaded you have to create a state machine of sorts to parse out if the data is valid. Another approach is to use namespaced keys in a map then dispatching on the contents of the map. This removes a lot of ambiguity in the code.

Then you can use something like clojure.spec to validate your data:

(s/def :pos.switch/value #{0 90})
(s/def :pos.slider/value (set (range 90)))

(s/def :switch/change (s/or :nil nil?
                            :switch (s/keys :req [:pos.switch/value])
                            :slider (s/keys :req [:pos.slider/value])))

(s/valid? :switch/change {:pos.switch/value 42})           ; => false
(s/valid? :switch/change {:pos.slider/value 42})            ; => true
(s/conform :switch/change {:pos.switch/value 0})         ; => [:switch {:pos.switch/value 0}]
(s/conform :switch/change nil)                                       ; => [:nil nil]
(s/conform :switch/change {:pos.slider/dual 0})             ; => :clojure.spec/invalid

(s/exercise :switch/change)                                 ; => Generates 10 example data sets

And as you see from this example, you can use specs to dispatch, validate and even generate test data for your application. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

Michael Ball

unread,
May 26, 2016, 2:31:07 PM5/26/16
to Clojure, jo...@szakmeister.net
I use a little short circuited threading macro to deal with this. Here's a gist with the macro named short-> and how I would write out your first 2 validation steps.

https://gist.github.com/mikeball/10cc64fe97c119671918fb2d1d8b4118


The new spec stuff looks really interesting, haven't had a chance to look into it yet.

John Szakmeister

unread,
May 26, 2016, 4:12:36 PM5/26/16
to clo...@googlegroups.com
On Thu, May 26, 2016 at 1:47 PM, Sean Corfield <se...@corfield.org> wrote:
[snip]
> A pattern that I’ve also started using is something like this:
>
> (defn guarded-process [data]
> (some-> data
> first-guard
> second-guard
> (another-guard :with “parameters”)
> (process-the-data 1 2 3 4)))
>
> That works well for a process that operates on non-nil data and then you write the guards to return the data if it’s valid or nil if it isn’t.

You know, I know about half of the threading macros well, but I
haven't touched some->. It seems like a really good fit for some
problems I'm facing. Thank you for the suggestion!

-John

John Szakmeister

unread,
May 26, 2016, 4:25:38 PM5/26/16
to clo...@googlegroups.com
On Thu, May 26, 2016 at 2:12 PM, Timothy Baldridge <tbald...@gmail.com> wrote:
> I would suggest reviewing your data model a bit. One of the problems you
> are experiencing is that you are overloading the inputs to your function.
> And since they are overloaded you have to create a state machine of sorts to
> parse out if the data is valid. Another approach is to use namespaced keys
> in a map then dispatching on the contents of the map. This removes a lot of
> ambiguity in the code.

Can you describe what you mean a little more? I think what you're
trying to say it that I should somehow separate switched axes from
variable axes, etc., despite that the same data is present in
both--just with slightly different semantics.

The problem I see there is then I have to somehow figure out which
version to grab from the map. If I'm following correctly, you're
perhaps suggesting that I pull the type information higher into the
data model so I can use it to know which version to grab out of the
map? If that's the though process, it seems to me that it's
cluttering things rather than making it better--at least from my
ability to understand what the data model is actually saying. Having
the type grouped under the axis seems more coherent to me.

I've probably misunderstood what you're saying though, so an example
of what you mean would be really helpful. I'm certain that you're
telling me something profound, but I'm just not parsing it out. :-)

> Then you can use something like clojure.spec to validate your data:
>
> (s/def :pos.switch/value #{0 90})
> (s/def :pos.slider/value (set (range 90)))

Is there a way to cope with 0 and 90 actually being variable? What I
mean is that I don't know what the valid range is until I talk with
the back end equipment. So I can't say at compile time what the valid
range will be. I took that bit out of my example because I felt it
was making things too complex to be useful.

> (s/def :switch/change (s/or :nil nil?
> :switch (s/keys :req [:pos.switch/value])
> :slider (s/keys :req [:pos.slider/value])))
>
> (s/valid? :switch/change {:pos.switch/value 42}) ; => false
> (s/valid? :switch/change {:pos.slider/value 42}) ; => true
> (s/conform :switch/change {:pos.switch/value 0}) ; => [:switch
> {:pos.switch/value 0}]
> (s/conform :switch/change nil) ; =>
> [:nil nil]
> (s/conform :switch/change {:pos.slider/dual 0}) ; =>
> :clojure.spec/invalid
>
> (s/exercise :switch/change) ; => Generates
> 10 example data sets
>
> And as you see from this example, you can use specs to dispatch, validate
> and even generate test data for your application.

This is definitely interesting, though I thought spec was geared more
towards testing and less towards runtime validation. Well, now that I
say that, I guess what was said is that spec wasn't meant to be
enabled for everything all the time... you call out the bits you want
to do at runtime. It's probably still too new for me to use though,
especially since I'm currently using ClojureScript and clojure.spec
isn't available there yet. But definitely good to know about. Thank
you for the example!

-John

John Szakmeister

unread,
May 26, 2016, 4:29:23 PM5/26/16
to Nathan Davis, Clojure
(sorry for the repeat Nathan, this was meant to go to the list too)

On Thu, May 26, 2016 at 1:43 PM, Nathan Davis
<nda...@positronic-solutions.com> wrote:
> First off, I should say that you should first consider alternative
> approaches before considering the options below. For example, cond seems
> well-suited for this particular case:
>
>
> (defn verify-position[pos type]
> (cond (nil? pos) true
> (or (> pos 90)
> (< pos 0)) false
>
> .
> .
> .
>
> :else true))

Yeah, cond is definitely useful here, but not in general.

> However, sometimes you really do want imperative-style control flow. Two
> possibilities come to mind here:
>
> Monads (e.g., algo.monads)
> Continuations (e.g., pulley.cps)
>
> In fact, monads and (delimited) continuations are isomorphic. The main
> difference is one of view-point -- the essence of monads is the composition
> of computations; the essence of continuations is control flow.
>
> For a monadic approach, I would start with something like the "Maybe" monad,
> but add a monadic 'bail' operator (to use your terminology and avoid
> conflicting with the common use of 'return' as one of the fundamental
> operators on monads). Essentially, 'bail' would be constructed to abort the
> "current computation". Then if you implement a function in terms of the
> 'bailing' monad (and encapsulate the effect to that function), you can use
> 'bail' to effectively return a value from the function at any point in the
> computation.
>
> With continuations, all you need is an "abortive" continuation back to the
> call-site of the function. With pulley.cps, this is easily accomplished
> using call-cc / let-cc. For example, here's your first example in terms of
> let-cc:

This was very informative. I'm definitely going to write this down as
something to look into. I think there's probably other and better
choices for input validation, but I can see this being extremely
useful for some of the equipment control work I do.

Thank you for the suggestion.

-John

Erik Assum

unread,
May 26, 2016, 5:38:42 PM5/26/16
to clo...@googlegroups.com
Not being good at reading other peoples mind, I’ll give my guess as to what Timothy was trying to suggest:

If you define your input as a map with keys such as:

{:type :switched ; can be :switched :dual or :something
:pos 54}

You can make something like:

(defmulti verify-pos-for :type)

(defmethod verify-pos-for :switched [{:keys [pos]}]
(or (= pos 0) (= pos 90)))

(defmethod verify-pos-for :dual [m]
(nil? (:pos m)))

(defmethod verify-pos-for :default [{:keys [pos]}]
(and (<= pos 90)
(>= pos 0)))

(defn verify-pos [{:keys [pos] :as m}]
(or (nil? pos) (verify-pos-for m)))

I have most certainly messed up your logic, but I do feel multi methods are kind of cool for this.

Erik.

Mark Engelberg

unread,
May 26, 2016, 7:09:48 PM5/26/16
to clojure
On Thu, May 26, 2016 at 1:29 PM, John Szakmeister <jo...@szakmeister.net> wrote:

Yeah, cond is definitely useful here, but not in general.


cond is useful in general, just not the cond that is built-in to Clojure.

About 5 years ago, Christophe Grand pointed out in a blog post that cond, augmented with a few extra things (:let, :when, and :when-let) neatly solved a ton of situations where Clojure often gets inelegantly nested, i.e., interleavings of conditional tests with local variable definitions, protection against nil values, etc.

I've been using this better cond ever since, for five years now, routinely, in my own code.  Once you start using this better cond, you'll never want to go back.

Here's the implementation, for use in your own code: https://gist.github.com/Engelberg/9fc1264f938077cf03eee112ebed1768

The most important ingredient here is the ability to put :let into your cond.  There has been a JIRA issue for this for nearly 7 years (it's a natural extension to cond, because :let is allowed in for clauses).  Given the incredible value this feature offers in terms of keeping code nice and "flat" as opposed to deeply nested/indented, I'm surprised it hasn't yet made it in to Clojure core.  Maybe soon, though, if enough people demonstrate that they care.  Go vote for this issue:  http://dev.clojure.org/jira/browse/CLJ-200.

If you don't see how this feature would clean up code, feel free to post an example that feels ugly with cond, and I'll show you how this improves it.

John Szakmeister

unread,
May 27, 2016, 8:26:14 AM5/27/16
to clo...@googlegroups.com
On Thu, May 26, 2016 at 7:09 PM, Mark Engelberg
<mark.en...@gmail.com> wrote:
> On Thu, May 26, 2016 at 1:29 PM, John Szakmeister <jo...@szakmeister.net>
> wrote:
>>
>>
>> Yeah, cond is definitely useful here, but not in general.
>>
>
> cond is useful in general, just not the cond that is built-in to Clojure.

Sorry, I didn't mean it that way--just that it didn't seem useful to
me in general for the problems I've been facing with validation.

> About 5 years ago, Christophe Grand pointed out in a blog post that cond,
> augmented with a few extra things (:let, :when, and :when-let) neatly solved
> a ton of situations where Clojure often gets inelegantly nested, i.e.,
> interleavings of conditional tests with local variable definitions,
> protection against nil values, etc.
>
> I've been using this better cond ever since, for five years now, routinely,
> in my own code. Once you start using this better cond, you'll never want to
> go back.
>
> Here's the implementation, for use in your own code:
> https://gist.github.com/Engelberg/9fc1264f938077cf03eee112ebed1768
>
> The most important ingredient here is the ability to put :let into your
> cond. There has been a JIRA issue for this for nearly 7 years (it's a
> natural extension to cond, because :let is allowed in for clauses). Given
> the incredible value this feature offers in terms of keeping code nice and
> "flat" as opposed to deeply nested/indented, I'm surprised it hasn't yet
> made it in to Clojure core. Maybe soon, though, if enough people
> demonstrate that they care. Go vote for this issue:
> http://dev.clojure.org/jira/browse/CLJ-200.

I can see a lot of use for this! Thank you for pointing it out!

-John

hiskennyness

unread,
May 27, 2016, 8:52:45 AM5/27/16
to Clojure, jo...@szakmeister.net


On Thursday, May 26, 2016 at 10:50:24 AM UTC-4, John Szakmeister wrote:
I'm very much a fan of bailing out early in imperative
programming as it helps to avoid a bunch of nested if conditions
that are to follow and read.  This typically comes up when
checking arguments to ensure that they're valid (in range, of
acceptable values, etc).  One of the major stumbling blocks
I've had when writing Clojure is to find a good way to keep code
concise, but readable.


Food for thought: that is pretty much how folks felt back in the late 70s when GOTO became a four-letter word and was deprecated in favor of structured programming. So your thought that this might be a transitional thing for you seemed right. Even coming from common lisp, I am waiting out attitude transitions myself.

I introduced structured programming at my first for-hire programming job after watching the senior devs flipping through COBOL printouts back and forth trying to follow the GOTOs. When asked to take a stab at improving things, I suggested (in part) going structured. My manager liked the idea and when I said I was going to throw in the occasional dead obvious GOTO, he challenged me to go pure. I did so but with the attitude of showing how silly the code would turn out. I ended up convinced. There was an emergent property from going pure: if I crashed at line 2042, I knew all the conditions that were true, because I knew exactly how I got there. Silly had value.

Then another programmer was assigned to me and asked to clone my app for a different region. When she got stuck I helped her debug it, and it took hours (because compile times were half hours). It turns out the code we were staring at was forty lines down from a buggy "GOTO my-proc-end" by passing the code we knew had to be working, and monitors were 24x80 back then.

btw. I myself am quite fond of Common Lisp return-from, so I am no purist. But I am hearing your actual use cases are hairy enough that the semantics of a long chain of code chunks each of them free to bail might be hard to follow.

btw2, with others, I use COND a lot for these cases. But it only goes so far. Often I do some heavy-lifting in a clause that needs repeating in another clause if this one decides not to fire.

btw3, yeah, SOME is a great operator.

-hk

 

John Szakmeister

unread,
May 27, 2016, 4:09:10 PM5/27/16
to clo...@googlegroups.com
On Thu, May 26, 2016 at 5:41 PM, Erik Assum <er...@assum.net> wrote:
> Not being good at reading other peoples mind, I’ll give my guess as to what Timothy was trying to suggest:
>
> If you define your input as a map with keys such as:
>
> {:type :switched ; can be :switched :dual or :something
> :pos 54}
>
> You can make something like:
>
> (defmulti verify-pos-for :type)

Yes, a multi-method could be used here, but Timothy suggested the data
model might be wrong and that's what I was interested in. I think I'm
already setup to do the multi-methods with the current data model,
though it feels like a heavy-weight answer to the problem.

Thanks for the suggestion though!

-John
Reply all
Reply to author
Forward
0 new messages