Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AA Tree and tests #203

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
184 changes: 184 additions & 0 deletions src/FSharpx.Collections.Experimental/AaTree.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
namespace rec FSharpx.Collections.Experimental

open System.Collections
open FSharpx.Collections
open System.Collections.Generic

(* Implementation guided by following paper: https://arxiv.org/pdf/1412.4882.pdf *)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will be able to familiarize myself with it on Tuesday.


/// A balanced binary tree similar to a red-black tree which may have less predictable performance.
type AaTree<'T when 'T: comparison> =
| E
| T of int * AaTree<'T> * 'T * AaTree<'T>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using names for the T case data as in the Shape example here - https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions .

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Would be helpful when editing the tree itself to specify the different data types mean. (Added.)


member x.ToList() =
AaTree.toList x

interface IEnumerable<'T> with
member x.GetEnumerator() =
(x.ToList() :> _ seq).GetEnumerator()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AATree.toSeq?


interface System.Collections.IEnumerable with
member x.GetEnumerator() =
(x :> _ seq).GetEnumerator()

[<RequireQualifiedAccess>]
module AaTree =
/// O(1): Returns a boolean if tree is empty.
let isEmpty = function
| E -> true
| _ -> false

let private sngl = function
| E -> false
| T(_, _, _, E) -> true
| T(lvx, _, _, T(lvy, _, _, _)) -> lvx > lvy

/// O(1): Returns an empty AaTree.
let empty = E

let private lvl = function
| E -> 0
| T(lvt, _, _, _) -> lvt

let private nlvl = function
| T(lvt, _, _, _) as t ->
if sngl t
then (lvt - 1)
else lvt
| _ -> failwith "unexpected nlvl case"

let private skew = function
| T(lvx, T(lvy, a, ky, b), kx, c) when lvx = lvy
-> T(lvx, a, ky, T(lvx, b, kx, c))
| t -> t

let private split = function
| T(lvx, a, kx, T(lvy, b, ky, T(lvz, c, kz, d)))
when lvx = lvy && lvy = lvz
-> T(lvx + 1, T(lvx, a, kx, b), ky, T(lvx, c, kz, d))
| t -> t

/// O(log n): Returns a new AaTree with the parameter inserted.
let rec insert item = function
| E -> T(1, E, item, E)
| T(h, l, v, r) as node ->
if item < v
then split <| (skew <| T(h, insert item l, v, r))
elif item > v
then split <| (skew <| T(h, l, v, insert item r))
else node

let private adjust = function
| T(lvt, lt, kt, rt) as t when lvl lt >= lvt - 1 && lvl rt >= (lvt - 1)
-> t
| T(lvt, lt, kt, rt) when lvl rt < lvt - 1 && sngl lt->
skew <| T(lvt - 1, lt, kt, rt)
| T(lvt, T(lv1, a, kl, T(lvb, lb, kb, rb)), kt, rt) when lvl rt < lvt - 1
-> T(lvb + 1, T(lv1, a, kl, lb), kb, T(lvt - 1, rb, kt, rt))
| T(lvt, lt, kt, rt) when lvl rt < lvt
-> split <| T(lvt - 1, lt, kt, rt)
| T(lvt, lt, kt, T(lvr, T(lva, c, ka, d), kr, b)) ->
let a = T(lva, c, ka, d)
T(lva + 1, T(lvt - 1, lt, kt, c), ka, (split (T(nlvl a, d, kr, b))))
| _ -> failwith "unexpected adjust case"

let rec private dellrg = function
| T(_, l, v, E) -> (l, v)
| T(h, l, v, r) ->
let (newLeft, newVal) = dellrg l
T(h, newLeft, v, r), newVal
| _ -> failwith "unexpected dellrg case"

/// O(log n): Returns an AaTree with the parameter removed.
let rec delete item = function
| E -> E
| T(_, E, v, rt) when item = v -> rt
| T(_, lt, v, E) when item = v -> lt
| T(h, l, v, r) as node ->
if item < v
then adjust <| T(h, delete item l, v, r)
elif item > v
then T(h, l, v, delete item r)
else
let (newLeft, newVal) = dellrg l
T(h, newLeft, newVal, r)

/// O(log n): Returns true if the given item exists in the tree.
let rec exists item = function
| E -> false
| T(_, l, v, r) ->
if v = item then true
elif item < v then exists item l
else exists item r

/// O(log n): Returns true if the given item does not exist in the tree.
let rec notExists item tree =
not <| exists item tree

/// O(log n): Returns Some item if it was found in the tree; else, returns None.
let rec tryFind item = function
| E -> None
| T(_, l, v, r) ->
if v = item then Some v
elif item < v then tryFind item l
else tryFind item r

/// O(log n): Returns an item if it was found in the tree; else, throws error.
let rec find item tree =
match tryFind item tree with
| None -> failwith <| sprintf "Item %A was not found in the tree." item
| Some x -> x

let rec private foldOpt (f: OptimizedClosures.FSharpFunc<_,_,_>) x t =
match t with
| E -> x
| T(_, l, v, r) ->
let x = foldOpt f x l
let x = f.Invoke(x,v)
foldOpt f x r

/// Executes a function on each element in order (for example: 1, 2, 3 or a, b, c).
let fold f x t = foldOpt (OptimizedClosures.FSharpFunc<_,_,_>.Adapt(f)) x t

let rec private foldBackOpt (f: OptimizedClosures.FSharpFunc<_,_,_>) x t =
match t with
| E -> x
| T(_, l, v, r) ->
let x = foldBackOpt f x r
let x = f.Invoke(x,v)
foldBackOpt f x l

/// Executes a function on each element in reverse order (for example: 3, 2, 1 or c, b, a).
let foldBack f x t = foldBackOpt (OptimizedClosures.FSharpFunc<_,_,_>.Adapt(f)) x t

/// O(n): Returns a list containing the elements in the tree.
let toList (tree: AaTree<'T>) =
foldBack (fun a e -> e::a) [] tree

let toSeq (tree: AaTree<'T>) =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we provide a lazy implementation? IMO it is unexpected and surprising for the user that just getting the seq, without even using it is O(n). Maybe something along the lines of:

    let rec toSeq (tree: AaTree<'T>) =
        seq{
            match tree with
            | E -> ()
            | T(_, l, v, r) ->
                yield! toSeq l
                yield v
                yield! toSeq r
        }

?

tree |> toList |> List.toSeq

let toArray (tree: AaTree<'T>) =
tree |> toList |> List.toArray

/// O(n log n): Builds an AaTree from the elements in the given list.
let ofList collection =
List.fold (fun acc item -> insert item acc) empty collection

let ofSeq collection =
Seq.fold (fun acc item -> insert item acc) empty collection

let ofArray collection =
Array.fold (fun acc item -> insert item acc) empty collection

type AaTree<'T when 'T: comparison> with
member x.Insert(y) = insert y x
member x.Delete(y) = delete y x
member x.ToSeq() = toSeq x
member x.ToArray() = toArray x
member x.Fold(folder, initialState) = fold folder initialState x
member x.FoldBack(folder, initialState) = foldBack folder initialState x
member x.Find(y) = find y x
member x.TryFind(y) = tryFind y x
member x.IsEmpty() = isEmpty x
129 changes: 129 additions & 0 deletions tests/FSharpx.Collections.Experimental.Tests/AaTreeTest.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
namespace FSharpx.Collections.Experimental.Tests

open FSharpx.Collections
open FSharpx.Collections.Experimental
open Expecto
open Expecto.Flip
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that you are not using the flip style and that the code will compile again if you remove it.


module AaTreeTest =
[<Tests>]
let testAaTree =
testList "AaTree" [

(* Existence tests. *)
test "test isEmpty" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we divide the tests to have test per assertion/aspect and explain in the test name which aspect of isEmpty we are testing? Here I would go with something along the lines of "test isEmpty returns true for an empty aatree" and "test isEmpty returns false for a single element aatree". This is additional work for you to do now, but it will make future maintenance easier.

Expect.isTrue <| AaTree.isEmpty AaTree.empty <| ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use empty assertion messages

Expect.isFalse <| AaTree.isEmpty (AaTree.ofList [9]) <| ""
}

test "test exists" {
let tree = AaTree.ofList [9]
Expect.isTrue <| AaTree.exists 9 tree <| ""
Expect.isFalse <| AaTree.exists 10 tree <| ""
}

test "test notExists" {
let tree = AaTree.ofList [9]
Expect.isFalse <| AaTree.notExists 9 tree <| ""
Expect.isTrue <| AaTree.notExists 10 tree <| ""
}

test "test tryFind" {
let tree = AaTree.ofList ["hello"; "bye"]
Expect.equal (Some("hello")) <| AaTree.tryFind "hello" tree <| ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using AAA(arrange, act, assert) structure for the test.
Maybe:

let tree = AaTree.ofList ["hello"; "bye"]
let result = AaTree.tryFind "hello" tree
Expect.equal (Some "hello" ) result  "tryFind should find hello in aatree created from [hello; bye]"

Expect.isNone <| AaTree.tryFind "goodbye" tree <| ""
}

test "test find" {
let tree = AaTree.ofList ["hello"; "bye"]
Expect.equal "hello" <| AaTree.find "hello" tree <| ""
Expect.throws (fun () -> AaTree.find "goodbye" tree |> ignore) ""
}

(* Conversion from tests. *)
test "test ofList" {
let list = ['a'; 'b'; 'c'; 'd'; 'e']
let tree = AaTree.ofList list
for i in list do
Expect.isTrue <| AaTree.exists i tree <| ""
}

test "test ofArray" {
let array = [|1; 2; 3; 4; 5|]
let tree = AaTree.ofArray array
for i in array do
Expect.isTrue <| AaTree.exists i tree <| ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expect.containsAll?

}

test "test ofSeq" {
let seq = Seq.ofList ["hello"; "yellow"; "bye"; "try"]
let tree = AaTree.ofSeq seq
for i in seq do
Expect.isTrue <| AaTree.exists i tree <| ""
}

(* Conversion to tests. *)
test "test toList" {
let inputList = [0;1;2;3]
let tree = AaTree.ofList inputList
let outputList = AaTree.toList tree
Expect.equal outputList inputList ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it guaranteed that AaTree.toList output will be sorted?

}

test "test toArray" {
let inputArray = [|0;1;2;3|]
let tree = AaTree.ofArray inputArray
let outputArray = AaTree.toArray tree
Expect.equal outputArray inputArray ""
}

test "test toSeq" {
let inputSeq = Seq.ofList ["hi";"why";"try"]
let tree = AaTree.ofSeq inputSeq
let outputSeq = AaTree.toSeq tree
Expect.containsAll outputSeq inputSeq ""
}

(* Fold and foldback tests.
* We will try building two lists using fold/foldback,
* because that is an operation where order matters. *)
test "test fold" {
let tree = AaTree.ofList [1;2;3]
let foldBackResult = AaTree.fold (fun a e -> e::a) [] tree
Expect.equal foldBackResult [3;2;1] ""
}

test "test foldBack" {
let tree = AaTree.ofList [1;2;3]
let foldResult = AaTree.foldBack (fun a e -> e::a) [] tree
Expect.equal foldResult [1;2;3] ""
}

(* Insert and delete tests. *)
test "test insert" {
let numsToInsert = [1;2;3;4;5]
// Insert items into tree from list via AaTree.Insert in lambda.
let tree = List.fold (fun tree el -> AaTree.insert el tree) AaTree.empty numsToInsert

// Test that each item in the list is in the tree.
for i in numsToInsert do
Expect.isTrue <| AaTree.exists i tree <| ""
}

test "test delete" {
// We have to insert items into a tree before we can delete them.
let numsToInsert = [1;2;3;4;5]
let tree = List.fold (fun tree el -> AaTree.insert el tree) AaTree.empty numsToInsert

// Define numbers to delete and use List.fold to perform AaTree.delete on all
let numsToDelete = [1;2;4;5]
let tree = List.fold (fun tree el -> AaTree.delete el tree) tree numsToDelete

// Test that none of the deleted items exist
for i in numsToDelete do
Expect.isFalse <| AaTree.exists i tree <| ""

// Test that the one element we did not delete still exists in the tree.
Expect.isTrue <| AaTree.exists 3 tree <| ""
}
]