Step down

I would love to see that Kotlin would support the step-down rule in a bigger way…
The way I defined is most probably not the best way, so other options would be great.

Explicit:

fun highLevel0()  = midLevel1()  // not allowed, method not defined in StepDown
fun highLevel1() = midLevel1()  //allowed
fun highLevel2() =  lowLevel() //not allowed, move lowLevel up one level
@StepDown[highLevel1, highLevel2]{
    fun midLevel1() = ...
    @StepDown[midLevel1]{
       fun lowLevel() = ...
    }
}
My code
fun createResponse(selectedList: List<String>, include: Boolean, unselectedList: List<String>){
    listOf(
        selectedList.toSimpleResponse(positive = include),
        ListResponse(
            "Other brands",
            unselectedList.associateWith { ListItem(it) }
        ),
        Suggestion(listOf("no"))
    )
}

@StepDown[createResponse]{
    private fun List<String>.toSimpleResponse(positive: Boolean) = SimpleResponse(when{
        this.isEmpty() -> emptyResponseText()
        positive -> positiveResponseText(this)
        else -> negativeResponseText(this)
    })
    @StepDown[toSimpleResponse]{
        private fun emptyResponseText() = questions.random()
        @StepDown[emptyResponseText]{
            private val questions = listOf(
                "Do you want to filter on brand?",
                "Are there brands you are only interested in?"
            )
        }
            
        private fun positiveResponseText(selectedList: List<String>) = "According to the current filter, you only want to see"+when {
            selectedList.size == 1 -> "${selectedList.first()}."
            else -> " the brands ${selectedList.toReadableString()}."
        }+" Do you want to see more brands?"

        private fun negativeResponseText(selectedList: List<String>)= "According to the current filter, you don't want to see "+when {
            selectedList.size == 1 -> "${selectedList.first()}."
            else -> "the brands ${selectedList.toReadableString()}."
        } + "Do you want to exclude more brands?"
            
        @StepDown[positiveResponseText, negativeResponseText]{
            fun List<String>.toReadableString() = dropLast(1).joinToString() + " and " + last()
        }
    }
}

or just add stepDown:

fun highLevel1() = midLevel1()  //allowed
fun highLevel2() =  lowLevel() //not allowed, move lowLevel up one level
stepDown {
    fun midLevel1() = ...
    stepDown {
       fun lowLevel() = ...
    }
}
fun highLevel0()  = midLevel1()  // not allowed, move method above stepdown
My code
fun createResponse(selectedList: List<String>, include: Boolean, unselectedList: List<String>){
    listOf(
        selectedList.toSimpleResponse(positive = include),
        ListResponse(
            "Other brands",
            unselectedList.associateWith { ListItem(it) }
        ),
        Suggestion(listOf("no"))
    )
}

stepDown {
    private fun List<String>.toSimpleResponse(positive: Boolean) = SimpleResponse(when{
        this.isEmpty() -> emptyResponseText()
        positive -> positiveResponseText(this)
        else -> negativeResponseText(this)
    })
    stepDown {
         private fun emptyResponseText() = questions.random()
         StepDown {
             private val questions = listOf(
                 "Do you want to filter on brand?",
                 "Are there brands you are only interested in?"
             )
         }
            
        private fun positiveResponseText(selectedList: List<String>) = "According to the current filter, you only want to see"+when {
             selectedList.size == 1 -> "${selectedList.first()}."
             else -> " the brands ${selectedList.toReadableString()}."
        }+" Do you want to see more brands?"

        private fun negativeResponseText(selectedList: List<String>)= "According to the current filter, you don't want to see "+when {
            selectedList.size == 1 -> "${selectedList.first()}."
            else -> "the brands ${selectedList.toReadableString()}."
        } + "Do you want to exclude more brands?"
            
        stepDown {
            fun List<String>.toReadableString() = dropLast(1).joinToString() + " and " + last()
        }
    }
}

I never heard about this rule before. Do you know of any good articles, etc about this explaining the advantages of this? Are there any examples of this rule being applied to complex projects?
Than on the implementation side, how would this work with java interop (or would java just ignore this rule)?
Also does your second example imply that the order in which functions are defined matters?

I couldn’t find a lot of good sources and I haven’t searched for projects
I came accross it when I was watching Uncle Bobs videos, but as there isn’t real support in Java he recommends to indent the less important code, which looks very odd…
The first search result only tells how to do it…

There are 2 problems it tries to solve:

  1. There is one kind of private: classes probably have multiple private functions.
    Those private functions could belong to the entire class or just to one or a couple of functions.
    The private functions for the entire class is supported, while the private functions for a part of the class or just one method is not.
  2. A class can contain multiple abstractions. When you look into the code, you see all of these
    abstractions at the same level. The step-down rule provides a way to hide the less important functions, untill you drill into those functions. Hereby it gives a structured way of maintaining the code. in other words, it basically gives you
this

inside the code.

The second example would indeed imply the order in which the code would be defined.
This is according to the step-down rule itself, but I could understand if this isn’t the best solution.

The functions are basically more private than private. This means they aren’t seen outside the class. In other words, they are just private.

1 Like

Interesting idea. You can already do that in kotlin though.

fun foo() {
  StepDown.foo()
  StepDown.StepDown.foo()   // compiler error
}
private object StepDown {
  fun foo() {
    StepDown.foo()
  }
  private object StepDown {
    fun foo() {}
  }
}
1 Like

You’re right.
pro:

  • Tells you exactly where the object is defined, so maybe more readable?
  • It exists!!!

Improvements:

  • It should be possible to hide functions for other methods at the same abstraction: highLevel0.
  • It should make it easier to reference the methods as they are a replacement for normal functions, only more limited. Especially extension-funcitons: listOf(StepDown.run{a.foo()})
  • It needs a name: The stepdown should take as less attention as possible.

Maybe we can remove the need for explicit references by marking the functions by telling it has a util-method:

@Stepdown(StepDown::Class)
fun foo() : Unit

or the other way around:

@StepDown(this::foo)
private object StepDown

It should also not change the context (the this).

For the moment, it’s a great workaround as long as you aren’t using extension-functions.
Do you know, btw, if there is a way to automatically make those objects collapse?