Managing invariants in Clojure / Haskell

I was comparing OOP and FP methodologies and I couldn't focus on either one in functional programming - keeping invariants in the data structure.

For example, imagine the following requirements.

We have a list of projects and each project has a list of tasks and a list of assigned members. Each task can have a worker assigned, but only from the list of assigned project participants.

I can imagine how I can solve this problem in an OOP language like Java by adding the necessary checks and exceptions and this, in my opinion, will lead to more robust code.

But since data is decoupled from behavior in FP, how do I solve the same problem in FP, say, in Clojure or Haskell?

+3


source to share


4 answers


Your question is very general, many strategies can be used to solve related problems, but, in essence, checking invariants (at runtime) is "equal" than in OOP

assign :: Task -> Worker -> Either String Task
assign task worker =
    if not (taskProject task) `containsWorker` worker
        then Left "You can't assign..."
        else Right $ task { taskWorkers = worker : taskWorkers task }

      

One common behavior is constructor hide data

(like Task

, Worker

and Project

) and OOP copy is to write constructors private

.

module Scheduler (
  Task   -- instead `Task (..)`
, Worker -- instead `Worker (..)`
...
, assign
, createTask
, createWorker
...
) where

      



(I do not know the current support for Haskell friend

, protected

... it seems there is not a similar instance, and you can find a lot of Haskell modules Some.Module.Internals.Something

with private facilities)

The main question is how to structure the entire program to achieve the desired behavior.

Real World Haskell is a good starting point to learn about this or, as suggested on the related question, Large Scale Design in Haskell?

On the other hand, about the pre / post state in Haskell, you can read What are the precondition check options in Haskell .

+1


source


In Clojure, you can specify arbitrary conditions :pre

and :post

for any function. Here's an example from the documentation :

(defn constrained-sqr [x]
    {:pre  [(pos? x)]
     :post [(> % 16), (< % 225)]}
    (* x x))

      



There's also a very interesting core.contracts library that implements contracts in Clojure.

+4


source


You say data is "decoupled" from behavior in FP, but in Haskell (I don't know Clojure) you can easily define the data structure in a module, make it private, and only export functions to manipulate the data.

In other words, Haskell has no classes (OO-style), but it still has encapsulation.

+1


source


JoseJuan and MathematicalOrchid covered the key points about hiding constructors when exposing types and interfaces, but there is another way to manage some kinds of invariants in Haskell: encode them in the type system. Algebraic data types do this to a certain extent on their own:

data Tree a = Tri (Tree a) a (Tree a) a (Tree a)
            | Bin (Tree a) a (Tree a)
            | Tip

      

much more limited than

newtype Tree a = Tree [a] [Tree a]

      

But you can go much further by using nested types, phantom types, and GADT types. For example, Data.Sequence

defines

newtype Elem a = Elem a
data Node a = Node2 !Int a a | Node3 !Int a a a
data Digit a = One a | Two a a | Three a a a | Four a a a a
data FingerTree a = Empty
                  | Single a
                  | Deep !Int (Digit a)
                    (FingerTree (Node a))
                    (Digit a)
newtype Seq a = Seq (FingerTree (Elem a))

      

Please note that deep FingerTree a

contains FingerTree (Node a)

. This is called a "nested" or "irregular" type; it ensures that 2-3 trees on each level are exactly one level lower than the previous level.

The same form invariant can be maintained in different ways (but less efficiently, as it turns out) using the phantom and GADT types:

{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}
data Nat = Z | S Nat

-- This is a GADT; n is a phantom type
data Tree23 (n::Nat) a where
  Elem :: a -> Tree23 Z a
  Node2 :: !Int -> Tree23 n a -> 
             Tree23 n a -> Tree23 (S n) a
  Node3 :: !Int -> Tree23 n a -> Tree23 n a ->
             Tree23 n a -> Tree23 (S n) a

-- n is again a phantom type
data Digit (n::Nat) a
  = One (Tree23 n a)
  | Two (Tree23 n a) (Tree23 n a)
  | Three (Tree23 n a) (Tree23 n a) (Tree23 n a)
  | Four (Tree23 n a) (Tree23 n a) (Tree23 n a) (Tree23 n a)

-- n is still a phantom type
data FingerTree (n::Nat) a
     = Empty
     | Single a
     | Deep !Int (Digit n a) (FingerTree (S n) a) (Digit n a)

      

In this version, the level of the finger tree is "tracked" using the phantom type, and then the heights of 2-3 trees are forced to match using the GADT.

+1


source







All Articles