This weekends programming fun of mine was to write a 300 lines reversi program in F#. It will probably take a few more weekends to find out how to get alphabeta search parallelized and this is actually out of scope for this question.
What I found, though was that I could not come up with some "pure functional" way to implement alphabeta function. I.e. without any mutable state.
Any good ideas for that?
The only idea which came to my mind would be to write something like Seq.foldUntil() function, where the accumulator state is used to store the changes in state. And which can be canceled by the lambda function passed in.
Maybe looking like this:
let transformWhile<'t,'s,'r> (transformer : 's -> 't -> 's * 'r * bool ) (state : 's) (sequence : 't seq) : 'r seq
Here the impure alphabeta function...
let rec alphabeta depth alpha beta fork (position : ReversiPosition) (maximize : bool) : (SquareName option * int) =
match depth with
| 0 -> (None, snd (eval position))
| _ ->
let allMoves =
allSquares
|> Seq.map (fun sq -> (sq,tryMove (position.ToMove) sq position))
|> Seq.filter (fun pos -> match snd pos with | Some(_) -> true | None -> false )
|> Seq.map (fun opos -> match opos with | (sq,Some(p)) -> (sq,p) | _ -> failwith("only Some(position) expected here."))
|> Array.ofSeq
let len = allMoves.Length
match len with
| 0 -> (None, snd (eval position))
| _ ->
if maximize then
let mutable v = System.Int32.MinValue
let mutable v1 = 0
let mutable a = alpha
let b = beta
let mutable i = 0
let mutable bm : SquareName option = None
let mutable bm1 : SquareName option = None
while (i<len) && (b > a) do
let x,y = alphabeta (depth-1) a b false (snd allMoves.[i]) false
bm1 <- Some(fst allMoves.[i])
v1 <- y
if v1 > v then
bm <- bm1
v <- v1
a <- max a v
if b > a then
i <- (i + 1)
(bm,v)
else
let mutable v = System.Int32.MaxValue
let mutable v1 = 0
let a = alpha
let mutable b = beta
let mutable i = 0
let mutable bm : SquareName option = None
let mutable bm1 : SquareName option = None
while (i<len) && (b > a) do
let x,y = alphabeta (depth-1) a b false (snd allMoves.[i]) true
bm1 <- Some(fst allMoves.[i])
v1 <- y
if v1 < v then
bm <- bm1
v <- v1
b <- min b v
if b > a then
i <- (i + 1)
(bm,v)
While waiting for answers, I decided to give my transformWhile idea a try and this is what became of it:
module SeqExt =
let rec foldWhile<'T,'S,'R> (transformer : 'S -> 'T -> 'S * 'R * bool ) (state : 'S) (sequence : seq<'T>) : 'R option =
if (Seq.length sequence) > 0 then
let rest = (Seq.skip 1 sequence)
let newState, resultValue, goOn = transformer state (Seq.head sequence)
if goOn && not (Seq.isEmpty rest) then
foldWhile transformer newState rest
else
Some(resultValue)
else
None
Some interactive testing showed that it works for some trivial stuff, so I decided to write a new version of alphabeta, which now looks like this:
let rec alphabeta depth alpha beta fork (position : ReversiPosition) (maximize : bool) : (SquareName option * int) =
match depth with
| 0 -> (None, snd (eval position))
| _ ->
let allMoves =
allSquares
|> Seq.map (fun sq -> (sq,tryMove (position.ToMove) sq position))
|> Seq.filter (fun pos -> match snd pos with | Some(_) -> true | None -> false )
|> Seq.map (fun opos -> match opos with | (sq,Some(p)) -> (sq,p) | _ -> failwith("only Some(position) expected here."))
let len = Seq.length allMoves
match len with
| 0 -> (None, snd (eval position))
| _ ->
if maximize then
let result = SeqExt.foldWhile
( fun (state : int * int * SquareName option * int ) move ->
let curAlpha,curBeta,curMove,curValue = state
let x,y = alphabeta (depth-1) curAlpha curBeta false (snd move) false
let newBm,newScore =
if y > curValue then
(Some(fst move), y)
else
(curMove,curValue)
let newAlpha = max curAlpha newScore
let goOn = curBeta > newAlpha
((newAlpha,curBeta,newBm,newScore),(newBm,newScore),goOn)
) (alpha,beta,None,System.Int32.MinValue) allMoves
match result with
| Some(r) -> r
| None -> failwith("This is not possible! Input sequence was not empty!")
else
let result = SeqExt.foldWhile
( fun (state : int * int * SquareName option * int ) move ->
let curAlpha,curBeta,curMove,curValue = state
let x,y = alphabeta (depth-1) curAlpha curBeta false (snd move) true
let newBm,newScore =
if y < curValue then
(Some(fst move), y)
else
(curMove,curValue)
let newBeta = min curBeta newScore
let goOn = newBeta > curAlpha
((curAlpha,newBeta,newBm,newScore),(newBm,newScore),goOn)
) (alpha,beta,None,System.Int32.MaxValue) allMoves
match result with
| Some(r) -> r
| None -> failwith("This is not possible! Input sequence was not empty!")
Is that looking like something you functional programming pros would do? Or what would you do?
While the brute force search I had before was tail recursive (no call stack building up), this pure functional version is no longer tail recursive. Can anyone find a way to make it tail recursive again?