Packages

A collection of common Search Algorithms

Current section

Files

Jump to
search_algorithms_gleam src internal generalized_search.gleam
Raw

src/internal/generalized_search.gleam

import gleam/dict.{type Dict}
import gleam/list
import gleam/result
import gleam/set.{type Set}
import internal/search_container.{type EstimateStatePair, type SearchContainer}
/// A Record that represents the current State of the search.
///
/// Generics:
/// * `state_key` is used for `==` equality for the values and keys in visited and paths respectively
/// * `state` can be anything
///
/// Properties:
/// * `current` - the `state` tied to the "cost" it took to get there
/// * `search_container` - The abstract data structure set by the type of search, used to `push` state, and `pop` back in a specific order
/// * Implementations include a `Stack`, `Queue`, and `LIFOHeap`
/// * `visited` - a `Set` of visited locations, key'd by `state_key`
/// * `paths` - a collection of how we got to `state_key` by a list of `EstimateStatePair(state)`
///
/// Notes:
/// The `Int` in `EstimateStatePair(state)` is the min-based priority needed for the LIFOHeap container.
/// It is unused by `Stack` and `Queue`, and will always be `0` for them
pub type SearchState(state_key, state) {
SearchState(
current: EstimateStatePair(state),
search_container: SearchContainer(state),
visited: Set(state_key),
paths: Dict(state_key, List(EstimateStatePair(state))),
)
}
/// recursively search through next states until end us found, or there are no more states to check
pub fn search_until_found(
get_next_states: fn(state) -> Result(state, Nil),
is_found: fn(state) -> Bool,
state: state,
) -> Result(state, Nil) {
case is_found(state) {
True -> Ok(state)
False ->
get_next_states(state)
|> result.try(search_until_found(get_next_states, is_found, _))
}
}
fn get_next_search_state(
is_better: fn(EstimateStatePair(state), EstimateStatePair(state)) -> Bool,
make_key: fn(EstimateStatePair(state)) -> key,
get_next_states: fn(EstimateStatePair(state)) ->
List(EstimateStatePair(state)),
search_state: SearchState(key, state),
) -> Result(SearchState(key, state), Nil) {
let update_queue_paths = fn(
search_container_and_paths: #(
SearchContainer(state),
Dict(key, List(EstimateStatePair(state))),
),
estimate_state_pair: EstimateStatePair(state),
) {
let #(search_container, paths) = search_container_and_paths
let key = make_key(estimate_state_pair)
case set.contains(search_state.visited, key) {
True -> #(search_container, paths)
False -> {
let assert Ok(steps_so_far) =
dict.get(search_state.paths, make_key(search_state.current))
let updated_queue =
search_container.push(search_container, estimate_state_pair)
let updated_paths =
dict.insert(paths, key, [estimate_state_pair, ..steps_so_far])
case dict.get(paths, key) {
Error(Nil) -> #(updated_queue, updated_paths)
Ok(path) -> {
// logically, paths will always contain at least one item, so this is safe
let assert [previous_estimate_state_pair, ..] = path
case is_better(previous_estimate_state_pair, estimate_state_pair) {
True -> #(updated_queue, updated_paths)
False -> #(search_container, paths)
}
}
}
}
}
}
let #(new_search_container, new_paths) = {
let next_states = get_next_states(search_state.current)
list.fold(
next_states,
#(search_state.search_container, search_state.paths),
update_queue_paths,
)
}
new_search_container
|> search_container.pop()
|> result.map(fn(tuple) {
let #(estimate_state_pair, search_container) = tuple
SearchState(
estimate_state_pair,
search_container,
set.insert(search_state.visited, make_key(estimate_state_pair)),
new_paths,
)
})
|> result.try(fn(search_state) {
case set.contains(search_state.visited, make_key(search_state.current)) {
True ->
get_next_search_state(
is_better,
make_key,
get_next_states,
search_state,
)
False -> Ok(search_state)
}
})
}
/// a clever search that, based on the container type and the is_better function,
/// can be used to do A*, Dijkstra, BFS, or DFS
pub fn generalized_search(
search_container search_container: SearchContainer(state),
make_key make_key: fn(EstimateStatePair(state)) -> state_key,
is_better is_better: fn(EstimateStatePair(state), EstimateStatePair(state)) ->
Bool,
get_next_states get_next_states: fn(EstimateStatePair(state)) ->
List(EstimateStatePair(state)),
is_found is_found: fn(EstimateStatePair(state)) -> Bool,
initial_estimate_state_pair initial_estimate_state_pair: EstimateStatePair(
state,
),
) -> Result(List(EstimateStatePair(state)), Nil) {
let initial_key = make_key(initial_estimate_state_pair)
let search_state =
SearchState(
initial_estimate_state_pair,
search_container,
set.from_list([initial_key]),
dict.from_list([#(initial_key, [])]),
)
let end_result =
search_until_found(
get_next_search_state(is_better, make_key, get_next_states, _),
fn(search_state: SearchState(state_key, state)) {
is_found(search_state.current)
},
search_state,
)
let get_steps = fn(search_state: SearchState(state_key, state)) {
let assert Ok(steps) =
dict.get(search_state.paths, make_key(search_state.current))
steps
}
result.map(end_result, fn(st) { st |> get_steps() |> list.reverse() })
}