Same function implementation for different keyword namespaces in a map

Hi Clojurists,

I’m trying an idea to have a flat map instead of nested maps to store / pass data and I hit one interesting problem that I’ve never had with nested maps.

I have code like this:

(ns myapp.something.foo)

(defn get-foo [a b c]
  {::aaa (+ a b)
   ::bbb (+ a c)
   ::ccc (+ b c)})



(ns myapp.something.bar)

(defn get-foo [a b c]
  {::aaa (+ a b)
   ::bbb (+ a c)
   ::ccc (+ b c)})



(ns myapp.something)

(require '[myapp.something.foo :as foo])
(require '[myapp.something.bar :as bar])

(defn get-foobar [a b c]
  (merge
   (foo/get-foo a b c)
   (bar/get-foo a b c)))



(get-foobar 1 2 3)
; {:myapp.something.foo/aaa 3
;  :myapp.something.foo/bbb 4
;  :myapp.something.foo/ccc 5
;  :myapp.something.bar/aaa 3
;  :myapp.something.bar/bbb 4
;  :myapp.something.bar/ccc 5}

And as you can see, implementation of get-foo looks same and returns different output since all keywords are namespaced. If I didn’t want to namespace the keywords, I would have only one get-foo implementation and I wonder how to make only one implementation for this example. The obvious would be to add 3 functions get-aaa, get-bbb, get-ccc but I hope there is a better option.

Thank you.

Adam

You could potentially have a helper function that constructs get-foo in both cases:

;; in some other namespace
(defn make-foo [ka kb kc]
   (fn [a b c]
      {ka (+ a b)
       kb (+ a c)
       kc (+ b c)}))

;; in both .foo and .bar
(def get-foo (make-foo ::aaa ::bbb ::ccc))

Whether this is a good idea depends on if make-foo is meaningful here. Sometimes repetition is ok.

Thank you. It seems that’s the only good plan.

But I still think that something like merge-with-ns would be a useful function to have. I was thinking about writing it by iterating over all keys and creating a new map with keywords defined by keyword function but that seems like a lot of overhead…

Qualified keywords have semantic meaning. The two get-foo functions are different because the keys they create are different – even if they look the same syntactically.

:: is resolved by the reader to the current namespace – it’s just a shorthand for writing the fully-qualified key name, and it’s a bit dangerous because you can’t move ::a from one ns to another without changing the meaning (the semantics).

1 Like

If I understood you correctly, you want to be able to call a single implementation for get-foo from any namespace, is that correct? If so, you could try the following macro:

(defmacro get-foo
  [a b c]

  (let [ns-sym (ns-name *ns*)
        m      `{:aaa (+ ~a ~b)
                 :bbb (+ ~a ~c)
                 :ccc (+ ~b ~c)}]
    (into {}
          (map (fn [[k v]]
                 (let [k (keyword (name ns-sym) (name k))]
                   [k v])
                 ))
          m)))

The key part is the use of the ns-name function, which returns the name of the current namespace as a symbol, and this is actually the namespace from which you call the macro. (NOTE: The above assumes a, b and c will all be numbers. A more correct solution would assume any arbitrary expression which should be evaluated just once in the code which the macro emits, but for the sake of simplicity and illustrating the main macro functionality I have tried to keep it as clean as possible.)

If you go to any namespace and require-macro this macro, then calling it within that namespace you will get what you are after.

The above serves as a guide as to how you can in theory do this. As to whether you actually want to in practice will depend on what you actually need to use it for.

Alternatively you could just write a function which accepts an arbitrary string as first argument and uses that as the namespace from which to construct your map:

(defn get-foo [ns a b c]
  (let [m {:aaa (+ a b)
           :bbb (+ a c)
           :ccc (+ b c)}]
    (into {}
          (map (fn [[k v]]
                 (let [k (keyword (name ns) (name k))]
                   [k v])
                 ))
          m)))

Calling it thus:

(merge 
   (get-foo "myapp.something.foo" 1 2 3)
   (get-foo "myapp.something.bar" 1 2 3))

will give you the output you provided in your original post.

Not sure I understand, it seems to work no?

Or do you expect each get-foo to overwrite each other?

Anyways, like Sean said, my recommendation is do not use the :: syntax, it’s a bit confusing as it couples code organization with runtime semantics.

Just give explicit keyword names:

Inside myapp.something.foo namespace:

(defn get-foo [a b c]
  {:foo/aaa (+ a b)
   :foo/bbb (+ a c)
   :foo/ccc (+ b c)})

Inside myapp.something.bar namespace:

(defn get-foo [a b c]
  {:foo/aaa (+ a b)
   :foo/bbb (+ a c)
   :foo/ccc (+ b c)})

And if you get tired to typing the entire keyword namespace everytime, you can use the new keyword alias feature:

(ns myapp.something.bar
  (:require [myapp.foo :as-alias foo]))

(defn get-foo [a b c]
  {::foo/aaa (+ a b)
   ::foo/bbb (+ a c)
   ::foo/ccc (+ b c)})

Basically never user ::name, either write the whole thing: :qualifier/name or use an :as-alias and use :: in combination with the alias: ::alias/name.

Then give your keyword a namespace that isn’t tied to a code namespace.

Edit:

You can use ::name if you’re sure the keywords are all for internal private implementation usage within the namespace itself, put if any other namespace will use them, or worse, you export them to a file, a database, return them as a response to some clients, then really you want to make sure you control the qualifier with an alias or by explicitly typing it.

This is pretty good, thank you. I actually ended up with something even simpler:

(defn get-function [ns]
  (fn [a b c]
  {(keyword (str ns) "aaa") (+ a b)
   (keyword (str ns) "bbb") (+ a c)
   (keyword (str ns) "ccc") (+ b c)}))

(def something (get-function *ns*))

(something 1 2 3)

Cool, I didn’t know about :as-alias. Thank you.

I don’t get this. Did you mean?

(defn get-foo [a b c]
  {:myapp.something.foo/aaa (+ a b)
   :myapp.something.foo/bbb (+ a c)
   :myapp.something.foo/ccc (+ b c)})

PLEASE IGNORE THIS POST AS IT CONTAINS INCORRECT INFORMATION.

Hold on a second, there’s a typo here: not :as-alias but an :as alias, specifically in the ns :require vector, example:

(ns my.name.space
  (:require
   [myapp.something.foo :as foo]))

Within my.name.space not only can you write foo/some-func to refer to a function defined in myapp.something.foo, you can also write ::foo/some-keyword which is expanded by the reader to :myapp.something.foo/some-keyword. NOTE the double-colon before foo, this is crucial, otherwise a single colon will just result in :foo/some-keyword. This is an extremely useful feature which saves you form having to type out the full name of a required namespace to qualify keywords namespaced to that namespace.

I’m not sure what you think is a typo here?

The key thing about :as-alias is that it doesn’t load the ns so it doesn’t require the file to actually exist: you are just creating an alias that :: can expand.

:as will load the ns so it requires an actual file to exist (and, yes, it also creates the alias that :: can expand).

1 Like

Yep, that’s exactly what I found when I googled as-alias. It’s a great new feature.

1 Like

My utter bad, I had no idea there was an :as-alias as well as an :as. That’s also an extremely useful feature, good to know it exists.

It gets a bit confusing, so I will say: :<prefix>/<name> for a keyword, and when I say namespace I’m referring to a (ns <namespace>) ok?

So what I meant is you use the same keyword prefix in both namespace. That prefix does not need to map to an actual namespace, so it can just be :foo/ in both namespace.

So now if you call get-foo in either namespace, you get back the same keyword.

Overall though, my general advice is don’t couple the prefix of a keyword with the namespace of the file it is in, unless the keyword is only used within that file and nowhere else. You can do that by either typing a fully qualified keyword every-time with a prefix of your choosing, or use the new alias feature which saves you a bit of typing if your preferred prefix is very long.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.