Summary of my objective: Figure out how to use continuation-passing style to avoid a stack overflow when using an algorithm I believe cannot be made tail-recursive. Alternatively, find a way to make the function tail-recursive.
Details: I am new to F# (and functional programming in general) and I am attempting to implement the minimax algorithm with alpha-beta pruning. This is an algorithm used to determine the best possible move for a two-player game. The pseudocode for the algorithm can be found here: https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning
This is a resource I've found useful for understanding the flow of the algorithm: http://inst.eecs.berkeley.edu/~cs61b/fa14/ta-materials/apps/ab_tree_practice/
One difference in my implementation is that the players for the game I'm working on do not always alternate. For this reason, I've removed one of the parameters from the function. My implementation is below:
let rec minimax node depth alpha beta =
if depth = 0 || nodeIsTerminal node then
heuristicValue node
else
match node.PlayerToMakeNextChoice with
| PlayerOneMakesNextChoice ->
takeMax (getChildren node) depth alpha beta
| PlayerTwoMakesNextChoice ->
takeMin (getChildren node) depth alpha beta
and takeMax children depth alpha beta =
match children with
| [] -> alpha
| firstChild :: remainingChildren ->
let newAlpha = [alpha; minimax firstChild (depth - 1) alpha beta] |> List.max
if beta < newAlpha then newAlpha
else takeMax remainingChildren depth newAlpha beta
and takeMin children depth alpha beta =
match children with
| [] -> beta
| firstChild :: remainingChildren ->
let newBeta = [beta; minimax firstChild (depth - 1) alpha beta] |> List.min
if newBeta < alpha then newBeta
else takeMax remainingChildren depth alpha newBeta
The problem I'm having is that while takeMax
and takeMin
are tail-recursive, these methods call minimax
when assigning newAlpha
and newBeta
so it is still likely to produce a stack overflow when I call minimax
with a large depth. I have done some research and found that using continuation-passing style is a potential way to use the heap rather than the stack when the function cannot be made tail-recursive (and I believe after many hours of trying that this cannot). While I can understand some very basic examples, I'm having trouble understanding how I would apply the concept to this situation; I'd be very grateful if someone could help walk me through it.
Edit 1: My best understanding of the solution
let minimax node depth alpha beta =
let rec recurse node depth alpha beta k =
if depth = 0 || nodeIsTerminal node then
k (heuristicValue node)
else
match node.PlayerToMakeNextChoice with
| PlayerOneMakesNextChoice ->
takeMax (getChildren node) depth alpha beta k
| PlayerTwoMakesNextChoice ->
takeMin (getChildren node) depth alpha beta k
and takeMax children depth alpha beta k =
match children with
| [] -> k alpha
| firstChild :: remainingChildren ->
let continuation = fun minimaxResult ->
let newAlpha = [alpha; minimaxResult] |> List.max
if beta < newAlpha then k newAlpha
else takeMax remainingChildren depth newAlpha beta k
recurse firstChild (depth - 1) alpha beta continuation
and takeMin children depth alpha beta k =
match children with
| [] -> k beta
| firstChild :: remainingChildren ->
let continuation = fun minimaxResult ->
let newBeta = [beta; minimaxResult] |> List.min
if newBeta < alpha then k newBeta
else takeMax remainingChildren depth alpha newBeta k
recurse firstChild (depth - 1) alpha beta continuation
recurse node depth alpha beta id