Skip to content
marick edited this page Feb 28, 2013 · 3 revisions

I'm doubtful this feature is actually a good idea (over just using maps). I'm leaving it in the documentation because it does work, and maybe someone likes it. (If that someone is you: tell me.)

##Short version

Metaconstants can be assigned values that are accessed as if the metaconstant were a map or record:

(fact 
  (:a ..m..) => 1
  (provided ..m.. =contains=> {:a 1}))

You can also use these data prerequisites with fact-wide-prerequisites:

(fact "`keys`, `values`, and `contains?` work on metaconstants"
  (background ..m.. =contains=> {:a 3, :b 4})
  (keys ..m..) => [:a :b]
  (vals ..m..) => [3 4]
  (contains? ..m.. :a) => truthy
  (contains? ..m.. :c) => falsey)

Use data prerequisites when you want to describe code that acts on containers that contain at least particular key-value pairs. Like all metaconstants, data prerequisites are used when you want to say just enough to make facts true and leave other information to be described later or or elsewhere.

##The longer version

Languages like Ruby and Smalltalk make it difficult for objects to rummage around in the internal data of other objects. Instead, instance variables are revealed with accessor functions. You could do the same in clojure. Given a map like {:one 1, :two 2}, you could provide accessor functions like one and two, defined like this:

(def one :one)
(def two :two)

That's not idiomatic, though. (:one thing) and (one thing) convey different messages to the reader. The first says "This is data that already exists. It's cheap to get." The latter at least suggests that the data might need to be calculated, fetched across the network, etc. Indeed, given the pervasiveness of the idiom, seeing a function access for something that seems like it could just be a keyword lookup raises suspicions that something weird must be going on in that function.

Midje supports this idiom by letting you describe, in a prerequisite, data that a metaconstant contains. That's written like this:

(fact
  (full-name ..person..) => "Brian Marick"
  (provided
     ..person.. =contains=> {:given-name "Brian", :family-name "Marick"}))

Notice that this prerequisite still lets us put off decisions like what else a person contains or whether a person is a map or a record.

Metaconstants are not exactly either maps or records

Suppose you have this situation:

(defn f [person]
  ;; code you're writing to make the below fact true
)

(fact 
  (f ...person...) => truthy
  (provided 
    ...person.... =contains=> {:age 33}))

Here's what you can rely on within f:

  • The age can be retrieved in any of three ways:

      (:age person)
      (get person :age)
      (person :age)

    Note that by using the last form, you're suggesting that person probably is not a record, since defrecord "out of the box" doesn't produce instances that can act as functions.

  • When metaconstants are turned into strings via str, pr-str, or functions that use them, the strings are their names, something like ...thing....

  • keys, values, contains?, map, and reduce all work in the normal way.

  • assoc and merge do not work. (What would the name of the resulting metaconstant be?)

  • Two metaconstants with different names are never equal. Metaconstants are supposed to be partial descriptions of the real structure. That's why the arrow you use to create them is called =contains=>. Once this fact checks out, what will happen when real objects with more content get passed to the function? Remember: in Clojure, unless you go to some lengths to avoid it, equality checks compare all fields, not just the ones you mentioned in your facts.

  • Equality on metaconstants has been defined so that a data-full metaconstant can be compared to a symbol and compare equal if the names are the same. That frees you from most worries about quoting:

    (fact
       (f ...thing...) => '(1 2 ...thing...)
       (provided
          ...thing... =contains=> {:a 1, :b 2}))

Odds and ends

  • You can describe a metaconstant's contents twice:

    (fact
      (+ (:a ..m..) (:b ..m..)) => 3
      (provided
        ..m.. =contains=> {:a 1}
        ..m.. =contains=> {:b 2})

    The contents are merged together. (As with merge, in case of conflicts, the later value takes precedence.)
    Prerequisite metaconstants described in prerequisite forms are also merged with those from provided clauses. The provided keys take precedence.

  • You can describe both a metaconstant's contents and how it's used in functions:

    (fact
      (fullname ...person...) => "Mr. Brian Marick"
      (provided
        ...person... =contains=> {:given "Brian", :family "Marick"}
        (salutation ...person...) => "Mr. "))
Clone this wiki locally