Fork me on GitHub
#clojure-austin
<
2022-04-16
>
staypufd02:04:53

Article I read today that I thought the group may enjoy.

staypufd03:04:24

This one also. CLOS and it’s predecessor Flavors are where Clojure gets it’s Multi-methods from.

dpsutton03:04:47

my coworker cam has written a library that brings back some of the extra stuff that CLOS offers: https://github.com/camsaul/methodical

❤️ 1
camdez21:04:13

I always miss :around methods, so I’m always excited about projects like this but so far haven’t taken the “I need this badly enough that I’ll introduce a ‘non-standard’ thing into my stack” step. But I may well get there.

😁 1
mlimotte21:04:42

It would be a cool built-in language feature, though. It only comes up once or twice in a project, so, yea, would be hard to justify introducing a lib.

staypufd03:04:27

I did a short talk on that lib about 2.5 years ago if I remember right. I use it myself. It’s good.

mlimotte21:04:03

At yesterdays’ get together, a question came up about core.logic. Not sure I have all the detail about what you want to do, @camdez, but here is a start.

(require '[clojure.core.logic :as logic])

(defn keywordo 
  [kw ns-var name-var]
  (logic/all 
    (logic/== (namespace kw) ns-var)
    (logic/== (name kw) name-var)))

(logic/run* [q]
  (logic/fresh [ns nam]
    (keywordo :foo/bar ns nam)
    (logic/== q [ns nam])))

camdez21:04:07

Was going to message you about this! Give me a sec to poke at that and I’ll let you know if it’s exactly what I was thinking of. Thanks!

mlimotte21:04:50

Just edited it. It worked for me, beucase I had extra requires already in my repl, but this one should work for yuo.

dpsutton21:04:36

but this can only go one way. You can’t unify two things into a keyword, right?

(logic/run* [q]
  (logic/fresh [kw]
    (keywordo kw "foo" "bar")
    (logic/== q kw)))

mlimotte21:04:06

that’s the way this fn works. But i think i can do it the other way.

camdez21:04:37

I was going to post the exact same thing as @U11BV7MTK. That’s the particular detail that has me hung up.

camdez21:04:18

And just to be clear, I’m wondering if we can write it as a (single) relation that can be run in both directions.

mlimotte21:04:10

(defn keyword2o 
  [ns nam kw-var]
  (logic/all 
    (logic/== (keyword ns nam) kw-var)))

(logic/run* [q]
  (logic/fresh [kw]
    (keyword2o "foo" "bar" kw)
    (logic/== q kw)))

=> (:foo/bar)

🎉 1
mlimotte21:04:35

☝️ That one goes the other way

dpsutton21:04:08

ok so you have to know which side are the literals and which side has lvars?

mlimotte21:04:59

Yea. I think so.

camdez22:04:42

I wonder if something might be done with lvaro, but that’s technically dipping into the “non-relational” stuff. Not confident whether or not that matters.

camdez22:04:17

^ Like a conde that could check which side was fresh and choose the appropriate path.

mlimotte22:04:22

Possibly wthat plus conde

👍 1
camdez22:04:56

I imagine @U0954HGDQ could make quick work of this.

mlimotte22:04:07

😢 i don’t think lvaro works. That will test if it’s fresh or ground, but it still needs to be an LVar. Not sure this is possible. But I suspect that you don’t really need a bi-directional in order to solve whatever problem you’re trying to solve.

camdez22:04:38

I can’t tell you if this is nuts or not, but it does work:

(defn keywordo3 [kw nms]
  (logic/conde
   [(logic/nonlvaro nms)
    (logic/project [nms]
                   (logic/== kw (keyword (first nms) (second nms))))]
   [(logic/nonlvaro kw)
    (logic/project [kw]
                   (logic/== nms [(namespace kw) (name kw)]))]))

(logic/run* [q]
  (logic/fresh [sol1 sol2]
    (keywordo3 sol1 ["foo" "bar"])
    (keywordo3 :foo/bar sol2)
    (logic/== q [sol1 sol2])))
;; => ([:foo/bar ["foo" "bar"]])

camdez22:04:59

@U0E703ECU I think lvaro and nonlvaro might be poorly named, because they’re actually (per their docs) about whether or not the variable is fresh.

mlimotte22:04:28

Nice! I hadn’t used project before.

camdez23:04:38

This is clearly overkill for this particular problem, but here’s an example of how you could use something like keywordo, leaning on the ability to go in both directions:

(defn keywordo4 [kw ns nm]
  (logic/conde
   [(logic/nonlvaro ns)
    (logic/nonlvaro nm)
    (logic/project [ns nm]
                   (logic/== kw (keyword ns nm)))]
   [(logic/nonlvaro kw)
    (logic/project [kw]
                   (logic/== [ns nm] [(namespace kw) (name kw)]))]))

(defn- rewrite-one-namespace [mapping expr]
  (or (first
       (logic/run* [q]
         (logic/fresh [ns nm ns' kw']
           (logic/pred expr keyword?)
           (keywordo4 expr ns nm)
           (logic/membero [ns ns'] mapping)
           (keywordo4 kw' ns' nm)
           (logic/== q kw'))))
      expr))

(defn rewrite-namespaces [mapping expr]
  (walk/prewalk (partial rewrite-one-namespace mapping) expr))

(rewrite-namespaces
 [["foo.bar" "f"]
  ["core.logic" "logic"]]
 '(let [xs {:foo.bar/baz      1
            :clojure.core/str 2
            :core.logic/run*  3}]
    (:foo.bar/baz xs)))
;; => (let [xs {:f/baz 1, :clojure.core/str 2, :logic/run* 3}] (:f/baz xs))

camdez23:04:08

Given a set of mappings between namespaces, I’m walking a (code) form and rewriting the namespaces in the keywords if a mapping exists.

camdez23:04:57

I ended up rewriting keywordo yet again because I ran into an issue with the namespace / name pair approach—when I was passing in two lvars in a vector, the containing vector was still considered grounded even though neither of the contained lvars was.

camdez23:04:17

I found it a little uncomfortable just passing the literal expr value into core.logic without binding it to an Lvar, but I guess it’s fine? Seems to work fine. I really need to go back and read the underpinnings of minikanren again.

mlimotte23:04:52

Good to see you’re having fun with it. It’s interesting as a demonstration, but has a single concrete answer as opposed to as et of constraints where you want generate one or more solutions. I think the tricky thing about core.logic (or miniKanren) is figuring out when a problem fits that constraint model.

camdez00:04:18

Yeah, that’s fair. My example above is a little too trivial to make any sense at all, but might be sensible in the context of a larger system of rules (à la kibit), but I suppose I’m mostly just be learning on the pattern matching capabilities. My interest in this really just centered on understanding the right way to model these kinds (logical) relations between literal values, so that those relations could be used in any kind of logic programming.

mlimotte01:04:28

Right on. Get familiar and then you’ll have it as a tool when you need it.

👍 1
camdez05:04:19

For anyone (somehow) still following along at home, here are two improved implementations of keywordo:

(defn keywordo5 [kw ns nm]
  (logic/conde
   [(logic/pred ns (some-fn string? nil?))
    (logic/pred nm string?)
    (logic/project [ns nm]
                   (logic/== kw (keyword ns nm)))]
   [(logic/pred kw keyword?)
    (logic/is ns kw namespace)
    (logic/is nm kw name)]))

(defn keywordo6 [kw ns nm]
  (logic/or*
   [(logic/project [ns nm]
                   (if (and (or (string? ns) (nil? ns)) (string? nm))
                     (logic/== kw (keyword ns nm))
                     logic/fail))
    (logic/project [kw]
                   (if (keyword? kw)
                     (logic/== [ns nm] [(namespace kw) (name kw)])
                     logic/fail))]))
These two fully constrain the inputs to valid values, so it functions more like a proper relation and there’s no need to call logic/pred ahead of time or worry about what types you’re passing to it. They also properly handle non-namespaced keywords, so you can do stuff like:
(logic/run* [q]
  (keywordo6 q nil "bar"))
;; => (:bar)
keywordo5 is prettier but makes a bunch of wasteful calls to logic/project, which is the core of what I changed in keywordo6. Starting to wrap my head around this stuff…