Generate a copy of schematized function with custom return value

24 views
Skip to first unread message

Matthias Diehn Ingesman

unread,
Apr 11, 2016, 8:22:07 AM4/11/16
to Plumbing and Graph: the Clojure utility belt
Hi.

I am in a situation where I think my (lack of) macro skills are blocking my progress when it comes to using the nice schema annotations I have; so I ask you here for advice.

Let me provide some background. In my code base all functions accessing the database are implemented as protocols, where every protocol function implementation is simply a call to a function annotated using schema.core/defn. The reason for using protocols is testing, i.e. I want to create a testing database object that implements all the protocols, but with fixed return values. All this works as expected.

The problem occurs when I want the test database object to implement the protocols and keep the validation logic of the functions wrapped by the procols. I can't just do this:

(extend-type TestDB
  my.ns/Protocol
  (afn [some args]
    value))

Since calls to afn on the test database will never hit the functions that are implemented with schema.core/defn.

My strategy was thus to implement a macro to copy the schema from a function onto the parameters and body of another function, such that I could write:

(extend-type TestDB
  my.ns/Protocol
  (afn [some args]
       (copy-schema my.ns/validated-function [some args] value)))

This would validate all arguments and return values when afn is called on the test database object.

The copy schema macro is trouble though. Here is what I have implemented:

(defn copy-schema*
  [source-fn parameters & body]
  (let [fn-schema (schema.core/fn-schema (eval source-fn))]
    `(schema.core/fn :- ~(:output-schema fn-schema)
       ~(into [] (mapcat (fn [p s]
                           [p :- (:schema s)])
                         parameters
                         (first (:input-schemas fn-schema))))
       ~@body)))

(defmacro copy-schema
  [source-fn parameters & body]
  (apply copy-schema* source-fn parameters body))

copy-schema works when the schema is defined in the same namespace, and is not a map schema. Examples:

(schema.core/defschema S1
  (schema.core/enum "one"))

(schema.core/defschema S2
  schema.core/Int)

(schema.core/defschema S3
  {:a schema.core/Int})

(schema.core/defn s1-impl :- S1
  [a :- S1]
  a)

(def s1-impl-copy (copy-schema s1-impl
           [a]
           (* a a)))
;=> <working copy>

(schema.core/defn s2-impl :- S2
  [a :- S2]
  a)

(def s2-impl-copy (copy-schema s2-impl
           [a]
           (* a a)))
;=> <working copy>

(schema.core/defn s3-impl :- S3
  [a :- S3]
  a)

(def s3-impl-copy (copy-schema s3-impl
           [a]
           (* a a)))
;=> <crash with ClassNotFoundException on current namespace>

Since I don't know whether this is schema-related or not, I figured asking the question here first is not a bad choice.

Can anyone explain what I am doing wrong, to make it crash when on map schemas?

Kind regards,
Matthias

Jason Wolfe

unread,
Apr 11, 2016, 8:40:32 AM4/11/16
to Matthias Diehn Ingesman, Plumbing and Graph: the Clojure utility belt
Macros are tricky business; from a quick peek at this, I'm not sure
exactly what's going wrong. My first guess would be that you're
passing an evaluated schema where a form that produces a schema would
typically be expected, but I'm not sure (and I'm not sure how you
would best work around that).

I can think of two simpler options that don't involve macros, however
(although I understand they may be less than ideal).:

1. Define schematized functions that call your protocol methods, and
call those everywhere (considering the protocol to be an
implementation detail). That way the schemas apply regardless of
which protocol implementation you use.

2. Manually validate the input and output schemas in your test
versions using s/validate. You could also change your implementation
of copy-schema to do this, which should be much easier to get right
than trying to emit another schema macro declaration inside.

Best,
Jason
> --
> You received this message because you are subscribed to the Google Groups
> "Plumbing and Graph: the Clojure utility belt" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to prismatic-plumb...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Matthias Diehn Ingesman

unread,
Apr 11, 2016, 9:08:48 AM4/11/16
to Plumbing and Graph: the Clojure utility belt, matd...@gmail.com
You pointed me in the right direction I think; at least my preliminary tests show that it works. Thanks!

Your first guess was spot on, the output schema that I was attaching was already evaluated. I looked at the implementation of defschema to see how I would go building a proper internal schema using that, and came up with:

(defn copy-schema*
  [source-fn parameters & body]
  (let [fn-schema (schema.core/fn-schema (eval source-fn))]
    `(schema.core/fn :- ~(vary-meta
                           (schema.core/schema-with-name (:output-schema fn-schema) 'name)
                           assoc :ns '(ns-name *ns*))
       ~(into [] (mapcat (fn [p s]
                           [p :- (:schema s)])
                         parameters
                         (first (:input-schemas fn-schema))))
       ~@body)))


This probably still has room for improvement, but so far it seems like it is building the schema correctly.

I appreciate your alternatives, and agree that both are viable. I like your first suggestion best, and will have to think hard on whether it makes sense to spend the time transitioning our code base to that style.

Thanks again for the hint. :-)

Matthias

Matthias Diehn Ingesman

unread,
Apr 11, 2016, 9:32:38 AM4/11/16
to Plumbing and Graph: the Clojure utility belt, matd...@gmail.com
Well, it doesn't work for simple schemas now. This is probably the same issue I have seen other places as well, when using defschema for aliasing things like schema.core/Int.

Best,
Matthias

Jason Wolfe

unread,
Apr 11, 2016, 9:51:31 AM4/11/16
to Matthias Diehn Ingesman, Plumbing and Graph: the Clojure utility belt
No problem! In the simple case, you might be able to just check if
it's an IMeta and if not, don't bother messing with the metadata. Or
you can always wrap in an `s/named` or something and apply metadata to
that.

Cheers,
Jason

On Mon, Apr 11, 2016 at 7:17 PM, Matthias Diehn Ingesman
>>> > email to prismatic-plumb...@googlegroups.com.
>>> > For more options, visit https://groups.google.com/d/optout.
>
> --
> You received this message because you are subscribed to the Google Groups
> "Plumbing and Graph: the Clojure utility belt" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to prismatic-plumb...@googlegroups.com.

Matthias Diehn Ingesman

unread,
Apr 13, 2016, 7:24:56 AM4/13/16
to Plumbing and Graph: the Clojure utility belt, matd...@gmail.com
Just to let you know that I have opted for making the schema defined functions thin wrappers around the protocols, since that seems like the path of least resistence as well as the best design. :-)

Best,
Matthias
>>> > For more options, visit https://groups.google.com/d/optout.
>
> --
> You received this message because you are subscribed to the Google Groups
> "Plumbing and Graph: the Clojure utility belt" group.
> To unsubscribe from this group and stop receiving emails from it, send an

Jason Wolfe

unread,
Apr 13, 2016, 11:26:50 AM4/13/16
to Matthias Diehn Ingesman, Plumbing and Graph: the Clojure utility belt
Thanks for the follow-up -- sounds like a good call :).

Cheers,
Jason

On Wed, Apr 13, 2016 at 5:09 PM, Matthias Diehn Ingesman
>> >>> > email to prismatic-plumb...@googlegroups.com.
>> >>> > For more options, visit https://groups.google.com/d/optout.
>> >
>> > --
>> > You received this message because you are subscribed to the Google
>> > Groups
>> > "Plumbing and Graph: the Clojure utility belt" group.
>> > To unsubscribe from this group and stop receiving emails from it, send
>> > an
>> > email to prismatic-plumb...@googlegroups.com.
>> > For more options, visit https://groups.google.com/d/optout.
>
> --
> You received this message because you are subscribed to the Google Groups
> "Plumbing and Graph: the Clojure utility belt" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to prismatic-plumb...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages