File: FourFours.hs

package info (click to toggle)
haskell-sbv 10.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,148 kB
  • sloc: haskell: 31,176; makefile: 4
file content (214 lines) | stat: -rw-r--r-- 9,508 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
-----------------------------------------------------------------------------
-- |
-- Module    : Documentation.SBV.Examples.Queries.FourFours
-- Copyright : (c) Levent Erkok
-- License   : BSD3
-- Maintainer: erkokl@gmail.com
-- Stability : experimental
--
-- A query based solution to the four-fours puzzle.
-- Inspired by <http://www.gigamonkeys.com/trees/>
--
-- @
-- Try to make every number between 0 and 20 using only four 4s and any
-- mathematical operation, with all four 4s being used each time.
-- @
--
-- We pretty much follow the structure of <http://www.gigamonkeys.com/trees/>,
-- with the exception that we generate the trees filled with symbolic operators
-- and ask the SMT solver to find the appropriate fillings.
-----------------------------------------------------------------------------

{-# LANGUAGE DeriveAnyClass      #-}
{-# LANGUAGE DeriveDataTypeable  #-}
{-# LANGUAGE FlexibleInstances  #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving  #-}
{-# LANGUAGE TemplateHaskell     #-}

{-# OPTIONS_GHC -Wall -Werror #-}

module Documentation.SBV.Examples.Queries.FourFours where

import Data.SBV
import Data.SBV.Control

import Data.List (inits, tails)
import Data.Maybe

-- | Supported binary operators. To keep the search-space small, we will only allow division by @2@ or @4@,
-- and exponentiation will only be to the power @0@. This does restrict the search space, but is sufficient to
-- solve all the instances.
data BinOp = Plus | Minus | Times | Divide | Expt

-- | Make 'BinOp' a symbolic value.
mkSymbolicEnumeration ''BinOp

-- | Supported unary operators. Similar to 'BinOp' case, we will restrict square-root and factorial to
-- be only applied to the value @4.
data UnOp  = Negate | Sqrt | Factorial

-- | Make 'UnOp' a symbolic value.
mkSymbolicEnumeration ''UnOp

-- | The shape of a tree, either a binary node, or a unary node, or the number @4@, represented hear by
-- the constructor @F@. We parameterize by the operator type: When doing symbolic computations, we'll fill
-- those with 'SBinOp' and 'SUnOp'. When finding the shapes, we will simply put unit values, i.e., holes.
data T b u = B b (T b u) (T b u)
           | U u (T b u)
           | F

-- | A rudimentary 'Show' instance for trees, nothing fancy.
instance Show (T BinOp UnOp) where
   show F       = "4"
   show (U u t) = case u of
                    Negate    -> "-" ++ show t
                    Sqrt      -> "sqrt(" ++ show t ++ ")"
                    Factorial -> show t ++ "!"
   show (B o l r) = "(" ++ show l ++ " " ++ so ++ " " ++ show r ++ ")"
     where so = fromMaybe (error $ "Unexpected operator: " ++ show o)
                        $ o `lookup` [(Plus, "+"), (Minus, "-"), (Times, "*"), (Divide, "/"), (Expt, "^")]

-- | Construct all possible tree shapes. The argument here follows the logic in <http://www.gigamonkeys.com/trees/>:
-- We simply construct all possible shapes and extend with the operators. The number of such trees is:
--
-- >>> length allPossibleTrees
-- 640
--
-- Note that this is a /lot/ smaller than what is generated by <http://www.gigamonkeys.com/trees/>. (There, the
-- number of trees is 10240000: 16000 times more than what we have to consider!)
allPossibleTrees :: [T () ()]
allPossibleTrees = trees $ replicate 4 F
  where trees :: [T () ()] -> [T () ()]
        trees [x] = [x, U () x]
        trees xs  = do (left, right) <- splits
                       t1            <- trees left
                       t2            <- trees right
                       trees [B () t1 t2]
          where splits = init $ tail $ zip (inits xs) (tails xs)

-- | Given a tree with hols, fill it with symbolic operators. This is the /trick/ that allows
-- us to consider only 640 trees as opposed to over 10 million.
fill :: T () () -> Symbolic (T SBinOp SUnOp)
fill (B _ l r) = B <$> free_ <*> fill l <*> fill r
fill (U _ t)   = U <$> free_ <*> fill t
fill F         = return F

-- | Minor helper for writing "symbolic" case statements. Simply walks down a list
-- of values to match against a symbolic version of the key.
sCase :: (Eq a, SymVal a, Mergeable v) => SBV a -> [(a, v)] -> v
sCase k = walk
  where walk []              = error "sCase: Expected a non-empty list of cases!"
        walk [(_, v)]        = v
        walk ((k1, v1):rest) = ite (k .== literal k1) v1 (walk rest)

-- | Evaluate a symbolic tree, obtaining a symbolic value. Note how we structure
-- this evaluation so we impose extra constraints on what values square-root, divide
-- etc. can take. This is the power of the symbolic approach: We can put arbitrary
-- symbolic constraints as we evaluate the tree.
eval :: T SBinOp SUnOp -> Symbolic SInteger
eval tree = case tree of
              B b l r -> eval l >>= \l' -> eval r >>= \r' -> binOp b l' r'
              U u t   -> eval t >>= uOp u
              F       -> return 4

  where binOp :: SBinOp -> SInteger -> SInteger -> Symbolic SInteger
        binOp o l r = do constrain $ o .== sDivide .=> r .== 4 .|| r .== 2
                         constrain $ o .== sExpt   .=> r .== 0
                         return $ sCase o
                                    [ (Plus,    l+r)
                                    , (Minus,   l-r)
                                    , (Times,   l*r)
                                    , (Divide,  l `sDiv` r)
                                    , (Expt,    1)   -- exponent is restricted to 0, so the value is 1
                                    ]

        uOp :: SUnOp -> SInteger -> Symbolic SInteger
        uOp o v = do constrain $ o .== sSqrt      .=> v .== 4
                     constrain $ o .== sFactorial .=> v .== 4
                     return $ sCase o
                                [ (Negate,    -v)
                                , (Sqrt,       2)  -- argument is restricted to 4, so the value is 2
                                , (Factorial, 24)  -- argument is restricted to 4, so the value is 24
                                ]

-- | In the query mode, find a filling of a given tree shape /t/, such that it evaluates to the
-- requested number /i/. Note that we return back a concrete tree.
generate :: Integer -> T () () -> IO (Maybe (T BinOp UnOp))
generate i t = runSMT $ do symT <- fill t
                           val  <- eval symT
                           constrain $ val .== literal i
                           query $ do cs <- checkSat
                                      case cs of
                                        Sat -> Just <$> construct symT
                                        _   -> return Nothing
    where -- Walk through the tree, ask the solver for
          -- the assignment to symbolic operators and fill back.
          construct F           = return F
          construct (U o s')    = do uo <- getValue o
                                     U uo <$> construct s'
          construct (B b l' r') = do bo <- getValue b
                                     B bo <$> construct l' <*> construct r'

-- | Given an integer, walk through all possible tree shapes (at most 640 of them), and find a
-- filling that solves the puzzle.
find :: Integer -> IO ()
find target = go allPossibleTrees
  where go []     = putStrLn $ show target ++ ": No solution found."
        go (t:ts) = do chk <- generate target t
                       case chk of
                         Nothing -> go ts
                         Just r  -> do let ok  = concEval r == target
                                           tag = if ok then " [OK]: " else " [BAD]: "
                                           sh i | i < 10 = ' ' : show i
                                                | True   =       show i

                                       putStrLn $ sh target ++ tag ++ show r

        -- Make sure the result is correct!
        concEval :: T BinOp UnOp -> Integer
        concEval F         = 4
        concEval (U u t)   = uEval u (concEval t)
        concEval (B b l r) = bEval b (concEval l) (concEval r)

        uEval :: UnOp -> Integer -> Integer
        uEval Negate    i = -i
        uEval Sqrt      i = if i == 4 then  2 else error $ "uEval: Found sqrt applied to value: " ++ show i
        uEval Factorial i = if i == 4 then 24 else error $ "uEval: Found factorial applied to value: " ++ show i

        bEval :: BinOp -> Integer -> Integer -> Integer
        bEval Plus   i j = i + j
        bEval Minus  i j = i - j
        bEval Times  i j = i * j
        bEval Divide i j = i `div` j
        bEval Expt   i j = i ^ j

-- | Solution to the puzzle. When you run this puzzle, the solver can produce different results
-- than what's shown here, but the expressions should still be all valid!
--
-- @
-- ghci> puzzle
--  0 [OK]: (4 - (4 + (4 - 4)))
--  1 [OK]: (4 / (4 + (4 - 4)))
--  2 [OK]: sqrt((4 + (4 * (4 - 4))))
--  3 [OK]: (4 - (4 ^ (4 - 4)))
--  4 [OK]: (4 + (4 * (4 - 4)))
--  5 [OK]: (4 + (4 ^ (4 - 4)))
--  6 [OK]: (4 + sqrt((4 * (4 / 4))))
--  7 [OK]: (4 + (4 - (4 / 4)))
--  8 [OK]: (4 - (4 - (4 + 4)))
--  9 [OK]: (4 + (4 + (4 / 4)))
-- 10 [OK]: (4 + (4 + (4 - sqrt(4))))
-- 11 [OK]: (4 + ((4 + 4!) / 4))
-- 12 [OK]: (4 * (4 - (4 / 4)))
-- 13 [OK]: (4! + ((sqrt(4) - 4!) / sqrt(4)))
-- 14 [OK]: (4 + (4 + (4 + sqrt(4))))
-- 15 [OK]: (4 + ((4! - sqrt(4)) / sqrt(4)))
-- 16 [OK]: (4 * (4 * (4 / 4)))
-- 17 [OK]: (4 + ((sqrt(4) + 4!) / sqrt(4)))
-- 18 [OK]: -(4 + (4 - (sqrt(4) + 4!)))
-- 19 [OK]: -(4 - (4! - (4 / 4)))
-- 20 [OK]: (4 * (4 + (4 / 4)))
-- @
puzzle :: IO ()
puzzle = mapM_ find [0 .. 20]