enumeration-0.1.0: src/Data/Enumeration/Traversal.hs
-- Copyright (c) 2014 Eric McCorkle. All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
--
-- 1. Redistributions of source code must retain the above copyright
-- notice, this list of conditions and the following disclaimer.
--
-- 2. Redistributions in binary form must reproduce the above copyright
-- notice, this list of conditions and the following disclaimer in the
-- documentation and/or other materials provided with the distribution.
--
-- 3. Neither the name of the author nor the names of any contributors
-- may be used to endorse or promote products derived from this software
-- without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS''
-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
-- TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
-- PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS
-- OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
-- USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
-- OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
{-# OPTIONS_GHC -Wall -Werror -funbox-strict-fields #-}
-- | Functionality for traversing an 'Enumeration'.
--
-- This module provides a typeclass, 'Traversal', which represents a
-- traversal scheme for 'Enumeration's. Traversals should be largely
-- independent of the 'Enumeration', though some variants like
-- 'Prioritized' cannot be wholly independent.
--
-- Three 'Traversal' instances are provided by this module:
-- 'DepthFirst', 'BreadthFirst', and 'Prioritized'. All traversals
-- work by maintaining a set of positions, which consist of an
-- 'Enumeration' and an index (the next value to give as a first path
-- element). At each step, some position is \"expanded\" by replacing
-- it with two positions: one with the index as an additional prefix
-- element, and one with the index incremented.
--
-- 'DepthFirst' is a simple depth-first scheme. It is not guaranteed
-- to reach all elements, and in some 'Enumeration's, it may never
-- produce an answer.
--
-- 'BreadthFirst' is a breadth-first scheme, which tends to be
-- better-behaved than 'DepthFirst'. It explores paths in ascending
-- order of path length. For 'Enumeration's with infinite-sized
-- branches, its behavior is not strictly breadth-first (as this would
-- never yield an answer), but it should still behave well for this
-- case.
--
-- 'Prioritized' is a scheme that uses a scoring function to rank
-- paths. At each step, it will select the \"best\" (highest-ranked)
-- option and expand it.
module Data.Enumeration.Traversal(
Traversal(..),
DepthFirst,
BreadthFirst,
Prioritized,
scoring,
mkPrioritizedTraversal
) where
import Data.Enumeration
import Data.Enumeration.Traversal.Class
import Data.Heap(MaxPrioHeap)
import Data.Sequence(Seq, (<|), (|>), ViewL(..), viewl)
import qualified Data.Heap as Heap
import qualified Data.Sequence as Sequence
-- | Depth-first traversal. Note that this style of traversal is not
-- guaranteed to be complete (it may deep-dive and never visit some
-- possibilities). However, this implementation should continue
-- producing results even with infinite-sized branches, so long as the
-- depth of any one path isn't too great.
newtype DepthFirst ty = DepthFirst { dfStack :: [(Enumeration ty, Integer)] }
instance Traversal DepthFirst where
mkTraversal enum = DepthFirst { dfStack = [(enum, 0)] }
getNext DepthFirst { dfStack = [] } = Nothing
getNext DepthFirst { dfStack = (enum, curr) : rest } =
case numBranches enum of
-- For a leaf, produce a result
Just 0 -> Just (fromPath enum [], prefix enum,
DepthFirst { dfStack = rest })
Just high
-- If we are exhausting the current step, proceed directly to
-- the next level
| curr + 1 >= high ->
getNext DepthFirst { dfStack = (withPrefix enum [curr], 0) : rest }
-- Otherwise, keep it on the stack.
| otherwise ->
getNext DepthFirst { dfStack = (withPrefix enum [curr], 0) :
(enum, curr + 1) : rest }
-- When there's a step with infinite branches, go ahead and
-- jettison the rest of the stack; we'll never get to it
-- anyway.
Nothing ->
getNext DepthFirst { dfStack = [(withPrefix enum [curr], 0),
(enum, curr + 1)] }
-- | Breadth-first traversal. This style of traversal is guaranteed
-- to be complete- that is, it will visit every possibility
-- eventually. However, it may take a very long time to reach any
-- given possibility.
newtype BreadthFirst ty =
BreadthFirst { bfQueue :: Seq (Enumeration ty, Integer) }
instance Traversal BreadthFirst where
mkTraversal enum = BreadthFirst { bfQueue = Sequence.singleton (enum, 0) }
getNext BreadthFirst { bfQueue = queue } =
case viewl queue of
(enum, curr) :< rest ->
case numBranches enum of
-- For a leaf, produce a result
Just 0 -> Just (fromPath enum [], prefix enum,
BreadthFirst { bfQueue = rest })
Just high
-- If we are exhausting the current head of the queue, remove it
| curr + 1 >= high ->
getNext BreadthFirst { bfQueue = rest |>
(withPrefix enum [curr], 0) }
-- Otherwise, keep it on the queue
| otherwise ->
getNext BreadthFirst { bfQueue = ((enum, curr + 1) <| rest) |>
(withPrefix enum [curr], 0) }
-- If there's a step with infinite branches, cycle it to the
-- back of the queue, so we don't deep-dive into it.
Nothing ->
getNext BreadthFirst { bfQueue = rest |>
(withPrefix enum [curr], 0) |>
(enum, curr + 1) }
EmptyL -> Nothing
-- | Prioritized traversal. Will always pick the highest-scored
-- option. Completeness depends entirely on the scoring function.
data Prioritized ty =
Prioritized {
-- | The scoring function used in a 'Prioritized' traversal scheme.
scoring :: !((Enumeration ty, Integer) -> Float),
priHeap :: !(MaxPrioHeap Float (Enumeration ty, Integer))
}
-- | Create a prioritized traversal with a given scoring function.
mkPrioritizedTraversal :: ((Enumeration ty, Integer) -> Float)
-- ^ The scoring function to use.
-> Enumeration ty
-- ^ The enumeration to use.
-> Prioritized ty
mkPrioritizedTraversal scorefunc enum =
let
initial = (enum, 0)
scored = (scorefunc initial, initial)
in
Prioritized { scoring = scorefunc, priHeap = Heap.singleton scored }
inverseDepth :: (Enumeration ty, Integer) -> Float
inverseDepth (enum, curr) =
case numBranches enum of
Just finitemax -> -(fromIntegral (length (prefix enum)) -
(fromIntegral curr / fromIntegral finitemax))
Nothing -> -(fromIntegral (length (prefix enum)))
instance Traversal Prioritized where
mkTraversal = mkPrioritizedTraversal inverseDepth
getNext pri @ Prioritized { scoring = scorefunc, priHeap = heap } =
case Heap.view heap of
Just ((_, (enum, curr)), rest) ->
case numBranches enum of
-- For a leaf, produce a result
Just 0 -> Just (fromPath enum [], prefix enum,
pri { priHeap = rest })
-- If we're exhausting the current step, don't keep it in the heap
Just high | curr + 1 >= high ->
let
newelem = (withPrefix enum [curr], 0)
scored = (scorefunc newelem, newelem)
withNew = Heap.insert scored rest
in
getNext pri { priHeap = withNew }
-- Otherwise, insert the incremented current and the new
-- branch into the heap.
_ ->
let
newelem = (withPrefix enum [curr], 0)
scored = (scorefunc newelem, newelem)
increment = (enum, curr + 1)
incscored = (scorefunc increment, increment)
withNew = Heap.insert scored rest
withInc = Heap.insert incscored withNew
in
getNext pri { priHeap = withInc }
Nothing -> Nothing