On Dec 13, 1:24 am, Rich Hickey <richhic...@gmail.com> wrote:
> fnil seems to me to have greater utility than patching all functions
> that apply functions with default-supplying arguments.
Neat :) I like it.
> The get-in function could be enhanced, and would mirror get.
Should I interpret 'could' as 'patch welcome' or 'let me think about
it'?
Regards,
Tim.
Hi Rich,
To further comment on fnil, after having experimented with it a bit
now, I've come to slightly prefer specifying the 'default' value
before the function just because I think it reads nicer:
(fnil 0 inc) ;; instead of (fnil inc 0)
(fnil [] conj) ;; instead of (fnil conj [])
I read it as "fill nil with 0 for inc"
I suppose "fill nil of inc with 0" makes just as much sense but I find
"inc 0" leads my eye to believe 0 will always be passed to inc,
whereas "0 inc" does not. Putting the function last makes it clearer
to me that the 0 is conditional. It also looks more like if nil 0.
This contrasts with get, which makes perfect sense having the default
last, but I think at this point fnil and get are sufficiently far
apart that a different argument order would not be surprising.
Just a small observation I thought I'd raise for discussion to see
what preferences are out there if this function becomes widespread.
Regards,
Tim.
(defn fnil-2
[f & defaults]
(fn[& args]
(let [used-args (map (fn [default-value value]
(if value value default-value))
defaults args)]
(apply f used-args))))
user=>(def nil+ (fnil-2 + 0 1 2 3 4 5 6))
user=>(nil+ nil)
0
user=>(nil+ 0 nil)
1
user=>(nil+ 0 0 nil)
2
user=>(nil+ 0 0 0 nil)
3
...
user=>(nil+ 0 0 0 0 0 0 nil)
6
;This example exceeds the provided # of nil values
user=>(nil+ 0 0 0 0 0 0 0 nil)
NPE
You get the idea. Also, Rich's style matches the signature of partial
better, which I personally prefer.
Just my $.02
Sean
Hi Sean,
Thanks for commenting.
Just by way of clarification, taking the function as the last argument
seems to work fine in my experiments. I'm sure it could be better but
here what I've been using:
(defn fnil
"Creates a new function that if passed nil as an argument,
operates with supplied arguments instead. The function must be
passed as the last argument."
[& more]
(fn [& m] (apply (last more)
(map #(if (nil? %1) %2 %1)
m
(concat (butlast more)
(drop (count (butlast more)) m))))))
Relating to your examples:
user=> (def nil+ (fnil 0 1 2 3 4 5 6 +))
user=> (nil+ 0 0 0 0 0 0 nil)
6
user=> (nil+ 0 0 0 0 0 0 0 nil)
java.lang.NullPointerException (NO_SOURCE_FILE:0)
user=> ((fnil 1 +) nil 2 3 4 5)
15
; note fnil-2 does not handle the last case, though of course it could
easily if you wanted it to:
user=> ((fnil-2 1 +) nil 2 3 4 5)
java.lang.ClassCastException: java.lang.Integer cannot be cast to
clojure.lang.IFn (NO_SOURCE_FILE:0)
; in principle I don't think one form is any more restrictive than the
other, it just comes down to a matter of preference which is the key
part I wanted to generate discussion about.
> matches the signature of partial better, which I personally prefer.
Yes that is precisely why it catches me out to write (fnil + 1)
because it looks like a partial application. Partial applications are
actually very common even when partial is not explicitly used:
user=> (swap! (atom 1) + 1)
2
So I'm used to seeing arguments to the right of a function get
absorbed so to speak, and used to seeing conditionals occur in the
middle of the form. Again just clarifying that I too like partial
application style but have the opposite reaction in this case of
wanting to contrast that for fnil as it is not a partial application.
This of course is just my preference and I'm glad to hear reasoning
for the other style.
Regards,
Tim.
Correction, I just realized of course it doesn't work if I specify the
arguments around the wrong way! I should have done:
user=> ((fnil-2 + 1) nil 2 3 4 5)
1
; I think this should result in 15, but that's just an implementation detail.
On Dec 30 2009, 6:18 am, Timothy Pratley <timothyprat...@gmail.com>
wrote:
Patches welcome for get-in, and my fnil.
Thanks,
Rich
Sean
On Jan 29, 9:04 am, Sean Devlin <francoisdev...@gmail.com> wrote:
> Rich,
> Your example didn't support a variadic signature. Is that the long
> term plan?
>
It's the short term plan. Let's see if there's any real need for more
than three. I've never needed more than one.
Rich
Groovy! uploaded to:
https://www.assembla.com/spaces/clojure/tickets/256-get-in-optional-default-value
https://www.assembla.com/spaces/clojure/tickets/257-add-fnil-for-wrapping-functions-to-handle-nil
Regards,
Tim.
If I understand this arity version of get-in correctly, won't the
default also be used if the value stored in the nested data structure
evaluates to something false-y?
Anyway, thanks for creating the patches!
Am 31.01.2010 um 18:29 schrieb Daniel Werner:
>> ([m ks not-found]
>> (if-let [v (reduce get m ks)]
>> v
>> not-found)))
>
> If I understand this arity version of get-in correctly, won't the
> default also be used if the value stored in the nested data structure
> evaluates to something false-y?
Yes. It should probably read:
(if-let [l (reduce get m (pop ks))]
(get l (peek ks) not-found)
not-found))
Sincerely
Meikel
Thanks for spotting that early!
On 1 February 2010 05:46, Meikel Brandmeyer <m...@kotka.de> wrote:
> (if-let [l (reduce get m (pop ks))]
> (get l (peek ks) not-found)
> not-found))
Good idea, but peek and pop work differently on vectors and sequences,
seeing get-in is not constrained to use vectors this could lead to an
unexpected behavior:
user=> (def m {:a 1, :b 2, :c {:d 3, :e 4}, :f nil})
#'user/m
user=> (get-in3 m '(:c :e) 2) ;peek-pop keys applied in unexpected order
2
user=> (get-in2 m '(:c :e) 2) ;expected result
4
I've replaced the patch on assembla with this:
(reduce #(get %1 %2 not-found) m ks)))
And added test cases for the falsey returns and seq args
Regards,
Tim.
On Feb 1, 1:57 pm, Timothy Pratley <timothyprat...@gmail.com> wrote:
> Good idea, but peek and pop work differently on vectors and sequences,
> seeing get-in is not constrained to use vectors this could lead to an
> unexpected behavior:
> user=> (def m {:a 1, :b 2, :c {:d 3, :e 4}, :f nil})
> #'user/m
> user=> (get-in3 m '(:c :e) 2) ;peek-pop keys applied in unexpected order
> 2
> user=> (get-in2 m '(:c :e) 2) ;expected result
> 4
o.O Ok... 100% of my use-cases for get-in were with vectors up to now.
Didn't think about the allowing lists also.
> I've replaced the patch on assembla with this:
> (reduce #(get %1 %2 not-found) m ks)))
> And added test cases for the falsey returns and seq args
Consider this (admittedly constructed) case:
(get-in {:a {:b 1}} [:x :c] {:c :uhoh})
I would not mind only allowing vectors in the interface. I would
expect the length of such a chain sufficiently short for easy
conversion if necessary. Or to use butlast/last instead of peek/pop.
Again the "damage" would be limited by the short keychain length.
(Of course it would have to be considered: is the usual keychain
really short?)
Sincerely
Meikel
Excellent point!
> Or to use butlast/last instead of peek/pop.
I think this is the best approach. butlast/last have linear time so
the overhead is small.
There are still some sharp edges I'm not sure about:
(A) user=> (get-in {:a 1} [])
{:a 1}
;; this is existing behavior, but I feel the result should be nil
(B) user=> (get-in {:a 1} nil)
{:a 1}
;; this is existing behavior, but I feel the result should be nil (nil
is a seq so not an exception)
(C) user=> (get-in {:a 1} 5)
java.lang.IllegalArgumentException: Don't know how to create ISeq
from: java.lang.Integer (NO_SOURCE_FILE:0)
;; existing behavior, seems sensible to throw an exception here rather
than return nil
(D) user=> (get-in {nil {:a 1}} [] 2)
{:a 1}
;; new behavior
;; using last/butlast only -> this is wrong... need to check the seq size
;; the solution depends upon what is correct for the preceding cases
so need to establish those first
Alternatively one could take the view that [] and nil are illegal key
sequences, in which case should get-in enforce that (via preconditions
or just a check) or should it just be added to the doc-string that
using those values is undefined, or both?
I've marked the ticket back to new in the meantime... will update once resolved.
For reference here is a version that behaves most like existing get-in:
(defn get-in
"Returns the value in a nested associative structure,
where ks is a sequence of keys. Returns nil if the key is not present,
or the not-found value if supplied."
([m ks]
(reduce get m ks))
([m ks not-found]
(if (empty? ks)
m
(if-let [l (reduce get m (butlast ks))]
(get l (last ks) not-found)
not-found))))
I'm not convinced returning the map when given an empty key sequence
is the right thing to do, I'd prefer it to return nil or throw an
exception in both arity cases.
Regards,
Tim.
On Feb 2, 1:19 am, Timothy Pratley <timothyprat...@gmail.com> wrote:
> There are still some sharp edges I'm not sure about:
> (A) user=> (get-in {:a 1} [])
> {:a 1}
> ;; this is existing behavior, but I feel the result should be nil
+1 for nil
> (B) user=> (get-in {:a 1} nil)
> {:a 1}
> ;; this is existing behavior, but I feel the result should be nil (nil
> is a seq so not an exception)
+1 for nil
> (C) user=> (get-in {:a 1} 5)
> java.lang.IllegalArgumentException: Don't know how to create ISeq
> from: java.lang.Integer (NO_SOURCE_FILE:0)
> ;; existing behavior, seems sensible to throw an exception here rather
> than return nil
+1 for exception
> (D) user=> (get-in {nil {:a 1}} [] 2)
> {:a 1}
> ;; new behavior
> ;; using last/butlast only -> this is wrong... need to check the seq size
> ;; the solution depends upon what is correct for the preceding cases
> so need to establish those first
>
> Alternatively one could take the view that [] and nil are illegal key
> sequences, in which case should get-in enforce that (via preconditions
> or just a check) or should it just be added to the doc-string that
> using those values is undefined, or both?
I'm not sure about this too. I tend to an exception. (get m) will also
throw an exception...
> For reference here is a version that behaves most like existing get-in:
>
> (defn get-in
> "Returns the value in a nested associative structure,
> where ks is a sequence of keys. Returns nil if the key is not present,
> or the not-found value if supplied."
> ([m ks]
> (reduce get m ks))
> ([m ks not-found]
> (if (empty? ks)
> m
> (if-let [l (reduce get m (butlast ks))]
> (get l (last ks) not-found)
> not-found))))
I would get rid of the if-let. Together with throwing an exception in
case of an empty key sequence we get:
(defn get-in
"Returns the value in a nested associative structure,
where ks is a sequence of keys. Returns nil if the key is not
present,
or the not-found value if supplied."
([m ks] (get-in m ks nil))
([m ks not-found]
(if-let [ks (seq ks)]
(get (reduce get m (butlast ks)) (last ks) not-found)
(throw (Exception. "key sequence must not be empty")))))
Sincerely
Meikel
I think I disagree.
If you view 'get-in' as an unwrapping operation, unwrapping by zero
steps should return the existing collection, no?
{:foo {:bar {:baz 1}}}
[] => {:foo ...
[:foo] => {:bar ...
[:foo :bar] => {:baz ...
This maps trivially to a sophisticated user's recursive mental model
of get-in:
(defn get-in [m ks]
(loop [looking-at m
first-key (first ks)
remaining-keys (rest ks)]
(if-not first-key
looking-at
(recur (get looking-at first-key)
(first remaining-keys)
(rest remaining-keys)))))
... if there are no keys, it returns m. That's intuitive to me, at
least.
Can you explain why you think the result should be nil?
>> (B) user=> (get-in {:a 1} nil)
>> {:a 1}
>> ;; this is existing behavior, but I feel the result should be nil
>> (nil
>> is a seq so not an exception)
>
> +1 for nil
As above: I equate nil with the empty sequence.
On Feb 2, 7:55 am, Richard Newman <holyg...@gmail.com> wrote:
> I think I disagree. Can you explain why you think the result should be nil?
Woops. I got confused. I didn't mean nil for empty key sequences. I
meant throwing an exception as does get.
Sincerely
Meikel
Thanks for that description I completely agree.
> Can you explain why you think the result should be nil?
I was not thinking very clearly :) Loosely 'oh there is nothing to
look up'. Thanks for setting me straight.
> As above: I equate nil with the empty sequence.
Yup.
Ok patch updated - salient part is:
+ ([m ks not-found]
+ (if (seq ks)
+ (if-let [l (reduce get m (butlast ks))]
+ (get l (last ks) not-found)
+ not-found)
+ m)))
Which preserves all the desired behavior so far :)
http://www.assembla.com/spaces/clojure/tickets/256-get-in-optional-default-value
Regards,
Tim.
On Feb 2, 1:48 pm, Timothy Pratley <timothyprat...@gmail.com> wrote:
> > If you view 'get-in' as an unwrapping operation, unwrapping by zero steps
> > should return the existing collection, no?
>
> Thanks for that description I completely agree.
Hmm.. I thought of get-in as a recursive application of get. get-in
now diverges from get. Maybe this version should be called "unwrap"
instead?
Sincerely
Meikel
Ah yes! Ok patch updated to:
+ ([m ks not-found]
+ (if (seq ks)
+ (get (reduce get m (butlast ks)) (last ks) not-found)
+ m)))
Note that (seq ks) will throw an illegal argument exception if ks is 5
for instance, if ks is nil or empty the original map is preserved.
> Hmm.. I thought of get-in as a recursive application of get. get-in
> now diverges from get. Maybe this version should be called "unwrap"
> instead?
Zero applications of get to a map might be thought of as the map itself.
Are you thinking of a particular scenario where throwing an exception
would be better?
Regards,
Tim.
On Feb 2, 2:31 pm, Timothy Pratley <timothyprat...@gmail.com> wrote:
> > Hmm.. I thought of get-in as a recursive application of get. get-in
> > now diverges from get. Maybe this version should be called "unwrap"
> > instead?
>
> Zero applications of get to a map might be thought of as the map itself.
> Are you thinking of a particular scenario where throwing an exception
> would be better?
Ok. One could see this like that:
(get-in m [:a :b]) => (get (get m :a) :b)
(get-in m [:a]) => (get m :a)
(get-in m []) => m
In so far I understand the picture of what happens. But does it make
sense?
get-in does a lookup of a key sequence in a nested structure of
associative things. I think nil/[] are simply not in the domain of get-
in. However it can be extended to nil/[] as the identity.
So in the end it will probably boil down to some "suitable definition"
argument of the domain of get-in. And I see some applications, where
just returning the original thing might be handy.
I'm persuaded. :)
Sincerely
Meikel