Fork me on GitHub
#clojure
<
2020-07-26
>
vlaaad10:07:47

I want your opinion! I've been thinking about writing something like this as a -main for cli program (as opposed to using tools.cli):

(defn -main [& opts]
  (let [[f & args] (map (fn [str]
                          (try
                            (let [form (read-string str)]
                              (cond
                                (qualified-symbol? form) @(requiring-resolve form)
                                (symbol? form) @(ns-resolve 'my.ns form)
                                :else form))
                            (catch Exception _ str)))
                        opts)]
    (apply f args)))
What it does, is basically reads all args, resolves symbols to what they refer, and invokes arg list as a function. Unqualified symbols are resolved in ns that defines main (my.ns in this example) Now what's left is making a bunch of defs that receive varags for actual functionality. Examples that show how args are evaluated: clj -m my.ns clojure.main/repl :eval macroexpand - starts a repl that macroexpands instead of evaluating. clj -m my.ns prn :host 127.0.0.1 :port 8080 - prints :host "127.0.0.1" :port 8080 Real-world example is defining custom tasks as normal functions:
(defn average
  "Calculates number average"
  [& nums]
  (println (/ (apply + nums) (count nums))))
This now can be used from the cli:
$ clj -m my.ns average 1 4 10
5
Command help that prints docstring:
(defn help [f]
  (println (:doc (meta (resolve (symbol (Compiler/demunge (.getName (class f)))))))))
Usage:
$ clj -m my.ns help average
Calculates number average
Yes, it's not unix-style args, but it is very much clojure style. Yes, this entry point can do A LOT of stuff it wasn't asked for, but what it was asked for it does super easy. Question: is this too insane? What do you think?

potetm14:07:24

why would it be insane?

potetm14:07:38

you could always augment it down the road with: 1. Stringified commands via a dispatch map (e.g. myns instead of my.ns ) 2. per-command flag parsing (via tools.cli)

potetm14:07:07

this is exactly what we do at cisco threatgrid fwiw

vlaaad14:07:44

I thought it to be weird because of how hacky this feels

vlaaad14:07:20

But it's so small and powerful! This approach already has per command flag parsing, using simple Clojure functions! Defaults support out of the box with destructuting. I felt it's strange that I've never seen any similar clojure command line interfaces

Alex Miller (Clojure team)22:07:28

something with similar intent is actually in the new version of clj (dev version) I released last week. have not announced with details yet

Alex Miller (Clojure team)22:07:34

I have so much to say about it, can't possibly summarize here (and it's actually scaled down from some other things)

Alex Miller (Clojure team)22:07:15

Rich and I have actually been working on a variety of ideas in this area for several months, so fortuitous timing! :)

parrot 6
vlaaad05:07:06

Woah, that's wonderful!

Ivan10:07:39

hello everyone, I've been struggling with a question and wanted to share my thoughts with you, and maybe you can guide me through this. This is not directly tied to clojure (although Rich has made a comment on this general question), it is a more generic question. So, here it goes: The question is whether keys with nil values should be treated differently from keys that are missing.. To give more context, this question came up when thinking how parsing configuration should behave and when should default values apply. I assume a map that represents a configuration. The keys are the options, the values represent the configuration-option value. The question then becomes: Should missing configuration options be treated differently from defined configuration options that have nil as the value. To give you my thoughts on this: I understand that there are cases where a key being defined matters. However, for the majority of the cases, which includes parsing configuration, it should not. Let me answer why: I perceive nil (or NULL, or None, etc) as a placeholder value for nothing. It exists to indicate that a thing (a variable) is defined, but it is uninitialized; its value is unset - it is set to nothing. From that perspective nil always means unset. And if something is unset (whether this was done by omission, or explicitly by setting it to nil) then a default should apply (whenever a default is defined and makes sense of course). The main thing here is that nil means unset. One argument against this would be a configuration option handler that behaves in a specific way when set to nil, which it would not if the option was omitted. I find two problems with this: 1. it is unintuitive, and 2. nil should not be used to indicate a value (it means nothing), instead a proper value should be used (whether it is a keyword, an enumeration, etc) - if the value has meaning, it should reflect and communicate that. Does that make sense? What do you think on this subject? Do you have any relevant resources around this? (semi-)related: - https://corfield.org/blog/2018/12/06/null-nilable-optionality/ - https://ask.clojure.org/index.php/8387/how-to-avoid-nil-values-in-maps

valerauko11:07:18

imo you've found a pretty good answer yourself. i try to use nil/none etc as little as the language allows me too. so yeah, it makes sense and i agree.

vlaaad11:07:18

> nil should not be used to indicate a value so "person has no ssn" is {:person/ssn :ssn/none} instead of {:person/ssn nil}? I like it

sveri14:07:22

This {:person/ssn :ssn/none} is a basically the "NullObject" or "EmptyObject" Pattern. I personally would prefer to not have that key set at all. The problem with nil is, it often happens that it gets a second meaning, like some error occured, or, the programmer did not know what to do in that special case and returns nil instead. So yea, I am also on the "avoid nil as much as possible" side.

💯 6
jaihindhreddy16:07:49

"defined but uninitialised" and/or "is unset" sounds like a place-oriented mindset to me. If we instead take an information-centric view, {:email nil} is wrong, and I'd try to limit this kind of a thing to the very edges of the system. I agree with sveri that {:email :email/none} or {:email :gdpr/redacted} doesn't seem right either. The key should just be absent IMHO. That being said, I've never used Clojure in-anger (read: for work), so take this with a pinch of salt 😄

Ivan17:07:37

yes, @U883WCP5Z I agree too, that the key should be absent. However, when defining a configuration, it may be there. What should happen then? And by my reasoning on nil, I expect the program to behave as if the key was not defined. In other words, I do not differentiate between existence of a key that is set to nil and absence of a key, as, when I ask to get the value in both cases it will be nil. Considering nil as unset, then I can proceed to apply the same behaviour. From the perspective of information-centric or place-oriented mindset: this is based more on the information I have (a nil value) and less on the structure of information object (a map with key in place). I suggest that looking at the value (the data) at hand seems better than looking at the structure (the place). In a similar way, we can imaging the configuration as a function with the options as params. We call the function and in the process we pass arguments that match the params with values.

(defn compose_config [& {:keys [option-x option-y option-z]}] ...)
When we do not want to set a configuration option we leave it out, we do not pass a value. This will result in the corresponding param to be nil. When we pass a value of nil for a configuration option, then the function cannot actually know that this was set explicitly. Thus, in both cases the handling will be the same.
(assert (= (compose_config :option-x "foo" :option-y "bar" :option-z nil) (compose_config :option-x "foo" :option-y "bar")))

Drew Verlee17:07:59

If the function inputs don't meet the contract then the behavior is undefined. you can short circuit and return an error to the caller to catch this before that point though. In your case, a spec on the config should inform caller's that nil isn't a valid value.

3
Drew Verlee17:07:52

Anything else starts to assign business meaning to nil, which will be hard to maintain

Ivan17:07:41

but now you're talking about validation, which IMO is a separate step. The steps are: - get config (read/load/unserialize/etc) - set defaults ("parse" to get the different options) - validate - .. then use safely with the above function in mind, "get config" is achieved by call the function and passing arguments. "set defautls" is the step that I am discussing. And the question is whether a nil value that was passed in, or a nil value for an argument that was not passed in should be handled differently. I am arguing that it should be handled in the same way because nil would mean that the option is unset. "validate" is then invoked. The validation will actually check the types and structure of the compose configuration object/map. Spec is relevant here. > Anything else starts to assign business meaning to nil, which will be hard to maintain I think that what I described agrees with that

Drew Verlee17:07:04

Validation would come before setting defaults. The function has a contract, I'm suggesting it's best to not have that include accepting nil values and trying to guess what a caller meant. The issue isn't what you can do with the nil, it's what did the caller mean by it.

Drew Verlee17:07:25

Summoning Rich's words: "I'm worried about fred sheppy, is he missing?". When he covered this topic in his speculation talk.

Drew Verlee17:07:56

You can reach the same goal with what your suggesting, but the seperation of concerns isn't as clear.

Drew Verlee17:07:20

You can also validate at ever stage.

Ivan19:07:26

> Validation would come before setting defaults. How can this happen? a default will take the place of a configuration option value that is not set. How can you validate a configuration before setting values to the options? If you do that, then you allow nils. What is the point here?

Ivan19:07:46

> The function has a contract, I'm suggesting it's best to not have that include accepting nil values and trying to guess what a caller meant. the function says that it will compose a configuration object from the given configuration options. Some configuration options are optional. The function accepts "named arguments". It does not say that all arguments must be set.

Ivan19:07:31

> but the seperation of concerns isn't as clear can you expand on this more? what are the suggested step that you would put in place?

seancorfield19:07:30

Destructuring sort of blurs the lines because you can no longer test contains?, you can only test for the value itself and assume that it was missing if the binding is to nil. If you follow the (best) practice of not having nil values in hash maps, that assumption is very reasonable -- and Clojure leans heavily on nil-punning so nil results from function calls are generally allowed to indicate "absence of result".

Drew Verlee19:07:36

(-> args validate merge-defaults) There is nothing that prevents validation before merging defaults. But this is a side topic to the question you raised. What does nil mean? As you suggest, it's not a good thing to slot into any keys value. You can write a spec that says this key is optionally, but if you provide it, the value must be not nil. I'm agreeing it's a delima, I'm saying your system can't fix it. Garbage in garbage out, you can make a guess that they meant unset, but if the next caller means something else then it's going to fail. If it's acceptable for your system to assume and trim them out then it is, but that's a particular of your system and not a universal expectation.

seancorfield19:07:14

The end result of the thinking in my blog post was next.jdbc.optional which produces hash maps that do not contain keys whose corresponding values in the database were NULL but with interop against SQL, nil has an important meaning when inserting/updating because you need something to stand in for the value that indicates NULL and because we're using JDBC -- Java libraries -- nil in Clojure and null in Java are the same, so in those hash maps: the setter equivalent and the by-example where clause equivalent have to contain nil values sometimes.

Drew Verlee19:07:12

@U04V70XH6 makes sense because postures requires those slots be set by the caller right? It won't insert a nil by default? It's not a key value relationship, it's a tuple of a set size.

seancorfield19:07:06

@U0DJ4T5U1 For inserts, it is less of an issue if a column has a default of NULL, but for updates you must have a way to signify that you want to replace a column's current value with NULL.

emccue03:07:20

I tend to think of it somewhat like undefined in javascript. If something has a key with a nil, that means we know that is not available but if there is no key we don't know the actual value at that time. If that makes sense