clara-rules icon indicating copy to clipboard operation
clara-rules copied to clipboard

Defining rules with lexical vars?

Open brunoV opened this issue 10 years ago • 4 comments

defrules does not close over lexicals, so this fails:

(ns nag.test-locals
  (:require [clara.rules :refer :all]))

(defrecord Message [message])

(let [f #(println "It worked!" %)]
  (defrule foo
    [?f <- Message]
    =>
    (f ?f)))

(-> (mk-session)
    (insert (->Message "hi!"))
    fire-rules)

;;; CompilerException java.lang.RuntimeException: Unable to resolve symbol:
;;; f in this context, compiling:(null:12:1)

It seems to me that I may be trying to do something unidiomatic. I'm writing a toy notification system much like the sensors one in the clara-examples project. I want to package it as a library that will be used by a larger project, and I want it to be possible to customize the actions that are carried out on each notification.

I could ask the users to write Clara rules on their namespaces and then call mk-session with all the relevant IRuleSources. But I would prefer to keep Clara as an implementation detail, and have a more standard API in which callers register callbacks.

Is this the wrong way to about things? Would you say the use case is legitimate?

brunoV avatar Jun 25 '14 13:06 brunoV

As a clarification, this version works:

(ns nag.test-locals
  (:require [clara.rules :refer :all]))

(defrecord Message [message])

(def f #(println "It worked!" %))

(defrule foo
  [?f <- Message]
  =>
  (f ?f))

(-> (mk-session)
    (insert (->Message "hi!"))
    fire-rules)

;;; It worked! #test_locals.Message{:message hi!}

brunoV avatar Jun 25 '14 13:06 brunoV

I hadn't anticipated this usage pattern, so defrule does simply ignore the surrounding context. Fortunately it should be a simple change to support this behavior. The underlying logic in clara.rules.dsl/parse-rule captures the env, so we just need to update defrule to pass it down. (Right now, defrule simply passes an empty env to parse-rule, but we should be able to safely grab &env in the macro itself.)

I'll probably have time tonight to make this change. Placing def* in blocks isn't common but there are legit use cases to do so, so we should behave in an unsurprising manner here.

Another option to consider for your particular use case is to simply place the user-provided callbacks in a fact matched by the rules. For instance, if you had a "Callbacks" type that contained user-provided closures. Your library could insert that Callbacks instance into the working memory, match it on the left-hand side, and invoke the callbacks on the right-hand side. I'm not sure if this makes sense for your use case or not, but it might be simpler to express their needs in terms of facts in the working memory as opposed to the closed environment around rules. (Just a thought...I don't know the problem space well enough to say whether this is better than what you're doing now.)

rbrush avatar Jun 25 '14 15:06 rbrush

That's a great suggestion, @rbrush, and I think it would be sufficient for my needs. Funny, I had another problem that I struggled with for a while and I solved it somewhat elegantly by adding a new fact type. It seems that I still need to get used to the rule-based way of thinking :)

I agree with you that calling def* within a block is not common -- earlier I was looking for something that would return an anonymous rule. The closest I got was using clara.rules.dsl/parse-rule, but it seemed a bit low level and it still had the dynamic vs. lexical scope issue.

Maybe it makes sense to have a function in the main clara.rules namespace that does something like:

(let [lhs (...)
      rhs (...)
      rule (mk-rule {:lhs lhs :rhs rhs})]

  (-> (mk-session [rule] 'some.other.namespace)
  ...))

and behaves properly with regards to lexical scope.

However, I would ask you to consider holding off on adding anything or making any changes until you're convinced that it's the right thing. As of me, I think that adding a Callback type is the idiomatic thing to do here so I'm covered :)

Thank you for writing Clara by the way. I'm impressed by it, I'm planning on using it for my projects and I hope to contribute to it some time.

brunoV avatar Jun 25 '14 15:06 brunoV

Having a mk-rule in the main namespace makes sense. It would be a pretty simple to add on top of the underlying API. (That would only be supported in Clojure, not ClojureScript, since creating rules and sessions on the fly depends on eval, but that's probably okay.)

I'll think about the best path forward a bit. It sounds like you have an approach for your current need at least.

And I appreciate the kind words on Clara! It's been a fun project.

rbrush avatar Jun 27 '14 01:06 rbrush