I've created the following implementation of a trie in F# as an effort to learn the language. I would like to know what could be done to make it more idiomatic. Any other pointers are welcome as well.
Of particular interest are:
- Are my tests sufficient? Is there a better way to construct the tests?
- Can the logic for insert be made cleaner?
- To constrain type or not for functions and parameters?
- What remains to be done to classify this as a proper collection?
type Trie<'T when 'T : comparison> = {
IsTerminal: bool;
Children: Map<'T, Trie<'T>>;
}
module Trie =
let empty = {
IsTerminal = false;
Children = Map.empty;
}
let rec insert branch trie =
match branch with
| [] ->
{ trie with IsTerminal = true; }
| el::els ->
let child =
match Map.tryFind el trie.Children with
| Some t -> t
| None -> empty
let child' = insert els child
let children =
if Map.containsKey el trie.Children
then Map.remove el trie.Children
else trie.Children
let children' = Map.add el child' children
{ trie with Children = children' }
let rec contains branch trie =
match branch with
| [] -> trie.IsTerminal
| el::els ->
match Map.tryFind el trie.Children with
| Some t -> contains els t
| None -> false
let rec toSeq trie =
seq {
for KeyValue(label, trie) in trie.Children do
if trie.IsTerminal then yield [label]
let children = toSeq trie
for child in children do
yield label :: child
}
module Test =
let ``insert can insert char into root`` =
let result = empty |> insert ['a']
Map.containsKey 'a' result.Children
do assert ``insert can insert char into root``
let ``insert can insert char into multiple levels`` =
let result = empty |> insert ['a'..'d']
Map.containsKey 'd' (((result.Children |> Map.find 'a').Children |> Map.find 'b').Children |> Map.find 'c').Children
do assert ``insert can insert char into multiple levels``
let ``contains finds branches that exist in tree`` =
let result = empty |> insert ['a'..'d']
contains ['a'..'d'] result
do assert ``contains finds branches that exist in tree``
let ``contains does not find incomplete branches`` =
let result = empty |> insert ['a'..'d']
result |> contains ['a'..'c'] |> not
do assert ``contains does not find incomplete branches``
let ``contains does not find extended branches`` =
let result = empty |> insert ['a'..'d']
result |> contains ['a'..'e'] |> not
do assert ``contains does not find extended branches``
branch
is a poor name choice. I'm trying to keep this implementation generic. If the consumer intends to store individual words (i.e. autocomplete), they would pass in a char array. If a consumer intends to store sentences or phrases (i.e. Markov chain) they could pass in a string array. If the consumer is intending to store bytes (i.e. compression dictionary) they could pass in a byte array. I'm not certain what to call the path in the trie which the inserted array creates, so I chosebranch
. Suggestions are quite welcome. I'm not fond of el::els either. \$\endgroup\$insert
toadd
to feel more like the other collection types \$\endgroup\$