Fork me on GitHub

G'day gurus - I have a couple of hopefully simple questions on style: I have a little fn (basically a predicate) that has three possible outcomes (return values) - true, false, and "the argument you provided is invalid" (which can happen through no fault of the caller's, since the argument is used to lookup a shared mutable data source). Right now I'm using true, false, and nil to identify these three cases, but the use of nil has me feeling a bit uncomfortable (due to nil punning etc.). What would you suggest? FWIW I'm very reluctant to use exceptions (this is not an "exceptional" situation, and they're a pain to handle), but I'm not sure what else would be idiomatic - perhaps an :invalid-argument keyword? Related: right now my fn has a name with a ? at the end, since it's basically a predicate, but I'm a little concerned about this third state it might return. Should I be ditching the ? suffix in the interests of clearly communicating intent?


why not three keywords that more mimic what your domain is about?


ie, if you were looking up if a check could clear you could return :sufficient (funds) :insufficient and :no-account


The use case here is the question "is this user (identified with an id of some kind) in my company or not".


then handle the three cases. you could then have an easy predicate based on these with (defn is-sufficient? [check account] (= (clear-check check account) :sufficient) or whatever your domain needs


The issue is that "user identified by id" part - it's entirely possible for an identifier to no longer be valid.


and what separates nil from false in this case?


Right - that's exactly my question.


but wouldn't that answer user in your company the same?


the predicate of yours sounds like a binary predicate to me


I also have an inverse fn "is this user not in my company", and it also returns nil for that third case.


yes they are in teh company or no, they are not


no longer active versus not valid employee number sounds separate to me


It seems "wrong" to me to have two fns that should be logical not, not actually be so in some cases (i.e. when the user identifier is invalid)


then change your question? But if the question is "this id is an employee in our company" has two answers to me


Right - in fact the fn (deliberately) can't tell the caller why the identifier is invalid, just that it is.


well you can make another function to do that


The trick here is that the identifier is "resolved" via a multi-method that can take all sorts of types.


In some of those cases, it's not possible for the identifier to be invalid (i.e. the identifier is, in fact, the object we're looking up, having previously been loaded from the data source).


does the object satisfy a protocol or record or anything? can you ask some type of isa?


Nah - I greatly dislike protocols, though I'm not entirely sure why. 😉


The two fns in question are same-pod? and cross-pod?.


(defn same-pod?
      "Returns true if the given user is in the same pod as the authenticated connection user, or nil if the user doesn't exist."
      [connection user-identifier]
      (let [them (if (:company user-identifier)
                   (user connection user-identifier))
            me (user connection)]
        (same-company? me them)))


this is what i am thinking


and here same-company is (= (:company me) (:company them))


so use them if its already the object (which seems to be identified by having a :company keyword entry or else look it up


Yeah - that's a nice optimisation on not looking up the user details if we already have them! 👍


the overwrite the variable with a version of itself is a common pattern.


Though it doesn't answer my original question about having a "predicate" that returns three states, and whether using nil is appropriate for the third state.


oh i thought this worked around that. I understood nil would come up when you had the object you were already looking up. So my goal was to collapse this down into a true predicate


Nope - (user ...) can return nil.


but my suggestion would be figure out how to use two states or get a type that has three values. In other words, only use boolean types if you have boolean states. if you have three make your own descriptive keywords for the state


and if you do that you can return a map that includes the reason why something is invalid along with the fact that it is invalid


Yeah I wasn't planning on providing any information on why the identifier is invalid - as you said above that can be provided by some other fn.


(e.g. (user ...))


👍 Anyhoo this has been very useful - thanks @dpsutton ! 👍


no problem. best of luck