Rationale of behavior when invoking composite data type as 0-arity function

110 views
Skip to first unread message

dieter.v...@gmail.com

unread,
Nov 21, 2021, 9:22:47 AM11/21/21
to Clojure
Hello,

It seems to be a design decision that 0-arity invoke of a composite data type gives an ArityException: the composite data type does not implement the IFn when no arguments are given.
Is there a certain design decision behind this behavior? (or a certain use-case)


repl> ;composite data type evaluates to itself
repl> {:a 1 :b (hash "word")}
{:a 1 :b -304538205}
repl> '{:a 1 :b (hash "word")}
{:a 1 :b (hash "word")}
repl> (def mydata '{:a 1 :b (hash "word")})
repl> mydata
{:a 1 :b (hash "word")}
repl> ;composite data type implements IFn for its keys
repl> (mydata :b)
(hash "word")
repl> ; there is no '0-arity' implementation of IFn for composite data type
repl> ({})
... (ArityException)...
repl> (mydata)
... (ArityException)...
repl> ; instead i have to type eval
repl> ((eval mymap) :b)
 -304538205

I know its only 4 letters and a space extra, but software composition is supposed to avoid code duplication and perhaps the idea makes sense that invoking a map without arguments evaluates it... Hence the question about the choice made for the current behavior.

A possible small workaround 
(defrecord qid [qid]                                               
     clojure.lang.IFn                                                 
     (invoke [this] (eval qid))
But expect this to throw alot of bugs: this record is not the same simple map.
(issues with other protocols, reducers, transducers and much more I don't know of.)

I hope this is the right google group to ask this question.
kind regards,
Dieter

James Reeves

unread,
Nov 21, 2021, 10:51:37 AM11/21/21
to 'EuAndreh' via Clojure
On Sun, 21 Nov 2021, at 2:22 PM, dieter.v...@gmail.com wrote:
repl> (mydata)
... (ArityException)...
repl> ; instead i have to type eval

repl> ((eval mymap) :b)
 -304538205

I know its only 4 letters and a space extra, but software composition is supposed to avoid code duplication and perhaps the idea makes sense that invoking a map without arguments evaluates it... Hence the question about the choice made for the current behavior.

Evaluating a map is a fairly costly and uncommon use case. I don't believe I've ever had cause to eval an entire map, or if I have it can't have been more than a few times in the past 12 years of using Clojure. If I call a data structure with no arguments, it is most likely to be a mistake, so my expectation would be for the compiler or runtime to raise an error.

Syntax sugar is generally reserved for very common operations. Each piece of sugar that is introduced is something extra the reader needs to learn. Moreover, if they see (eval x) it is clear an evaluation is taking place; if they see (x), it is not obvious at all.

Not only is performing a lookup on a map a very common operation, treating a map as a function makes intuitive sense. A function is a mapping between two sets; a map is also a mapping between two sets. It's logical that they might share an interface.

--
James Reeves






Brent Millare

unread,
Nov 21, 2021, 9:38:57 PM11/21/21
to Clojure
I'm curious why you are saving hashmaps that have clojure code within it with the intention of evaluating this code as embedded in the hashmap? What is the use case? Are you trying to delay evaluation? Regardless, eval always incurs a cost and should generally be avoided if you can use "runtime" techniques instead. Is the embedded code trusted?

Best,
Brent

Dieter Van Eessen

unread,
Nov 24, 2021, 2:52:43 PM11/24/21
to clo...@googlegroups.com
Hello Brent,

The use case I had in mind was to keep a map readable during development. Take a simple map: {:type QTDIR :path (hash "a string")}. It's easier to play with this data if evaluation of certain symbols and functions is delayed.

Thanks you both for your answer,
kind regards,
Dieter




--
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 a topic in the Google Groups "Clojure" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clojure/d16Ow0MvhPU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojure+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/clojure/ff296de8-ce0f-4798-a1cc-cd3e4b38c631n%40googlegroups.com.

Brent Millare

unread,
Nov 25, 2021, 9:35:09 AM11/25/21
to Clojure
Ok so I think taking a step back, I think their is some complecting going on here. I don't see how you see "that invoking a map without arguments evaluates it" as a generalization. I believe Rich's intention behind map's, set's, and vector's being callable is that they naturally behave as primitive (mathematically) functions, which map a key space to an output space. So first, there aren't any natural meanings for calling a map with zero arguments. (If anything, I think it should return nil. Because sometimes we would call functions using apply like (apply f [a b c]), and sometimes that list is empty, like (apply {} []). This is just like how (+) evaluates to 0. But I digress...).

What you are generally talking about is controlling code evaluation. You are specifically storing code within the values (and potentially keys) of the map. Really, this code is just lists, aka data. It is really important to keep the phases of evaluation separate, otherwise this makes macro writing very confusing (and coding in general). What you are suggesting complects calling a map (keys to values), with evaling a map (recursively eval any non quoted lists). These are separate actions/ideas. When we do want to operate in different phases, then we really do want to call eval explicitly as this is a "dramatic" action. Eval takes literal data and pipes them through unless they are lists, where it treats the first arg as a function/special-form, and the output can be very different from the input. In contrast, often transformations on maps result in other maps. My take here is, again, this should be explicit. In your intended behavior example (({}) :b), it is calling eval behind the scenes. This is very hidden and leads to very different performance implications. My take is you are suggesting reducing code size but not actually addressing duplication, and in the process making special behaviors less intuitive. (One other thing is what you suggest doesn't blend nicely with -> as you'd still have to call .invoke at the end instead of just calling eval.)

-----

To support your cause, however, I think there are many tools to help you with code manipulation and code analysis during runtime.

If you want your code to work as is but have code analysis phases, you can use macros to insert code analyzing phases without impacting the evaluation and runtime implications of the code itself.

For example:
(def code-size (atom 0))
(defmacro count-code [some-code]
  (reset! code-size (count (str some-code)))
  some-code)

(count-code {:a :b}) ;; => {:a :b}
@code-size ;; => 7

From there, you can write code analysis walkers as functions (not macros) and use them at runtime with explicit quoted forms at the repl, or insert them into your macros for analyzing "production" code.

dieter.v...@gmail.com

unread,
Dec 5, 2021, 9:55:44 AM12/5/21
to Clojure
Hello Brent, thank you for the response. You're right, better keep a clear distinction between invoking and evaluating.
Reply all
Reply to author
Forward
0 new messages