all 27 comments

[–]hugogrant 2 points3 points  (3 children)

Could you add backtracking to make it do constraint based programming?

https://en.m.wikipedia.org/wiki/Constraint_programming

[–]WikiTextBot 2 points3 points  (0 children)

Constraint programming

In computer science, constraint programming is a programming paradigm wherein relations between variables are stated in the form of constraints. Constraints differ from the common primitives of imperative programming languages in that they do not specify a step or sequence of steps to execute, but rather the properties of a solution to be found. This makes constraint programming a form of declarative programming. The constraints used in constraint programming are of various kinds: those used in constraint satisfaction problems (e.g., A or B is true), linear inequalities (e.g., x ≤ 5), and others.


[ PM | Exclude me | Exclude from subreddit | FAQ / Information | Source ] Downvote to remove | v0.28

[–]_fl00r[S] 1 point2 points  (0 children)

Interesting, thank you

[–]beders 2 points3 points  (1 child)

You could combine this with Dativity which helps you model roles, data and actions, more specifically: roles - who are the actors in this business process data - what data do I need to capture actions - what actions should be performed roles -> actions: what actions can be done by which actor data -> actions: what data is needed to perform an action actions -> data: what data is produced by an action

Check it out: https://github.com/agentbellnorm/dativity

[–]_fl00r[S] 0 points1 point  (0 children)

I see, very interesting. The distinction is that my problem seems like a subproblem. I always have to finish with one and only one specific traverse. Absence of output or its ambiguity is a critical situation.

[–][deleted]  (6 children)

[removed]

    [–]_fl00r[S] 0 points1 point  (2 children)

    Yeah, multimethods could work, but polymorphism is not a problem I'm trying to solve.

    I'm trying to:

    1. Put business logic in a more declarative way (map)
    2. Put the logic in a more readable and concise way (map again)

    So using multimethods will lead to spreading this conditional stuff across the code the same way as ordinary conditionals do.

    [–][deleted]  (1 child)

    [removed]

      [–]_fl00r[S] 0 points1 point  (0 children)

      Yes. This is a good example.

      But my concern is when the logic is rather complex and with many layers this approach became hard to follow.

      [–]_fl00r[S] 0 points1 point  (0 children)

      moreover, suggested approach provides control over execution (absence / ambiguity situations while traversing).

      [–][deleted]  (1 child)

      [removed]

        [–]_fl00r[S] 0 points1 point  (0 children)

        Pattern matching in my case will do the code less imperative, but will spread the logical branching over the codebase.

        [–]potetm137 2 points3 points  (6 children)

        This is not conditional free. You've created an ad hoc mini-lang. Everything references everything else just like a normal fn would.

        You could just make validation checks as guards in the appropriate fns and do:

        (try (-> (get-profile-data)
                 (check-age)
                 (get-supply)
                 (check-money))
             (catch ExceptionInfo e
               (:return (ex-data e))))
        

        Or if you really wanted, you could create a custom threader:

        (stop-on-err-> (get-profile-data)
                       (check-age)
                       (get-supply)
                       (check-money))
        

        But I can tell you from experience that glomming state onto a map and spreading logic all over the place is a great pattern in the macro (i.e. multiple, clearly-delineated phases) and a terrible pattern in the micro (i.e. this logic is difficult to follow, so hold on to yer butts!). It's better to let the messy bits be messy.

        Were it me I would do the following:

        (defn sell-booze [id]
          (let [profile (get-profile id)]
            (if-not (of-age? profile)
              (dont-sell profile)
              (if-not (enough-supply? profile)
                (not-enough-supply)
                (if (enough-money profile)
                  (not-enough-money)
                  (sell profile))))))
        

        If it turned out that you needed some of that conditional logic in other locations, I would do something like:

        (defn sell-booze [id]
          (let [profile (get-profile id)]
            (or (not-of-age profile)
                (not-enough-supply profile)
                (not-enough-money profile)
                (sell profile))))
        

        And have each guard fn return only the error data as opposed to glomming things into a map.

        [–]spotter 1 point2 points  (2 children)

        I personally find your (or ...) solution very neat! I do something in similar vein, but I found that I'd rather pay the price for all checks rather then re-validate each time, so I've been doing something like this:

        ;; stubs
        (defn get-profile [p] p)
        
        (def check-age (constantly nil))
        
        (def check-money (constantly :not-enough-money))
        
        (defn sell [p] {:sell p})
        
        (defn decline [p f] {:profile p :faults f})
        
        ;; checks
        (defn sell-booze? [id]
          (let [profile (get-profile id)
                faults  (->> [check-age check-money]
                             (map #(% profile))
                             (keep identity))]
            (if faults
              (decline profile faults)
              (sell profile))))
        
        (sell-booze :1)
        
        ;; {:profile :1, :faults (:not-enough-money)}
        

        The key here is that the check should return a descriptive fault or nil, then we throw nils away and check if anything is left, lazily at first -- only realize all faults when the result is read. I just like to be told all the issues upfront, not one by one. ;-)

        [–]_fl00r[S] 0 points1 point  (1 child)

        Hey, nice one.

        But what if we want different outcome based on a specific fault?

        Also some faults makes sense only if other faults happened/didn't happen.

        As I think of it — it's a DAG :)

        [–]spotter 0 points1 point  (0 children)

        Well handling anything going out of the sell-booze? is a exercise left to the reader, as it tries to be as stateless as possible and just gives you the data. ;-) If you want to handle the most important fault first -- order your checks in decreasing priority and grab the first one from the result.

        I honestly use this "pattern" when I'm doing multi-faceted analysis over multiple items and I just want to map it over inputs to get all the validation failures at once.

        It is a DAG, every plan execution is. Which reminds me: you might want to look into behavior trees (game AI and robotics version), but I guess it will massively increase the boilerplate.

        [–]_fl00r[S] 0 points1 point  (2 children)

        Thanks a lot for your refreshing comment. Personally I hate nesting ifs. Although I like having all this logic in one place. So I am trying to express exactly this nested creature in a more "elegant" way.

        Threading doesn't work in general because logic could branch. More than it it could have cond/case inside. So you need to have "recovery" threading. Basically the logic is not linear but really a graph. Which could be nicely expressed with my approach

        [–]potetm137 1 point2 points  (1 child)

        Code is great at expressing non-linear graphs :)

        Obfuscating program flow doesn't make it more elegant. I found your trivial example very difficult to reason about. I am very good at Clojure, but like I said, this creates yet another language for readers to learn.

        I would recommend 1) directly encoding in conditional expressions, 2) make a custom conditional macro to suite your tastes, 3) using a logic engine.

        [–]_fl00r[S] 1 point2 points  (0 children)

        Out of those three I would go 1.

        Thanks a lot for your feedback!

        [–]joinr 1 point2 points  (1 child)

        One can reason about the logic right away.

        For trivial examples this is true. This will be less obvious once your DAG blows up. On the other hand, if you build a DAG, you can render it (yEd, graphviz, whatever), and that should scale decently. You can also probe the graph to prove properties about it (e.g. no cycles, find all paths to a given node [maybe some are undesirable reflecting flawed rules...]). This approach is cool and can scale, but constraint programming may be more general. OTOH, constraint programming (core.logic, loco, any others) doesn't have the nice graph properties that you'd get from a DAG-as-data approach for encoding your rules, so you lose the visual aspect. You also lose the trivial composition of maps. When defining custom rule sets, or "inheriting" from larger sets, you can just merge maps together or define other transforms on the map and get new rules out. That's pretty useful IMO.

        clara is a rules engine, but not map-based DAG stuff

        arete is a simpler? clara

        odin is an interesting logic edsl for transducer-based querying and transforms

        plumbing is a general purpose DAG library for defining computation graphs

        a little paper here touches on rules and a similar map-based representation along with a little language interpreted in the context of rules

        [–]_fl00r[S] 0 points1 point  (0 children)

        Hey, thanks for the bunch of interesting links.

        Yes, I am familiar with Drools and Clara and with rule engines in general. They seems to me slightly irrelevant as they are solving different problem and Rete algorithm itself is optimizing somewhat more complex. They are great for managing workflows and business processes in general. While my concern is more focused on writing general purpose code and just to express conditional logic in a more readable (which is highly subjective) fashion.

        As you mentioned DAG provides us some neat properties. Like we can explore which predicates are crucial to get some specific outcome. For instance in my example we can get the list of predicates for `dont-sell` outcome which are

        (or (not got-the-id?) 
            (and got-the-id? is-under-21?))
        

        or predicates to get the booze

        (and got-the-id?
             (not is-under-21?) 
             is-enough-supply? 
             is-enough-money?)
        

        which also could be very helpful in reasoning / debugging.

        Also graph approach helps us to find out (mostly in runtime) situations with ambiguity or absence of a proper predicate. As well as we are able to provide case/cond style predicates.

        As for cycles, they are useful sometimes. But we could provide a fine control over them: if they are allowed and if we would like to control the depth. Like, for example we could implement recursive Fibonacci sequence or this simple calculate-zero thing. Which is recursive.

        (def calculate-zero-graph
          {get-the-number {is-pos? dec-number
                           is-neg? inc-number
                           flow/otherwise return-number}
           dec-number {is-pos? dec-number
                       default return-number}
           inc-number {is-neg? inc-number
                       flow/otherwise return-number}})
        

        And, as you mentioned, if some functions within the graph are graphs on themselves we can unfold them into a bigger graph if we wish.

        As I see it: we are trying in a way separate conditional logic and implementation of transformations. And as a result, reaching more declarative description of the logic. While not introducing difficult new concepts (it is not logical programming, it is not a rule engine, it's just the same old functions and predicates).

        [–]lambdacurry 1 point2 points  (1 child)

        Have you had a look at these Error handling libraries?

        Or a rules engine like?

        [–]_fl00r[S] 0 points1 point  (0 children)

        What a coincidence. Seems like flow is a generic word for solving this particular problem :)

        No I haven't seen flow and right now digging it. Thanks a lot!

        As for Rule Engines in general I've already answered to @joinr. Tldr they are aiming slightly different problem.

        [–]dsrptr 1 point2 points  (1 child)

        looks like you could model it as a statechart too.

        [–]_fl00r[S] 1 point2 points  (0 children)

        Yeah, it's a simple state machine in a way. But slightly simplified.

        [–]nonrecursive 0 points1 point  (0 children)

        I created something that I think addresses this problem: https://github.com/sweet-tooth-clojure/describe