Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a good way to check return types when refactoring?

Currently when I'm refactoring in Clojure I tend to use the pattern:

(defn my-func [arg1 arg2]
  (assert (= demo.core.Record1 (class arg1) "Incorrect class for arg1: " (class arg1))
  (assert (= demo.core.Record1 (class arg2) "Incorrect class for arg2: " (class arg2))
  ...

That is, I find myself manually checking the return types in case a downstream part of the system modifies them to something I don't expect. (As in, if I refactor and get a stack-trace I don't expect, then I express my assumptions as invariants, and step forward from there).

In one sense, this is exactly the kind of invariant checking that Bertrand Meyer anticipated. (Author of Object Oriented Software Construction, and proponent of the idea of Design by Contract).

The challenge is that I don't find these out until run-time. I would be nice to find these out at compile-time - by simply stating what the function expects.

Now I know Clojure is essentially a dynamic language. (Whilst Clojure has a 'compiler' of sorts, we should expect the application of values to a function to only come to realisation at run-time.)

I just want a good pattern to make refactoring easier. (ie see all the flow-on effects of changing an argument to a function, without seeing it break on the first call, then moving onto the next, then moving onto the next breakage.)

My question is: Is there a good way to check return types when refactoring?

like image 397
hawkeye Avatar asked Jan 21 '16 09:01

hawkeye


2 Answers

If I understand you right, prismatic/schema should be your choice. https://github.com/plumatic/schema

(s/defn ^:always-validate my-func :- SomeResultClass
  [arg1 :- demo.core.Record1
   arg2 :- demo.core.Record1]
  ...)

you should just turn off all the validation before release, so it won't affect performance.

core.typed is nice, but as far as i remember, it enforces you to annotate all your code, while schema lets you only annotate critical parts.

like image 120
leetwinski Avatar answered Dec 04 '22 10:12

leetwinski


You have a couple of options, only one of which is "compile" time:

Tests

As Clojure is a dynamic language, tests are absolutely essential. They are your safety net when refactoring. Even in statically typed languages tests are still of use.

Pre and Post Conditions

They allow you to verify your invariants by adding metadata to your functions such as in this example from Michael Fogus' blog:

(defn constrained-fn [f x]
  {:pre  [(pos? x)]
   :post [(= % (* 2 x))]}
  (f x))

(constrained-fn #(* 2 %) 2)
;=> 4
(constrained-fn #(float (* 2 %)) 2)
;=> 4.0
(constrained-fn #(* 3 %) 2)
;=> java.lang.Exception: Assert failed: (= % (* 2 x)

core.typed

core.typed is the only option in this list that will give you compile time checking. Your example would then be expressed like so:

(ann  my-func (Fn [Record1 Record1 -> ResultType]))
(defn my-func [arg1 arg2]
...)

This comes at the expense of running core.typed as a seperate action, possibly as part of your test suite.

And still on the realm of runtime validation/checking, there are even more options such as bouncer and schema.

like image 36
leonardoborges Avatar answered Dec 04 '22 11:12

leonardoborges