Exploratory Haskell

It doesn’t have to be so thought out.

A lot of people think that Haskell is great for expressing a problem that you understand really well, but it’s not so great for sketching out a problem and prototyping. In Ruby, you can start writing code that kinda works, and refine it to be more and more correct. In Haskell, you really have to get the code to type check before you do anything else, and if you don’t have a good idea of your data model, then you don’t have types to work with, and you can’t really make it all work. Right?

I’m fortunate enough to be able to pick whatever language I want to solve programming problems in my Artificial Intelligence course at UGA. One assignment was to implement the DPLL algorithm from the textbook (Russell and Norvig). I decided to copy the pseudocode, translate it directly into Haskell, and see if I could get to a working solution from there. The Wikipedia article linked does a good job explaining the algorithm, so I’ll just go over the code. Very briefly, the algorithm is a way of taking a list of clauses of Boolean logic and determining whether or not there is a way to assign the variables in the clauses to make the whole expression true.

The original code is available in this repository.

The Pseudocode

Here is the pseudocode for the algorithm, reproduced from Norvig and Russell’s textbook:

function DPLL(clauses, symbols, model ) returns true or false
    if every clause in clauses is true in model then
        return true
    if some clause in clauses is false in model then 
        return false
    P, value ← FIND-PURE-SYMBOL (symbols, clauses, model)
    if P is non-null then 
        return DPLL(clauses, symbols – P, model ∪ {P =value})
    P, value ← FIND-UNIT-CLAUSE (clauses, model)
    if P is non-null then
        return DPLL(clauses, symbols – P, model ∪ {P =value})
    P ← FIRST (symbols)
    rest ← REST (symbols)
    return DPLL(clauses, rest, model ∪ {P =true}) 
        or DPLL(clauses, rest, model ∪ {P =false}))

Now let’s make it Haskell! We’ll translate names and terms directly and assume functions exist that we can use. We already know that P can be nullable, so we’ll go ahead and use Maybe for that type. Instead of if checking, we’ll pattern match on the result.

dpll :: Clauses -> Symbols -> Model -> Bool
dpll clauses symbols model =
  if all (`isTrueIn` model) clauses then
      True
  else if any (`isFalseIn` model) clauses then
      False
  else case findPureSymbol symbols clauses model of
      (Just sym, val) ->
          dpll clauses (symbols `minus` sym)
                       (model `including` (sym := val))
      (Nothing, val) ->
          case findUnitClause clauses model of
              (Just sym, val) ->
                  dpll clauses (symbols `minus` sym)
                               (model `including` (sym := val))
              (Nothing, val) ->
                  case symbols of
                       (x:xs) ->
                           dpll clauses xs (model `including` (x := False))
                           || dpll clauses xs (model `including` (x := True))
                       [] ->
                           False -- should not happen?

This doesn’t compile or type check at all. Despite that, hlint is capable of suggesting code improvements, and we can follow those to a much nicer looking implementation. Here’s a mostly syntastic transformation to the above code:

dpll :: Clauses -> Symbols -> Model -> Bool
dpll clauses symbols model
  | all (`isTrueIn` model) clauses  = True
  | any (`isFalseIn` model) clauses = False
  | otherwise =
      case findPureSymbol symbols clauses model of
           Just (sym := val) ->
             dpll clauses (symbols `minus` sym)
                           (model `including` (sym := val))
           Nothing ->
             case findUnitClause clauses model of
                  Just (sym := val) ->
                    dpll clauses (symbols `minus` sym)
                                  (model `including` (sym := val))
                  Nothing ->
                    case symbols of
                         (x:xs) ->
                           dpll clauses xs (model `including` (x := False)) 
                           || dpll clauses xs (model `including` (x := True))
                         [] ->
                           False -- really why

The “pattern matching on Maybe” approach is kind of ugly. Let’s extract those branches into named subexpressions and use maybe from Data.Maybe to choose which branch to go on, and pattern match directly on the list at the function definition rather than later on.

dpll :: Clauses -> Symbols -> Model -> Bool
dpll clauses symbols@(x:xs) model
  | all (`isTrueIn` model) clauses  = True
  | any (`isFalseIn` model) clauses = False
  | otherwise =
      maybe pureSymbolNothing pureSymbolJust (findPureSymbol symbols clauses model)
  where
    pureSymbolJust (sym := val) =
      dpll clauses (symbols `minus` sym) (model `including` (sym := val))
    pureSymbolNothing =
      maybe unitClauseNothing unitClauseJust (findUnitClause clauses model)
    unitClauseJust (sym := val) =
      dpll clauses (symbols `minus` sym) (model `including` (sym := val))
    unitClauseNothing =
      dpll clauses xs (model `including` (x := False))
      || dpll clauses xs (model `including` (x := True))

We’ve eliminated the manual pattern matching. While more astute programmers might have noticed what’s going on with the previous form, I didn’t quite catch on to the overall pattern. Note that we’re doing the same thing in every terminal branch: we recursively call dpll, and only the variable assignment changes! More than that, we’re expressing the idea of “Try this thing. If it succeeds, take the result. If it fails, try the next thing.”

The Maybe monad captures a similar idea, but isn’t quite what we want – that returns Nothing if any are Nothing, and really, we want to take the first Just value and do something with it.

Maybe has an Alternative instance, which does exactly what we want:

> Nothing <|> Just 10 <|> Nothing <|> Just 2
Just 10

One of the cool things we can do with an Alternative is the asum function. We give it a Foldable t full of Alternative f => f a values, and it gives us back an f a based on the definition of Alternative for our f. We can very succinctly express the idea “Try these actions and take the first successful one” with this!

Alright, let’s refactor our code to use that idea. We’re using the Maybe functor all over the place, so to bring dpll in line with the find functions, we’ll return a Maybe Model. This makes the function even more useful – now you get back the model instead of just whether or not the statement is satisfiable!

dpll :: Clauses -> Symbols -> Model -> Maybe Model
dpll clauses symbols model
  | all (`isTrueIn` model) clauses  = Just model
  | any (`isFalseIn` model) clauses = Nothing
  | otherwise =
      let controlList :: [Maybe Assignment]
          controlList =
            [ findPureSymbol symbols clauses model
            , findUnitClause clauses model
            , (:= False) <$> listToMaybe symbols
            , (:= True)  <$> listToMaybe symbols
            ]

controlList is our list of possible assignments. We want to take the first one that actually works and do something with it.

          next :: Assignment -> Maybe Model
          next (sym := val) =
            dpll clauses (symbols `minus` sym) (model `including` (sym := val))

The next step is taking the assignment, and recursively calling dpll with the symbol removed from the symbol collection and the model including the assignment.

And now, for the expression that puts everything together:

       in asum (map (>>= next) controlList)

Haskell’s laziness works well for us here. In a strict language, this code would generate all possible models before picking the first one. We can really leverage Haskell’s laziness to make this efficient and fast.

“uh but it still doesn’t compile”

So we’ve taken a pseudocode algorithm, translated it to Haskell, and applied a bunch of Haskell idioms to it. There are no type definitions, and the code doesn’t compile, and virtually no functions are defined. Now I guess we’ll want to fill in those lower level details with stuff that fits what we want.

Let’s analyze what we’re doing with the terms to get some idea of what data types will fit for them. A symbol could be anything – but we’ll just use a String for now.

type Symbol = String

I’ve used listToMaybe on the symbols. This betrays my intent – Symbols is going to be a list. That gives us enough information to define minus as well.

type Symbols = [Symbol]

minus :: Symbols -> Symbol -> Symbols
minus xs = (xs \\) . pure
-- or, if you don't like point-free,
minus xs x = xs \\ [x]

The Clauses type is a collection of expressions. An expression is a list of terms in Conjunctive Normal Form (CNF).

type Clauses = [CNF]

With CNF, juxtaposion is conjunction. We can therefore represent a CNF expression as a list of literals:

type CNF = [Literal]

Finally, a literal is a pair of Sign and Symbol.

type Literal = (Sign, Symbol)

A sign is a function that either negates or doesn’t negate a boolean expression.

data Sign = Pos | Neg
  deriving (Eq, Ord, Show)

apply :: Sign -> Bool -> Bool
apply Pos = id
apply Neg = not

-- some helper functions for testing
n :: Symbol -> Literal
n c = (Neg, c)

p :: Symbol -> Literal
p c = (Pos, c)

We can now actually construct an example Clauses to test our function (when it, you know, compiles):

ex :: Clauses
ex =
  [ [ n "p", p "a", p "c" ]
  , [ n "a" ]
  , [ p "p", n "c" ]
  ]

The main use we have for the clauses type is to map over it with isTrueIn and isFalseIn, checking every clause in the list against the model. The model is an assignment of symbols to truth values, and random access time is important. We’ll use a map.

type Model = Map Symbol Bool

isTrueIn :: CNF -> Model -> Bool
isTrueIn cnf model = or (mapMaybe f cnf)
  where
    f (sign, symbol) = apply sign <$> Map.lookup symbol model

Here, we’re looking up every symbol in the CNF expression, and applying the literal’s sign to the possible value. If there’s no value in the model, then it doesn’t return anything. or checks to see if there are any True values in the resulting list. If there are, then the CNF is true in the model.

isFalseIn :: CNF -> Model -> Bool
isFalseIn cnf model = all not literals
  where
    literals =
      map f cnf
    f (sign, symbol) =
      apply sign (Map.findWithDefault (apply sign True) symbol model)

isFalseIn is trickier – we map over the CNF expression with a default value of the sign applied to True. Then, we apply the sign again to the resulting value. all not is a way of saying “every element is false.”

Now the compiler is complaining about not recognizing the := symbol. As it happens, any infix function that prefixed with a : is a data constructor. We’ll define the data type Assignment and give it some accessor functions.

data Assignment 
  = (:=) 
  { getSymbol :: Symbol
  , getValue :: Bool 
  }

instance Show Assignment where
  show (s := v) = "(" ++ s ++ " := " ++ show v ++ ")"

An advantage of using a data constructor is that we can pattern match on the values of that constructor. This gives us a rather nice definition of the including function:

including :: Model -> Assignment -> Model
including m (sym := val) = Map.insert sym val m

The final remaining items that aren’t defined are findPureSymbol and findUnitClause. From the textbook,

Pure symbol heuristic: A pure symbol is a symbol that always appears with the same “sign” in all clauses. For example, in the three clauses (A ∨ ¬ B), (¬ B ∨ ¬ C), and (C ∨ A), the symbol A is pure because only the positive literal appears, B is pure because only the negative literal appears, and C is impure.

If a symbol has all negative signs, then the returned assignment is False. If a symbol has all positive signs, then the returned assignment is True. We’ll punt refining the clauses with the model to a future function…

findPureSymbol :: Symbols -> Clauses -> Model -> Maybe Assignment
findPureSymbol symbols clauses' model =
  asum (map makeAssignment symbols)
  where
    clauses = refinePure clauses' model
    makeAssignment :: Symbol -> Maybe Assignment
    makeAssignment sym =
      (sym :=) <$> negOrPos (signsForSymbol sym)

We’re using asum again to pick the first assignment that works out. This maps the assignment of the sym variable over the negOrPos function, which determines whether the symbol should have a True or False assignment.

    signsForSymbol :: Symbol -> [Sign]
    signsForSymbol sym =
      clauses >>= signsForSymInClause sym
    signsForSymInClause :: Symbol -> CNF -> [Sign]
    signsForSymInClause sym =
      map fst . filter ((== sym) . snd)

So we’re filtering the CNF (a list of Literals or (Sign, Symbol)) to only contain the elements whose second element is equal to the symbol. And then we’re extracting the first element, leaving just the Signs.

    negOrPos :: [Sign] -> Maybe Bool
    negOrPos = getSingleton . nub

getSingleton :: [a] -> Maybe a
getSingleton [x] = Just x
getSingleton _   = Nothing

Finally, we nub the list, and if it’s a singleton, then we have a success.

Now, we’ll define the findUnitClause function. From the textbook,

Unit clause heuristic: A unit clause was defined earlier as a clause with just one literal. In the context of DPLL, it also means clauses in which all literals but one are already assigned false by the model. For example, if the model contains B = true, then (¬B ∨ ¬C) simplifies to ¬C, which is a unit clause. Obviously, for this clause to be true, C must be set to false. The unit clause heuristic assigns all such symbols before branching on the remainder.

As above, we’ll punt refining the clauses with the model until later.

findUnitClause :: Clauses -> Model -> Maybe Assignment
findUnitClause clauses' model = assignSymbol <$> firstUnitClause
  where
    clauses :: Clauses
    clauses = refineUnit clauses' model

    firstUnitClause :: Maybe Literal
    firstUnitClause =
      asum (map (getSingleton .  mapMaybe ifInModel) clauses)

    ifInModel :: Literal -> Maybe Literal
    ifInModel (sign, symbol) = 
      case Map.lookup symbol model of
           Just val -> if apply sign val
                          then Nothing
                          else Just (sign, symbol)
           _ -> Just (sign, symbol)

    assignSymbol :: Literal -> Assignment
    assignSymbol (sign, symbol) = symbol := apply sign True

This is much simpler than pure symbols! We map assignment over the first unit clause that is found. The first unit clause is found with the asum technique. We take the list of clauses, and for each clause, first determine what to do if it’s in the model already. If the symbol is in the model, then we check to see if the literal in the clause has a True or False value by applying the sign to the value. If the value is True, then we don’t include it. Otherwise, we include the literal in the list. Finally, we attempt to get the singleton list. asum gets the first clause which satisfies these conditions.

Now, in the previous functions, we punted refining the clauses. It’s time to do that. For a pure symbol, the given optimization is (from the book):

Note that, in determining the purity of a symbol, the algorithm can ignore clauses that are already known to be true in the model constructed so far. For example, if the model contains B = false, then the clause (¬ B ∨ ¬ C) is already true, and in the remaining clauses C appears only as a positive literal; therefore C becomes pure.

We’ll start by folding the model and clauses into a new set of clauses. The helper function will go through each symbol in the model, find the relevant clauses, and modify them appropriately.

refinePure :: Clauses -> Model -> Clauses
refinePure = Map.foldrWithKey f
  where
    f :: Symbol -> Bool -> Clauses -> Clauses
    f sym val = map discardTrue
      where
        discardTrue =
          filter (not . clauseIsTrue)
        clauseIsTrue (sign, symbol) =
          symbol == sym && apply sign val

The optimization from the text for the unit clause is:

In the context of DPLL, it also means clauses in which all literals but one are already assigned false by the model. For example, if the model contains B = true, then (¬ B ∨ ¬ C) simplifies to ¬ C, which is a unit clause. Obviously, for this clause to be true, C must be set to false. The unit clause heuristic assigns all such symbols before branching on the remainder.

refineUnit :: Clauses -> Model -> Clauses
refineUnit clauses model = map refine clauses
  where
    refine :: CNF -> CNF
    refine cnf =
      case allButOneFalse cnf of
           Just (s := True)  -> [p s]
           Just (s := False) -> [n s]
           Nothing -> cnf

    allButOneFalse :: CNF -> Maybe Assignment
    allButOneFalse =
      getSingleton . filter (not . getValue) . map assign

    assign :: Literal -> Assignment
    assign (sign, sym) =
      sym := Map.findWithDefault (apply sign True) sym model

If all but one of the literals in the CNF are false, then we return that with the proper assigment. Otherwise, we return the whole CNF expression.

Starting from a straight up transcription, we’ve now finally implemented enough to solve problems!

solved :: Maybe Model
solved = dpll ex ["p", "a", "c"] Map.empty

The output will be kind of ugly, so let’s make a pretty printing function:

showModel :: Model -> String
showModel = 
  unlines . map (show . snd) . Map.toList . Map.mapWithKey (:=)

Evaluating solved in GHCi gives us:

Prelude HW6> putStr . showModel . fromJust $ solved
(a := False)
(c := False)
(p := False)

Assigning the variables with those values does return a true model. Nice!

Australia

Given a set of colors, and a map, can you color every state in a way that all touching states have different colors?

This is the coloring problem (with a good intro here). As it happens, Australia makes a nice simple model for this, and the three coloring of Australia is easy enough to do by hand that it makes a good model for testing our software out. Let’s define the problem and find the solution!

First, we’ll want to define our symbols:

colors :: [Symbol]
colors = [green, blue, red]

green = "-green"
blue = "-blue"
red = "-red"

states :: [Symbol]
states =
  [ western
  , southern
  , northern
  , queensland
  , newSouthWales
  , victoria
  ]

western  = "Western"
southern = "Southern"
northern = "Northern"
queensland = "Queensland"
newSouthWales = "New South Wales"
victoria = "Victoria"

Now we’ll express that a given state can have one color, but only one:

hasColor :: Symbol -> Clauses
hasColor st = 
   [ [ p $ st `is` green
     , p $ st `is` blue
     , p $ st `is` red
     ]
   , [ n $ st `is` blue
     , n $ st `is` red
     ]
   , [ n $ st `is` green
     , n $ st `is` red
     ]
   , [ n $ st `is` green
     , n $ st `is` blue
     ]
   ]

Since our symbols are lists, we can concatenate them together. We don’t want to get the two confused, so we make specialized functions that only work on symbols and clauses, respectively.

is :: Symbol -> Symbol -> Symbol
is = (++)

(/\) :: Clauses -> Clauses -> Clauses
(/\) = (++)

And, since we’ll often want to take a list of things, apply a function to each, and make a clause out of the whole thing, we’ll alias the bind function to something that looks kind of like “take the conjunction of this whole set.”

(/\:) :: Monad m => m a -> (a -> m b) -> m b
(/\:) = (>>=)

At first, we’ll simply say that every state has a color.

initialConditions :: Clauses
initialConditions = states /\: hasColor

Next, we’ll say that for a pair of adjacent states, they can’t both be the same color.

adjNotEqual :: (Symbol, Symbol) -> Clauses
adjNotEqual (a, b) = colors /\: bothAreNot
  where
    bothAreNot color =
      [ [ n $ a `is` color
        , n $ b `is` color
        ]
      ]

Next, a list of adjacent states…

adjStates :: [(Symbol, Symbol)]
adjStates =
  [ (western, northern)
  , (western, southern)
  , (northern, southern)
  , (northern, queensland)
  , (southern, newSouthWales)
  , (southern, victoria)
  , (southern, queensland)
  , (newSouthWales, queensland)
  , (newSouthWales, victoria)
  ]

adjacentStatesNotEqual :: Clauses
adjacentStatesNotEqual = adjStates /\: adjNotEqual

australiaClauses :: Clauses
australiaClauses =
  initialConditions /\ adjacentStatesNotEqual

australiaSymbols :: Symbols 
australiaSymbols =
  is <$> states <*> colors

Now, we’ve finally accomplished the encoding, and we can get the solution.

australiaSolution :: Maybe Model
australiaSolution = dpll australiaClauses australiaSymbols mempty

It can be printed with the following function:

showOnlyTrue :: Model -> String
showOnlyTrue =
  unlines . map (show . snd) 
    . filter (getValue . snd) 
    . Map.toList . Map.mapWithKey (:=)

printAustralia :: IO ()
printAustralia = do
  let model = fromJust australiaSolution
  putStrLn (showOnlyTrue model)

Which, when evaluated in GHCi, gives us:

Prelude HW6> printAustralia
(New South Wales-red := True)
(Northern-red := True)
(Queensland-blue := True)
(Southern-green := True)
(Victoria-blue := True)
(Western-blue := True)

This can be verified manually to be a correct coloring of Australia.

Well, we started with a bunch of pseudocode, transcribed it directly into Haskell syntax, refactored it blindly until it was nice and idiomatic, and finally started implementing the details we’d assumed. This is rather different from how I usually approach solving problems in Haskell, and it was pretty fun.