1

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?

BitTickler
  • 10,905
  • 5
  • 32
  • 53

2 Answers2

1

I am familiar neither with the algorithm, nor with with F#, so I translated the pseudocode from Wikipedia to a purely functional variant:

function alphabeta(node, depth, α, β, maximizingPlayer)
  if depth == 0 or node is a terminal node
    return the heuristic value of node
  if maximizingPlayer
    return take_max(children(node), depth, α, β)
  else
    return take_min(children(node), depth, α, β)

function take_max(children, depth, α, β)
  v = max(v, alphabeta(head(children), depth - 1, α, β, FALSE))
  new_α = max(α, v)

  if β ≤ new_α or tail(children) == Nil
    return v
  else
    return take_max(tail(children), depth, α, β))

function take_min(children, depth, α, β)
  v = min(v, alphabeta(head(children), depth - 1, α, β, TRUE))
  new_β = min(β, v)

  if new_β ≤ α or tail(children) == Nil
    return v
  else
    return take_min(tail(children), depth, α, β))

The trick is to turn the foreach with break into a recursion with appropriate base case. I assumed that children(node) returns a cons list of nodes, which can be deconstructed using head/tail and tested for Nil.

Obviously, I can't test this, but I think it contains the right ideas (and it is almost Python...).

Also, maybe this is a case for memoization -- but that depends on the domain (which I am not familiar with). Parallelization is probably more difficult with this kind of recursion; for that, you maybe could build up a list of vs and alphas/betas in parallel (since the calls to alphabeta are probably the most expensive part), replacing the recursions with takeWhiles on those lists.

phipsgabler
  • 20,535
  • 4
  • 40
  • 60
  • Nicely done. Instead of abstracting the control flow (as I did it), you picked 2 separate functions. This is, bye the way the approach many non-wikipedia c/c++ examples look like. Minus the recursion, of course. – BitTickler Feb 16 '15 at 18:25
  • Only flaw is that the value of the node (result from previous deeper recursions) is compared to alpha and beta, not alpha with beta ...but that is no big thing. Meaning that the initial value of v is set to v_max/v_min respectively. – BitTickler Feb 16 '15 at 18:27
  • And that the min and max calls alternate (hence the name minimax) ;) – BitTickler Feb 16 '15 at 18:40
  • The v inside the definition of v is not defined in a proper way in take_max and take_min – David Oct 11 '20 at 15:59
0

A deeply functional approach is described in John Hughes, Why functional programming matters.

Moreover, you could have a look at the implementations to Russell & Norvig, Artificial Intelligence - A modern approach

David
  • 513
  • 7
  • 14