Skip to content

Commit 37e01fc

Browse files
Switch order of search arguments
This allows for more ergonomic partial application
1 parent 1996e04 commit 37e01fc

File tree

3 files changed

+56
-56
lines changed

3 files changed

+56
-56
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Lots of problems can be modeled as graphs, but oftentimes one doesn't want to us
1010
```haskell
1111
import Algorithm.Search (bfs)
1212

13-
countChange target = bfs add_one_coin (== target) [(> target)] 0
13+
countChange target = bfs add_one_coin [(> target)] (== target) 0
1414
where
1515
add_one_coin amt = map (+ amt) coins
1616
coins = [1, 5, 10, 25]
@@ -33,7 +33,7 @@ graph = Map.fromList [
3333
]
3434

3535
-- Run dfs on the graph:
36-
-- >>> dfs (graph Map.!) (== 4) [] 1
36+
-- >>> dfs [] (graph Map.!) (== 4) 1
3737
-- Just [3,4]
3838
```
3939

@@ -55,7 +55,7 @@ findPath start end =
5555
let next =
5656
map (\pt -> (1, taxicabDistance pt end, pt))
5757
. taxicabNeighbors
58-
in aStar next (== end) [isWall] start
58+
in aStar next [isWall] (== end) start
5959

6060
-- findPath p1 p2 finds a path between p1 and p2, avoiding the wall
6161
-- >>> findPath (0, 0) (2, 0)

src/Algorithm/Search.hs

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ import qualified Data.Set as Set
2020
import qualified Data.List as List
2121

2222

23-
-- | @bfs next solved prunes initial@ performs a breadth-first search over a set
23+
-- | @bfs next prunes found initial@ performs a breadth-first search over a set
2424
-- of states, starting with @initial@, generating neighboring states with
2525
-- @next@, and pruning out any state for which a function in @prunes@ returns
26-
-- 'True'. It returns a path to a state for which @solved@ returns 'True'.
26+
-- 'True'. It returns a path to a state for which @found@ returns 'True'.
2727
-- Returns 'Nothing' if no path is possible.
2828
--
2929
-- === Example: Making change problem
3030
--
3131
-- >>> :{
32-
-- countChange target = bfs add_one_coin (== target) [(> target)] 0
32+
-- countChange target = bfs add_one_coin [(> target)] (== target) 0
3333
-- where
3434
-- add_one_coin amt = map (+ amt) coins
3535
-- coins = [25, 10, 5, 1]
@@ -40,12 +40,12 @@ import qualified Data.List as List
4040
bfs :: Ord state =>
4141
(state -> [state])
4242
-- ^ Function to generate "next" states given a current state
43-
-> (state -> Bool)
44-
-- ^ Predicate to determine if solution found. 'bfs' returns a path to the
45-
-- first state for which this predicate returns 'True'.
4643
-> [state -> Bool]
4744
-- ^ List of ways to prune search. These are predicates which, if 'True', are
4845
-- considered to indicate a "dead end".
46+
-> (state -> Bool)
47+
-- ^ Predicate to determine if solution found. 'bfs' returns a path to the
48+
-- first state for which this predicate returns 'True'.
4949
-> state
5050
-- ^ Initial state
5151
-> Maybe [state]
@@ -57,10 +57,10 @@ bfs =
5757
generalizedSearch Seq.empty id (\_ _ -> False)
5858

5959

60-
-- | @dfs next solved prunes initial@ performs a depth-first search over a set
60+
-- | @dfs next prunes found initial@ performs a depth-first search over a set
6161
-- of states, starting with @initial@, generating neighboring states with
6262
-- @next@, and pruning out any state for which a function in @prunes@ returns
63-
-- 'True'. It returns a depth-first path to a state for which @solved@
63+
-- 'True'. It returns a depth-first path to a state for which @found@
6464
-- returns 'True'. Returns 'Nothing' if no path is possible.
6565
--
6666
-- === Example: Simple directed graph search
@@ -69,17 +69,17 @@ bfs =
6969
--
7070
-- >>> graph = Map.fromList [(1, [2, 3]), (2, [4]), (3, [4]), (4, [])]
7171
--
72-
-- >>> dfs (graph Map.!) (== 4) [] 1
72+
-- >>> dfs (graph Map.!) [] (== 4) 1
7373
-- Just [3,4]
7474
dfs :: Ord state =>
7575
(state -> [state])
7676
-- ^ Function to generate "next" states given a current state
77-
-> (state -> Bool)
78-
-- ^ Predicate to determine if solution found. 'dfs' returns a path to the
79-
-- first state for which this predicate returns 'True'.
8077
-> [state -> Bool]
8178
-- ^ List of ways to prune search. These are predicates which, if 'True', are
8279
-- considered to indicate a "dead end".
80+
-> (state -> Bool)
81+
-- ^ Predicate to determine if solution found. 'dfs' returns a path to the
82+
-- first state for which this predicate returns 'True'.
8383
-> state
8484
-- ^ Initial state
8585
-> Maybe [state]
@@ -91,20 +91,20 @@ dfs =
9191
generalizedSearch [] id (\_ _ -> True)
9292

9393

94-
-- | @dijkstra next solved prunes initial@ performs a shortest-path search over
94+
-- | @dijkstra next prunes found initial@ performs a shortest-path search over
9595
-- a set of states using Dijkstra's algorithm, starting with @initial@,
9696
-- generating neighboring states and their incremental costs with @next@, and
9797
-- pruning out any state for which a function in @prunes@ returns 'True'.
9898
-- This will find the least-costly path from an initial state to a state for
99-
-- which @solved@ returns 'True'. Returns 'Nothing' if no path to a solved state
99+
-- which @found@ returns 'True'. Returns 'Nothing' if no path to a solved state
100100
-- is possible.
101101
--
102102
-- === Example: Making change problem, with a twist
103103
--
104104
-- >>> :{
105105
-- -- Twist: dimes have a face value of 10 cents, but are actually rare
106106
-- -- misprints which are worth 10 dollars
107-
-- countChange target = dijkstra add_one_coin (== target) [(> target)] 0
107+
-- countChange target = dijkstra add_one_coin [(> target)] (== target) 0
108108
-- where
109109
-- add_one_coin amt =
110110
-- map (\(true_val, face_val) -> (true_val, face_val + amt)) coin_values
@@ -117,27 +117,27 @@ dijkstra :: (Num cost, Ord cost, Ord state) =>
117117
(state -> [(cost, state)])
118118
-- ^ Function to generate list of incremental cost and neighboring states
119119
-- given the current state
120-
-> (state -> Bool)
121-
-- ^ Predicate to determine if solution found. 'dijkstra' returns the shortest
122-
-- path to the first state for which this predicate returns 'True'.
123120
-> [state -> Bool]
124121
-- ^ List of ways to prune search. These are predicates which, if 'True', are
125122
-- considered to indicate a "dead end".
123+
-> (state -> Bool)
124+
-- ^ Predicate to determine if solution found. 'dijkstra' returns the shortest
125+
-- path to the first state for which this predicate returns 'True'.
126126
-> state
127127
-- ^ Initial state
128128
-> Maybe (cost, [(cost, state)])
129129
-- (Total cost, [(incremental cost, step)]) for the first path found which
130130
-- satisfies the given predicate
131-
dijkstra next solved prunes initial =
131+
dijkstra next prunes found initial =
132132
-- Dijkstra's algorithm can be viewed as a generalized search, with the search
133133
-- container being a heap, with the states being compared without regard to
134134
-- cost, with the shorter paths taking precedence over longer ones, and with
135135
-- the stored state being (cost so far, state).
136136
-- This implementation makes that transformation, then transforms that result
137137
-- back into the desired result from @dijkstra@
138138
unpack <$>
139-
generalizedSearch emptyLIFOHeap snd better next' (solved . snd)
140-
(map (. snd) prunes) (0, initial)
139+
generalizedSearch emptyLIFOHeap snd better next' (map (. snd) prunes)
140+
(found . snd) (0, initial)
141141
where
142142
next' (cost, st) = map (\(incr, new_st) -> (incr + cost, new_st)) (next st)
143143
unpack packed_states =
@@ -154,14 +154,14 @@ dijkstra next solved prunes initial =
154154
-- a new zero-length path to a point
155155

156156

157-
-- | @aStar next solved prunes initial@ performs a best-first search using
157+
-- | @aStar next prunes found initial@ performs a best-first search using
158158
-- the A* search algorithm, starting with the state @initial@, generating
159159
-- neighboring states, their cost, and an estimate of the remaining cost with
160160
-- @next@, and pruning out any state for which a function in @prunes@ returns
161-
-- 'True'. This returns a path to a state for which @solved@ returns 'True'.
161+
-- 'True'. This returns a path to a state for which @found@ returns 'True'.
162162
-- If the estimate is strictly a lower bound on the remaining cost to reach a
163-
-- @solved@ state, then the returned path is the shortest path. Returns
164-
-- 'Nothing' if no path to a @solved@ state is possible.
163+
-- solved state, then the returned path is the shortest path. Returns
164+
-- 'Nothing' if no path to a solved state is possible.
165165
--
166166
-- === Example: Path finding in taxicab geometry
167167
--
@@ -171,32 +171,32 @@ dijkstra next solved prunes initial =
171171
-- nextTowards dest pos = map (\p -> (1, dist p dest, p)) (neighbors pos)
172172
-- :}
173173
--
174-
-- >>> aStar (nextTowards (0, 2)) (== (0, 2)) [(== (0, 1))] (0, 0)
174+
-- >>> aStar (nextTowards (0, 2)) [(== (0, 1))] (== (0, 2)) (0, 0)
175175
-- Just (4,[(1,(1,0)),(1,(1,1)),(1,(1,2)),(1,(0,2))])
176176
aStar :: (Num cost, Ord cost, Ord state) =>
177177
(state -> [(cost, cost, state)])
178178
-- ^ Function which, when given the current state, produces a list whose
179179
-- elements are (incremental cost to reach neighboring state,
180180
-- estimate on remaining cost from said state, said state).
181-
-> (state -> Bool)
182-
-- ^ Predicate to determine if solution found. 'aStar' returns the shortest
183-
-- path to the first state for which this predicate returns 'True'.
184181
-> [state -> Bool]
185182
-- ^ List of ways to prune search. These are predicates which, if 'True', are
186183
-- considered to indicate a "dead end".
184+
-> (state -> Bool)
185+
-- ^ Predicate to determine if solution found. 'aStar' returns the shortest
186+
-- path to the first state for which this predicate returns 'True'.
187187
-> state
188188
-- ^ Initial state
189189
-> Maybe (cost, [(cost, state)])
190190
-- (Total cost, [(incremental cost, step)]) for the first path found which
191191
-- satisfies the given predicate
192-
aStar next found prunes initial =
192+
aStar next prunes found initial =
193193
-- The idea of this implementation is that we can use the same machinery as
194194
-- Dijkstra's algorithm, by changing Dijsktra's cost function to be
195195
-- (incremental cost + lower bound remaining cost). We'd still like to be able
196196
-- to return the list of incremental costs, so we modify the internal state to
197197
-- be (incremental cost to state, state). Then at the end we undo this
198198
-- transformation
199-
unpack <$> dijkstra next' (found . snd) (map (. snd) prunes) (0, initial)
199+
unpack <$> dijkstra next' (map (. snd) prunes) (found . snd) (0, initial)
200200
where
201201
next' (_, st) = map pack (next st)
202202
pack (incr, est, new_st) = (incr + est, (incr, new_st))
@@ -274,21 +274,21 @@ generalizedSearch :: (SearchContainer container state, Ord stateKey) =>
274274
-- old and should thus be inserted
275275
-> (state -> [state])
276276
-- ^ Function to generate "next" states given a current state
277-
-> (state -> Bool)
278-
-- ^ Predicate to determine if solution found. 'search' returns a path to the
279-
-- first state for which this predicate returns 'True'.
280277
-> [state -> Bool]
281278
-- ^ List of ways to prune search. These are predicates which, if 'True', are
282279
-- considered to indicate a "dead end".
280+
-> (state -> Bool)
281+
-- ^ Predicate to determine if solution found. 'search' returns a path to the
282+
-- first state for which this predicate returns 'True'.
283283
-> state
284284
-- ^ Initial state
285285
-> Maybe [state]
286286
-- ^ First path found to a state matching the predicate, or 'Nothing' if no
287287
-- such path exists.
288-
generalizedSearch empty mk_key better next solved prunes initial =
288+
generalizedSearch empty mk_key better next prunes found initial =
289289
let get_steps search_st = paths search_st Map.! mk_key (current search_st)
290290
in fmap (reverse . get_steps)
291-
. findIterate (solved . current) (nextSearchState better mk_key next prunes)
291+
. findIterate (nextSearchState better mk_key next prunes) (found . current)
292292
$ SearchState initial empty (Set.singleton $ mk_key initial)
293293
(Map.singleton (mk_key initial) [])
294294

@@ -330,7 +330,7 @@ instance Ord k => SearchContainer (LIFOHeap k a) (k, a) where
330330

331331
-- | @findIterate found next initial@ takes an initial seed value and applies
332332
-- @next@ to it until either @found@ returns True or @next@ returns @Nothing@
333-
findIterate :: (a -> Bool) -> (a -> Maybe a) -> a -> Maybe a
334-
findIterate found next initial
333+
findIterate :: (a -> Maybe a) -> (a -> Bool) -> a -> Maybe a
334+
findIterate next found initial
335335
| found initial = Just initial
336-
| otherwise = next initial >>= findIterate found next
336+
| otherwise = next initial >>= findIterate next found

test/Algorithm/SearchSpec.hs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,39 +57,39 @@ spec :: Spec
5757
spec = do
5858
describe "bfs" $ do
5959
it "performs breadth-first search" $
60-
bfs (cyclicUnweightedGraph Map.!) (== 4) [] 0
60+
bfs (cyclicUnweightedGraph Map.!) [] (== 4) 0
6161
`shouldBe` Just [1, 4]
6262
it "handles pruning" $
63-
bfs (cyclicUnweightedGraph Map.!) (== 4) [odd] 0
63+
bfs (cyclicUnweightedGraph Map.!) [odd] (== 4) 0
6464
`shouldBe` Just [2, 6, 4]
6565
it "returns Nothing when no path is possible" $
66-
bfs (cyclicUnweightedGraph Map.!) (== 4) [odd, (== 6)] 0
66+
bfs (cyclicUnweightedGraph Map.!) [odd, (== 6)] (== 4) 0
6767
`shouldBe` Nothing
6868
describe "dfs" $ do
6969
it "performs depth-first search" $
70-
dfs (cyclicUnweightedGraph Map.!) (== 4) [] 0
70+
dfs (cyclicUnweightedGraph Map.!) [] (== 4) 0
7171
`shouldBe` Just [3, 2, 8, 5, 4]
7272
it "handles pruning" $
73-
dfs (cyclicUnweightedGraph Map.!) (== 4) [odd] 0
73+
dfs (cyclicUnweightedGraph Map.!) [odd] (== 4) 0
7474
`shouldBe` Just [2, 6, 4]
7575
it "returns Nothing when no path is possible" $
76-
dfs (cyclicUnweightedGraph Map.!) (== 4) [odd, (== 6)] 0
76+
dfs (cyclicUnweightedGraph Map.!) [odd, (== 6)] (== 4) 0
7777
`shouldBe` Nothing
7878
it "handles doubly-inserted nodes" $
79-
dfs (acyclicUnweightedGraph Map.!) (==4) [] 0
79+
dfs (acyclicUnweightedGraph Map.!) [] (==4) 0
8080
`shouldBe` Just [1, 4]
8181
describe "dijkstra" $ do
8282
it "performs dijkstra's algorithm" $
83-
dijkstra (cyclicWeightedGraph Map.!) (== 'd') [] 'a'
83+
dijkstra (cyclicWeightedGraph Map.!) [] (== 'd') 'a'
8484
`shouldBe` Just (4, [(2, 'c'), (2, 'd')])
8585
it "handles pruning" $
86-
dijkstra (cyclicWeightedGraph Map.!) (== 'd') [(== 'c')] 'a'
86+
dijkstra (cyclicWeightedGraph Map.!) [(== 'c')] (== 'd') 'a'
8787
`shouldBe` Just (6, [(1, 'b'), (5, 'd')])
8888
it "returns Nothing when no path is possible" $
89-
dijkstra (cyclicWeightedGraph Map.!) (== 'd') [(== 'b'), (== 'c')] 'a'
89+
dijkstra (cyclicWeightedGraph Map.!) [(== 'b'), (== 'c')] (== 'd') 'a'
9090
`shouldBe` Nothing
9191
it "handles zero-length solutions" $
92-
dijkstra (cyclicWeightedGraph Map.!) (== 'd') [] 'd'
92+
dijkstra (cyclicWeightedGraph Map.!) [] (== 'd') 'd'
9393
`shouldBe` Just (0, [])
9494
describe "aStar" $ do
9595
let start = (0, 0)
@@ -98,10 +98,10 @@ spec = do
9898
map (\pt -> (1, taxicabDistance pt end, pt))
9999
. taxicabNeighbors
100100
it "performs the A* algorithm" $
101-
aStar next (== end) [] start
101+
aStar next [] (== end) start
102102
`shouldBe` Just (2, [(1, (1, 0)), (1, (2, 0))])
103103
it "handles pruning" $
104-
aStar next (== end) [isWall] start
104+
aStar next [isWall] (== end) start
105105
`shouldBe` Just (6, [(1, (0, 1)),
106106
(1, (0, 2)),
107107
(1, (1, 2)),
@@ -110,5 +110,5 @@ spec = do
110110
(1, (2, 0))
111111
])
112112
it "returns Nothing when no path is possible" $
113-
aStar next (== end) [isWall, \ p -> taxicabDistance p (0,0) > 1] start
113+
aStar next [isWall, \ p -> taxicabDistance p (0,0) > 1] (== end) start
114114
`shouldBe` Nothing

0 commit comments

Comments
 (0)