Вопрос

Is there an elegant way to create and use records with a mix of constant and probabilistic fields. I would like to be able to do something like the below, where "sampler" is a function that returns a sample from some distribution. the goal is to make it transparent to the user whether the filed accessed is constant or probabilistic.

> (defrecord Stat [val1 val2])

> (def s1 (Stat. 1 sampler))

> (:val1 s1)

> 1

> (:val2 s1)

> 4

> (:val2 s1)

> 2
Это было полезно?

Решение

Having keyword lookup behave in a way other than simply looking up a fixed value in a map is possible, for custom deftypes, but I strongly recommend against it. Not only is it a lot of work, it will defy the expectations of everyone reading your code.

However, a slight adjustment to your requirements yields an easy solution: Instead of (:val1 x), write (x :val1). Now x can simply be a function, which lets the input it receives dictate its behavior:

user> (defn stat [distributions]
        (fn [sample]
          ((get distributions sample))))

#'user/stat
user> (def s1 (stat {:val1 (constantly 1)
                     :val2 #(rand-int 5)}))
#'user/s1
user> (s1 :val1)
1
user> (s1 :val2)
3
user> (s1 :val2)
4

Другие советы

The lookup behaviour of a record defined by defrecord can't be changed. deftype offers more control, but it takes a bit of work to implement all of the correct interfaces. Realizing that, potemkin lets you easily define a map-like thing with custom behaviour:

(use '[potemkin :only [def-map-type]])

(def-map-type Stat [val1 val2]
              (get [_ k default-value]
                   (case k
                     :val1  val1
                     :val2  (val2)
                     default-value)))

(def s1 (->Stat 1 #(rand-int 10)))

(:val1 s1) ; => 1
(:val2 s1) ; => something in [0, 9]
(get s1 :val2) ; => something in [0, 9]

You can also define assoc, dissoc, and keys however they make sense for your data.

I would do this by using a protocol:

(defprotocol Sample
  (sample [m]))

Then extend the protocol to whatever structures you want to sample from in the following way:

  • Maps, records (and any other assciative data types): return the same type with the same keys and the result of calling sample on each value
  • Sets: select an element from the set at random
  • Numerical types (java.lang.Number): return the value unchanged
  • Function types (IFn): call the function with 0 arguments
  • Anything else (java.lang.Object): return the value unchanged (or an error, if you like...)

Now you can do stuff like:

(sample [#{1 2} (partial rand-int 10) {:a 1 :b #{5 6}}])
=> [2 7 {:a 1 :b 6}]

Advantages of this approach:

  • You can define an immutable "schema" for producing samples
  • After the sample is created, it is a pure immutable Clojure data structure (this is good since you don't want the result to change each time you read it!)
  • You can easily extend it to new types of random sampling in the future by extending the protocol further, or by creating new sampling functions
  • It's easy to compose with higher-order functions. For example you can do (take 1000 (repeatedly #(sample my-schema))) to get 1000 samples.

If you want to get more elaborate, you could also pass a seed as an additional optional argument to the sample function. This would enable reproducibility of samples if you do it correctly (this is very useful for testing, and it makes (sample x seed) work as a pure function).

It is fundamental to the basic Clojure datatypes that they are pure - they are immutible and stateless. There is no such thing as a "getter" for a clojure Record or hash-map (other than get, which will always return the same value when called with the same key on the same map or record instance). Indeterminate behaviors are inherently not pure.

That said, you could store the impure procedure in a field, and call that procedure to get your value.

user> (defrecord Stat [val1 val2])
user.Stat
user> (def s1 (Stat. 1 #(rand-nth [0 1 1 2 2 2 3 3 3 3 4 4 4 4 4])))
#'user/s1
user> ((:val2 s1))
1
user> ((:val2 s1))
1
user> ((:val2 s1))
3
user> ((:val2 s1))
4
user> ((:val2 s1))
4
user> ((:val2 s1))
4
user> ((:val2 s1))
3

If you need a true indeterministic getter method (if the caller simply can't be expected to call the field instead of just accessing it), you could instead use gen-class and define a getter method.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top