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

dynamic breadth- and depth-first traversal #200

Merged
merged 1 commit into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ tasks.withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
}
}

detekt {
allRules = true
Expand Down
4 changes: 2 additions & 2 deletions gradle/scripts/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ performance:
active: false

style:
MandatoryBracesIfStatements:
active: false
# OptionalWhenBraces:
# active: false
OptionalUnit:
Expand All @@ -25,6 +23,8 @@ style:
# active: false
BracesOnIfStatements:
active: false
BracesOnWhenStatements:
active: false

comments:
OutdatedDocumentation:
Expand Down
132 changes: 132 additions & 0 deletions src/commonMain/kotlin/ch/tutteli/kbox/dynamicTraversal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package ch.tutteli.kbox

import ch.tutteli.kbox.impl.DynamicDepthFirstTraversalSequence
import ch.tutteli.kbox.impl.DynamicBreadthFirstTraversalSequence
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmName

/**
* Returns a single sequence containing all elements [loadElements] recursively provides for the elements of the
* original sequence as well as the elements itself if [dropRoots] = false is specified.
* The operation is intermediate and stateless.
*
* For instance, in case of a tree structure, [loadElements] loads the children, in case of a graph the connected nodes.
* The order in which the elements are returned depends on the chosen [traversalAlgorithm] as well as [dropRoots].
* (consult the documentation of the options to get more information).
*
* @param T the type of the elements in the sequence.
*
* @param revisit If set to true (default false), we no longer track visited elements and will re-[loadElements] in
* case of a revisit, potentially turning this into an infinite loop.
* Use this option only if you know that there aren't any cycles or in case the [loadElements] function is not
* referentially transparent, i.e. outcome might depend on other factors not only on input.
* @param dropRoots if set to false (default true) the resulting sequence also includes the elements of the original
* sequence.
* @param traversalAlgorithm the algorithm which should be used for traversal
* (default [TraversalAlgorithmOption.BreadthFirst]).
* @param loadElements the function returning the children of a given element in case of a tree / the connected nodes
* in case of a graph.
*
* @since 1.1.0
*/
@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
fun <T> Sequence<T>.dynamicTraversal(
revisit: Boolean = false,
dropRoots: Boolean = true,
traversalAlgorithm: TraversalAlgorithmOption = TraversalAlgorithmOption.BreadthFirst,
loadElements: (element: T) -> Sequence<T>,
): Sequence<T> = when (traversalAlgorithm) {
TraversalAlgorithmOption.BreadthFirst -> DynamicBreadthFirstTraversalSequence(
this,
loadElements,
Sequence<T>::iterator,
revisit = revisit,
dropRoots = dropRoots
)

TraversalAlgorithmOption.DepthFirst -> DynamicDepthFirstTraversalSequence(
this,
loadElements,
Sequence<T>::iterator,
revisit = revisit,
dropRoots = dropRoots
)
}

/**
* Returns a single sequence containing all elements [loadElements] recursively provides for the elements of the
* original sequence as well as the elements as such if [dropRoots] = false is specified.
* The operation is intermediate and stateless.
*
* For instance, in case of a tree structure, [loadElements] loads the children, in case of a graph the connected nodes.
* The order in which the elements are returned depends on the chosen [traversalAlgorithm] as well as [dropRoots].
* (consult the documentation of the options to get more information).
*
* @param T the type of the elements in the sequence.
*
* @param revisit If set to true (default false), we no longer track visited elements and will re-[loadElements] in
* case of a revisit, potentially turning this into an infinite loop.
* Use this option only if you know that there aren't any cycles or in case the [loadElements] function is not
* referentially transparent, i.e. outcome might depend on other factors not only on input.
* @param dropRoots if set to false (default true) the resulting sequence also includes the elements of the original
* sequence.
* @param traversalAlgorithm the algorithm which should be used for traversal
* (default [TraversalAlgorithmOption.BreadthFirst]).
* @param loadElements the function returning the children of a given element in case of a tree / the connected nodes
* in case of a graph.
*
* @since 1.1.0
*/
@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
@JvmName("dynamicTraversalIterable")
fun <T> Sequence<T>.dynamicTraversal(
revisit: Boolean = false,
dropRoots: Boolean = true,
traversalAlgorithm: TraversalAlgorithmOption = TraversalAlgorithmOption.BreadthFirst,
loadElements: (element: T) -> Iterable<T>,
): Sequence<T> = when (traversalAlgorithm) {
TraversalAlgorithmOption.BreadthFirst -> DynamicBreadthFirstTraversalSequence(
this,
loadElements,
Iterable<T>::iterator,
revisit = revisit,
dropRoots = dropRoots
)

TraversalAlgorithmOption.DepthFirst -> DynamicBreadthFirstTraversalSequence(
this,
loadElements,
Iterable<T>::iterator,
revisit = revisit,
dropRoots = dropRoots
)
}

/**
* Represents the option to which traversal algorithm shall be applied.
*
* @since 1.1.0
*/
enum class TraversalAlgorithmOption {
/** Taking the tree as example, the resulting sequence contains the result in a breath first manner,
* i.e. if `dropRoots = false` then the elements of the original sequence are returned first (if `dropRoots] = true`
* then they are skipped/dropped), then the children of those elements, then the children of the children and so on.
*
* @since 1.1.0
*/
BreadthFirst,

/**
* Taking the tree as example, the resulting sequence contains the result in a depth first manner,
* i.e. if `dropRoots = false` then the first element of the original sequence is returned first
* (if `dropRoots] = true` then this element is skipped/dropped). Next the first child of this first element is
* returned, then the first child of the first child etc.
* Once there is no first child (i.e. we reached a leaf of the tree) the sibling of this child is visited, and again
* the first child of this sibling etc.
*
* @since 1.1.0
*/
DepthFirst
}
117 changes: 117 additions & 0 deletions src/commonMain/kotlin/ch/tutteli/kbox/impl/DynamicTreeTraversal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package ch.tutteli.kbox.impl


internal class DynamicBreadthFirstTraversalSequence<T, IterableLikeT>(
private val initialSequence: Sequence<T>,
private val loadElements: (T) -> IterableLikeT,
private val iteratorProvider: (IterableLikeT) -> Iterator<T>,
private val revisit: Boolean,
private val dropRoots: Boolean,
) : Sequence<T> {
override fun iterator(): Iterator<T> = object : DynamicTreeTraversalLikeIterator<T, IterableLikeT>(
initialSequence,
loadElements,
iteratorProvider,
revisit = revisit,
dropRoots = dropRoots
) {
override fun insertIterator(loadedChildrenIterator: Iterator<T>) {
iteratorsToVisit.add(loadedChildrenIterator)
}
}
}

internal class DynamicDepthFirstTraversalSequence<T, IterableLikeT>(
private val initialSequence: Sequence<T>,
private val loadChildren: (T) -> IterableLikeT,
private val iteratorProvider: (IterableLikeT) -> Iterator<T>,
private val revisit: Boolean,
private val dropRoots: Boolean,
) : Sequence<T> {
override fun iterator(): Iterator<T> = object : DynamicTreeTraversalLikeIterator<T, IterableLikeT>(
initialSequence,
loadChildren,
iteratorProvider,
revisit = revisit,
dropRoots = dropRoots
) {
override fun insertIterator(loadedChildrenIterator: Iterator<T>) =
// put on the stack so that we visit the children next (and not the siblings)
iteratorsToVisit.add(0, loadedChildrenIterator)
}
}

internal abstract class DynamicTreeTraversalLikeIterator<T, IterableLikeT>(
initialSequence: Sequence<T>,
protected val loadElements: (T) -> IterableLikeT,
private val iteratorProvider: (IterableLikeT) -> Iterator<T>,
private val revisit: Boolean,
private val dropRoots: Boolean
) : Iterator<T> {
private val visitedElements = HashSet<T>()
private val initialIterator = initialSequence.iterator()
protected val iteratorsToVisit = mutableListOf(initialIterator)
private var peek: Option<T> = None

override fun hasNext(): Boolean = hasVisitableIterator()

override fun next(): T {
if (hasVisitableIterator().not()) throw NoSuchElementException()
return when (val p = peek) {
is None -> throw IllegalStateException(
"we just checked hasNext and now we don't have next element, concurrent access? " +
"This Iterator is not thread-safe"
)

is Some -> {
peek = None
p.value
}
}
}

@Suppress(
// remove once Kotlin supports to inline functions + tailrec, then we could move something to another method
"NestedBlockDepth",
"CognitiveComplexMethod"
)
private tailrec fun hasVisitableIterator(): Boolean = when {
peek is Some -> true
iteratorsToVisit.isEmpty() -> false
else -> {
val firstIterator = iteratorsToVisit[0]
if (firstIterator.hasNext()) {
val element = firstIterator.next()
if (revisit || element !in visitedElements) {
peek = Some(element)
if (revisit.not()) {
visitedElements.add(element)
}
loadElements(element)
.let(iteratorProvider)
.also(this::insertIterator)

if (dropRoots && firstIterator == initialIterator) {
peek = None
hasVisitableIterator()
} else {
true
}
} else {
peek = None
hasVisitableIterator()
}
} else {
// pop the stack, visited all connected nodes
iteratorsToVisit.removeAt(0)
hasVisitableIterator()
}
}
}

protected abstract fun insertIterator(loadedChildrenIterator: Iterator<T>)
}

internal sealed class Option<out T>
internal object None : Option<Nothing>()
internal class Some<T>(val value: T) : Option<T>()
Loading