I'm trying to get more proficient with recursion schemes as they have so far been really helpful for turning gnarly explicit recursion code into something less spike-y. One of the other tools I tend to reach for when implementing algorithms that can get really confusing with explicit recursion is monad transformers / mutability. Ideally I'd like to get comfortable enough with recursion schemes such that I can ditch statefulness altogether. An example of an algorithm I'd still reach for the transformers for is minimax with alpha beta pruning. I did normal minimax with a catamorphism and minimax f-algebra ( data MinimaxF a f = MMResult a | MMState [f] Bool
), but I wasn't sure how I could extend this to do alpha beta pruning. I thought maybe I could use histomorphism, or maybe there was some custom solution with comonads, but I didn't know how to approach trying a solution using either technique.
In addition to a version of alpha beta pruning with recursion schemes any general advice you have about tackling similar problems would be much appreciated. For example I've had trouble applying recursion schemes to algorithms like Dijkstra that usually are implemented in an imperative fashion.
1条答案
按热度按时间wsewodh21#
Alpha-beta can be seen as an instance of minimax, where
min
andmax
are instantiated using a well-chosen lattice. Full gist .We represent games as a tree, where each internal node is a position in the game, waiting for a designated player to pick a move to a child node, and each leaf is a final position with its score, or value.
minimax
will work on any lattice, defined by the following class:The
Lattice
class is more general thanOrd
: andOrd
instance is aLattice
with decidable equality (Eq
). If we could redefineOrd
, then it would be appropriate to addLattice
as a superclass. But here a newtype will have to do:Here's minimax. It is parameterized by an embedding
leaf :: a -> l
of final values to the chosen lattice. One player maximizes the embedded value, the other player minimizes it.The "regular" minimax uses the scores of the game directly as the lattice:
For alpha-beta pruning, the idea is that we can keep track of some bounds on the optimal score, and this allows us to short-circuit the search. So the search is to be parameterized by that interval
(alpha, beta)
. This leads us to a lattice of functionsInterval a -> a
:An interval can be represented by
(Maybe a, Maybe a)
to allow either side to be unbounded. But we shall use better named types for clarity, and also to leverage a differentOrd
instance on each side:We will require that we can only construct
Pruning f
iff
satisfiesclamp i (f i) = clamp i (f (Bot, Top))
, whereclamp
is defined below. That way,f
is a search algorithm which may shortcircuit if it learns that its result lies outside of the interval, without having to find the exact result.Functions form a lattice by pointwise lifting. And when we consider only functions satisfying
clamp i (f i) = clamp i (f (Bot, Top))
and equate them modulo a suitable equivalence relation (Pruning f = Pruning g
ifclamp <*> f = clamp <*> g
), a short-circuiting definition of the lattice becomes possible.The
inf
of two functionsl
andr
, given an intervali = (alpha, beta)
, first runsl (alpha, beta)
to obtain a valuevl
. Ifvl <= alpha
, then it must beclamp i vl == alpha == clamp i (min vl (r i))
so we can stop and returnvl
without looking atr
. Otherwise, we runr
, knowing that the final result is not going to be more thanvl
so we can also update the upper bound passed tor
.sup
is defined symmetrically.Thus we obtain alpha-beta as an instance of minimax. Once the lattice above is defined, we only need some simple wrapping and unwrapping.
If all goes well,
alphabeta
andminimax
should have the same result: