Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling user input validation with preconditions functionally in Clojure

Tags:

clojure

I write a game server and have to check that messages arriving from users are correct and valid. That means they have to be of correct syntax, comply to parameter formatting and are semantically correct, i.e. complying to the game's rules.

My goal is to have an expressive, functional way without throwing exceptions that allows composability as good as possible.

I am aware of other similar questions but they either refer to {:pre ..., :post ...} which I dislike as only stringified information can be processed once the exception is thrown, or refer exception handling in general which I dislike because Clojure should be able to do this kind of task, or they refer to Haskell's monadic style with e.g. a maybe Monad à la (-> [err succ]) which I also dislike because Clojure should be able to handle this kind of task without needing a Monad.

So far I do the ugly way using cond as pre-condition checker and error codes which I then send back to the client who send the request:

(defn msg-handler [client {:keys [version step game args] :as msg}]
  (cond
    (nil? msg)                     :4001
    (not (valid-message? msg))     :4002
    (not (valid-version? version)) :5050
    (not (valid-step?    step))    :4003
    (not (valid-game-id? game))    :4004
    (not (valid-args?    args))    :4007
    :else (step-handler client step game args)))

and similar...

(defn start-game [game-id client]
  (let [games   @*games*
        game    (get games game-id)
        state   (:state game)
        players (:players game)]
    (cond
      (< (count players) 2) :4120
      (= state :started)    :4093
      (= state :finished)   :4100
      :else ...)))

Another way would be to write a macro, similar to defn and {:pre} but instead of throwing an AssertionError throw an ex-info with a map, but again: opposed to exception throwing.

like image 974
Kreisquadratur Avatar asked Oct 21 '22 09:10

Kreisquadratur


1 Answers

The heart of your question seems to be in your comment:

Yeah, maybe I should explain what about this is "ugly": It's the not-well composability of just return an actual result, which can be represented almost arbitrarily and a numbered keyword as error. My callstack looks like this: (-> msg msg-handler step-handler step-N sub-function), and all of them could return this one error type, but none of them returns the same success type and another thing is, that I have to manually design if en error return type should short-circuit or has to be expected to do some intermediate work (undo data changes or notify other clients in parallel)

Clojure 1.5+ has the some-> threading macro to get rid of nil? check boilerplate. We just need to gently adjust the code to substitute the nil? check for a check of our choice.

(defmacro pred->
  "When predicate is not satisfied, threads expression into the first form
  (via ->), and when that result does not satisfy the predicate, through the
  next, etc. If an expression satisfies the predicate, that expression is
  returned without evaluating additional forms."
  [expr pred & forms]
  (let [g (gensym)
        pstep (fn [step] `(if (~pred ~g) ~g (-> ~g ~step)))]
    `(let [~g ~expr
           ~@(interleave (repeat g) (map pstep forms))]
       ~g)))

Note with this definition, (pred-> expr nil? form1 form2 ...) is (some-> expr form1 form2...). But now we can use other predicates.


Example

(defn foo [x] (if (even? x) :error-even (inc x)))
(defn bar [x] (if (zero? (mod x 3)) :error-multiple-of-three (inc x)))

(pred-> 1 keyword? foo) ;=> 2
(pred-> 1 keyword? foo foo) ;=> :error-even
(pred-> 1 keyword? foo foo foo) ;=> :error-even

(pred-> 1 keyword? foo bar foo bar) ;=> 5
(pred-> 1 keyword? foo bar foo bar foo bar foo bar) ;=> :error-multiple-of-three

Your use case

A flexible choice would be to make a wrapper for validation errors

(deftype ValidationError [msg])

Then you can wrap your error code/messages as in (->ValidationError 4002) and change your threading to

(pred-> msg #(instance? ValidationError %) 
  msg-handler step-handler step-N sub-function)
like image 153
A. Webb Avatar answered Oct 23 '22 05:10

A. Webb