~technomancy/fennel

3 2

nil-safe nested table access operator

Details
Message ID
<CAAKhXoZx+p+_fgvgnYHz3vacK+qvu9Ra2m3vq+nxP7nvDm3LSg@mail.gmail.com>
DKIM signature
pass
Download raw message
Hi I wanted to suggest new operator for Fennel for nil-safe nested table access.

Currently Fennel has dot operator to access values stored under keys
in tables. This operator accepts several keys to look up value in
nested set of tables:

    >> (local t {:a {:b {:c 42}}})
    >> (. t :a :b :c)
    42

However if any of keys before :c were not present in respective
tables, we will get error of trying to index nil:

    >> (. t :a :nope :c)
    ;; error

I would like to propose ?. operator which will short-circuit if any of
keys are not present.

    >> (?. t [:a :nope :c])
    nil

This operator accepts keys in sequential table, because optional
default value can be specified as third argument:

    >> (?. t [:a :nope: :c] 27)
    27

This is handy when you're dealing with tables that can be altered over
the liftetime of the application. Below are two sample
implementations:

    (fn ?. [tbl keys not-found]
      (match keys
        [k] (match (. tbl k)
              v (if (. keys 2)
                    (?. v [(table.unpack keys 2)] not-found)
                    v)
              not-found)
        tbl))

    (fn ?. [tbl keys not-found]
      (var res tbl)
      (var t tbl)
      (each [_ k (ipairs keys)]
        (match (. t k)
          v (set [res t] [v v])
          (do (set res not-found)
              (lua :break))))
      res)

The first one is recursive and a bit more verbose after code
generation, also constructs new table on each step which may be not
optimal. The second one iterates over table of keys directly, but
modifies local variables by setting, and uses nasty (lua :break). I'm
not sure which one is better, but the second one is prematurely
optimized. Maybe a less complex implementation can be made, but I
couldn't think of one yet. And maybe this should be a macro instead?

As for the name, I think ?. is good name, as it has similar semantics
embedded in it as for -?> or -?>>. Some fonts with ligatures have
support for this character combination as well. Other variants include
get-in, access-in, maybe others have some ideas too?

-- 
Best regards,
Andrey Listopadov
Details
Message ID
<CAAKhXoYGpRnQ78sAzetZVDUSz7oTMSu3h1vd3aBeCEtNkf=YMQ@mail.gmail.com>
In-Reply-To
<CAAKhXoZx+p+_fgvgnYHz3vacK+qvu9Ra2m3vq+nxP7nvDm3LSg@mail.gmail.com> (view parent)
DKIM signature
pass
Download raw message
> The second one iterates over table of keys directly, but
> modifies local variables by setting, and uses nasty (lua :break).

This break can acutally be just left out as the amount of keys is
fairly small so we can just iterate til the end like this:

    (fn ?. [tbl keys not-found]
      (var res tbl)
      (var t tbl)
      (each [_ k (ipairs keys)]
        (match (. t k)
          v (set [res t] [v v])
          (set res not-found)))
      res)

That's how I've implemented this function in Cljlib, some time ago

-- 
Best regards,
Andrey Listopadov
Details
Message ID
<87sg5sg7rj.fsf@whirlwind>
In-Reply-To
<CAAKhXoZx+p+_fgvgnYHz3vacK+qvu9Ra2m3vq+nxP7nvDm3LSg@mail.gmail.com> (view parent)
DKIM signature
missing
Download raw message
Andrey Orst <andreyorst@gmail.com> writes:
> Hi I wanted to suggest new operator for Fennel for nil-safe nested table access.

Thanks for submitting this. I think it's a helpful addition.

> I would like to propose ?. operator which will short-circuit if any of
> keys are not present.
>
>     >> (?. t [:a :nope :c])
>     nil
>
> This operator accepts keys in sequential table, because optional
> default value can be specified as third argument:
>
>     >> (?. t [:a :nope: :c] 27)
>     27

Overall it seems good. However, making it take keys in a table seems
somewhat strange to me; it seems to break the parallelism with the
existing dot operator. I guess the advantage is that it lets you avoid
an `or` call?  But I think that's not really enough of a reason to
sacrifice consistency, since adding an `or` when needed is not much
extra overhead, and there's not really any precedent anywhere else in
Fennel for having a special "not found" argument.

> The first one is recursive and a bit more verbose after code
> generation, also constructs new table on each step which may be not
> optimal. The second one iterates over table of keys directly, but
> modifies local variables by setting, and uses nasty (lua :break). I'm
> not sure which one is better, but the second one is prematurely
> optimized. Maybe a less complex implementation can be made, but I
> couldn't think of one yet. And maybe this should be a macro instead?

It definitely needs to be a macro, yeah. I'm partial to the recursive
approach above but I'm not sure what it would look like translated into
a macro. Perhaps it could expand to something like this:

  (do
    (var x t)
    (each [_ k (ipairs [:a :nope :c])]
      (when (not= nil x)
        (match (. x k)
          n (set x n))))
    x)

Like you said, since the number of keys is fixed at compile time a
couple extra cycles thru the loop shouldn't matter. Or we could wait
until we have added conditions to the `each` special; this could be a
very good fit for that.

> As for the name, I think ?. is good name, as it has similar semantics
> embedded in it as for -?> or -?>>. Some fonts with ligatures have
> support for this character combination as well. Other variants include
> get-in, access-in, maybe others have some ideas too?

Yeah, ?. is definitely better than .? since the question mark at the end
should be reserved for predicates. Putting a question mark at the
beginning of an argument name is a convention indicating an optional
argument, but since this is a macro name I think it's clear enough that
the convention does not apply.

-Phil
Details
Message ID
<CAAKhXobXnaPgzOSr=7XPR6sYzpfpz1pZ=yHA2i2xzLevuWoYGg@mail.gmail.com>
In-Reply-To
<87sg5sg7rj.fsf@whirlwind> (view parent)
DKIM signature
pass
Download raw message
> Overall it seems good. However, making it take keys in a table seems
> somewhat strange to me; it seems to break the parallelism with the
> existing dot operator. I guess the advantage is that it lets you avoid
> an `or` call?

By making such function take fixed amount of argument at any time, and
specifying not-found value, we create functional interface. For
example, in Cljlib I have this as a get-in function, which can be used
like this:

    >> (mapv get-in [{:a {:b :val}} {:c [1 2]}] [[:a :b] [:c 10]] [:no-val 10])
    ["val" 10]

This can't be done so easilly with variadic function. But this
property can be left out to other libraries to implement, and Fennel
defenitively should have consistency across it's inbuilts.

> It definitely needs to be a macro, yeah. I'm partial to the recursive
> approach above but I'm not sure what it would look like translated into
> a macro. Perhaps it could expand to something like this:
>
>   (do
>     (var x t)
>     (each [_ k (ipairs [:a :nope :c])]
>       (when (not= nil x)
>         (match (. x k)
>           n (set x n))))
>     x)

Yeah, this seems much less complex.

> Like you said, since the number of keys is fixed at compile time a
> couple extra cycles thru the loop shouldn't matter. Or we could wait
> until we have added conditions to the `each` special; this could be a
> very good fit for that.

Well, it's not strictly fixed if we unpack some table I guess?

    >> (local ks [:a :b :c]
    >> (macrodebug (?. t (unpack ks)))
    (do
      (var x t)
      (each [_ k (ipairs [(unpack ks)])]
        (when (not= nil x)
          (match (. x k)
            n (set x n))))
      x)

But this should not be a big problem, since I doubt that anyone really
uses deeply nested tables with depth level that is significant enough
to impact performance by iterating til the end even if no keys were
found. (Unless someone will do this on purpose or by accident)
Reply to thread Export thread (mbox)