<@U1BMD3T29> no problem with asking again - just m...
# http4k
d
@arocnies no problem with asking again - just means we weren't good enough at explaining last time! 🙂 I'll concentrate on Filters in this explanation, because they are the main instigator of this pattern. It all starts with the Filter definition. In out original "Server as a Function" vision, we originally had Filter as a typealias of
(HttpHandler) -> HttpHandler
with the
then
methods being extension functions. This seemed cleaner, as HttpHandler was also only a typealias. Unfortunately, when you came to define a Filter, you'd need to explicitly declare the various types:
Copy code
typealias Filter = (HttpHandler) -> HttpHandler

val myFilter = { next: HttpHandler ->
    { req: Request -> next(req) }
}
This was mildly annoying (and I'm not sure it was the only reason), so we changed Filter to be an interface which extended
(HttpHandler) -> HttpHandler
and added an invoke() method to take the function:
Copy code
val myFilter2 = Filter { next ->
    { next(it) }
}
Overall we thought this read better. Onto the Filter
objects
- these were originally simple functions that existed at the top level (ie. not nested inside another object). However, we noticed that trying to refer to those top-level functions didn't automatically work in IntelliJ for: 1. Using auto-complete - it currently seems to work with "ctrl+space" if the function is declared in your local project, but not from external JARs. So within the http4k project itself it would be fine, but within an app which uses the http4k JARs, "ctrl+space" would not work. This was somewhat mitigated by grouping the filters under parent
object
instances - such as
ServerFilters
and
ClientFilters
. 2. Navigation to the source of a filter - both using "open class" command and switching between an implementation and the related unit test. I realise that you can use "symbol search" for this in IntelliJ - but there is a lot of noise in the list since everything is listed and you inevitabily have to type/scroll more than you need 🙂 To solve this, we had 2 options: 1. Convert each of the functions to (nested) classes:
Copy code
class OverrideContent(private val message: String) : Filter {
    override fun invoke(p1: HttpHandler) = { p1(it).body(message) }
}
We started with this but it bugged because: a. the requirement to define all the inputs as
private val
- this gets pretty tedious. b. the name of parameter "p1" in the Handler - and Intellij bugs you if it's not
p1
. We haven't found a way to name this parameter that works. c. the type of that class is forever
OverrideContent
and not
Filter
- eg. If I've got an instance of
OverrideContent
and I want to add it as a parameter to a function, IntelliJ automatically chooses to type the parameter as
OverrideContent
and not
Filter
.
OverrideContent
has no other methods or state to access - I can't think of a reason why you'd ever want to refer to it as anything other than as a
Filter
. 2. Convert each of the functions to nested `objects`:
Copy code
object OverrideContent {
    operator fun invoke(message: String) = Filter { next ->
        { next(it).body(message) }
    }
}
This achieved the same as the class version in terms of discoverability, but seemed cleaner/more compact and also kept the "identity" of the resultant object confined to being a Filter. You can also have helper functions scoped to just that object (although that also works for the class-based version). tl; dr - It's primarily for code-navigation limitations in the IDE + a bit of laziness, but there are a couple of other considerations.