This article is from 2017. In the meantime, the Haskell community has been playing around a lot with Effect Systems, which give you similar benefits with much better performance and generally less boilerplate. In particular, the effectful package (https://hackage.haskell.org/package/effectful) would let you write the code in the article as:
reserve :: (SearchTrains :> es, GetTypology :> es, Log :> es, RequestReservation :> es) => ReservationRequest -> Eff es ReservationResult
reserve request = do
trains <- send (SearchTrain (_dateTime request)) -- Search for trains at date-time
forM trains $ \train -> -- Loop on all the trains
typology <- send (GetTypology train) -- Get the typology of a train
... -- Implement the reservation rules
send (Log "Confirming reservation")
confirmed <- send (RequestReservation reservation)
...
(In actual usage you won't tend to use the send function - you'll write wrappers that do it for you).
This lets you avoid having to write an interpreter for some particular ReservationExpr monad - instead, you specify individual effect handlers, then use them as you want. For example, we could write something like this:
-- Make search train never return any trains
runWithoutTrains :: Eff (SearchTrains ': es) a -> Eff es a
runWithoutTrains = interpret $ \_unusedHere (SearchTrain name) -> pure []
ignoreLogs :: Eff (Log ': es) a -> Eff es a
ignoreLogs = interpret $ \_unusedHere (Log _message) -> pure ()
What's even more fun is that you can actually interpose effects. So, I can write this:
loggingTrainSearches :: (Log :> es, SearchTrain :> es) => Eff es a -> Eff es a
loggingTrainSearches = interpose $ \_unusedHere (SearchTrain name) -> do
send (Log ("Searching for train " <> show name))
send (SearchTrain name)
Being able to play around with handlers like this is really useful, both for testing and for adding on niceties to business logic.
I had read this article more as a "from-first-principles" example, but it's a good note for people who might be unfamiliar with practical Haskell. Using an effects system also promotes the domain DSL from an initial-tagless language (only the interpreter is extensible) to a tagless-final language (both the language and the interpreter are extensible).
It may be worth noting that effectful, polysemy, and the other "new kids" of effects systems are not the only option: record-of-functions, mtl, and type-class effects aren't cutting edge, but they're a little more approachable from an OOP dependency injection pattern.
Hexagonal architecture along with its variants pair particularly well with a project/module based environment such as, c#, rust etc. As it is possible to define relationships between said project that sort of act like restrictions. I.e. you can define a layer that only has access to your domain and ports making it fairly obvious how to structure things. Of course this can be abused, but I would expect Haskell or most other FP languages to have the same problem if the developer is devious enough.
When paired with gos duck typing you can actually get quite nice results as well using the above mentioned approach. Though there will be quite a bit of boilerplate/glue code
Yes, a good project/module system helps, but is no panacea. As TFA said:
"As a result of having only conventions, there is no strong insurance against a developer using the dependencies directly, instead of going through the ports, or adding a new dependencies without the associated port (it happens, we all have seen it)."
As long as you have some of the same developers working on multiple layers of the application's architecture, it's easy to accidentally reintroduce violations of the layer boundaries, even if you originally wrote the application using clean, well-designed interfaces.
Say project Layer2Implementation depends on project Layer1Interface, as it should, but also depends on Layer1Implementation, possibly due to a transitive dependency that's not immediately obvious. But the build system and IDE know the project dependency is there, and now it's all too easy to refer to a Layer1Implementation internal helper function or have one suggested to you via autocomplete, even if you didn't include it in your top-of-file "open" declarations. And because you were just fixing up something else in Layer 1 an hour ago, you think to yourself "aha, I know that function, it's useful!" and plug it right in to your new Layer 2 code.
This may be why some languages are so anal about requiring explicit public/private/internal accessibility modifiers on everything, but those are highly susceptible to code rot anyway. First the real conceptual interface ends up scattered among a bunch of different files. Then under time pressure, more and more things get mindlessly marked public just so a developer can get a feature into an easily testable state. Module interface files are a pain too if they're optional (as in, say, F#).
You really want to have one tangible, named entity that groups together the functionality you actually want to expose to other layers. You want to make it impossible to refer to things you shouldn't be referring to, without making accessibility hygiene a maintenance burden in its own right. I feel like ML-style modules and abstract data types are great for this, although I've never written a sizable application in any ML dialect. The new static interface stuff in .Net is a nice step in that direction though.
I think about this a lot: what is the best way we have to build software components? And what constructs are we missing? Because I really think such a construct should be first-class in a language to denote its’ significance.
I suspect it is something close to ML modules. But I have a hard time seeing them as much better than the equivalent OOP-style of encapsulation. I figure I must be missing something but nothing seems to jump out at me upon repeated investigations.
6 comments
[ 3.0 ms ] story [ 26.9 ms ] threadThis lets you avoid having to write an interpreter for some particular ReservationExpr monad - instead, you specify individual effect handlers, then use them as you want. For example, we could write something like this:
What's even more fun is that you can actually interpose effects. So, I can write this: Being able to play around with handlers like this is really useful, both for testing and for adding on niceties to business logic.It may be worth noting that effectful, polysemy, and the other "new kids" of effects systems are not the only option: record-of-functions, mtl, and type-class effects aren't cutting edge, but they're a little more approachable from an OOP dependency injection pattern.
When paired with gos duck typing you can actually get quite nice results as well using the above mentioned approach. Though there will be quite a bit of boilerplate/glue code
"As a result of having only conventions, there is no strong insurance against a developer using the dependencies directly, instead of going through the ports, or adding a new dependencies without the associated port (it happens, we all have seen it)."
As long as you have some of the same developers working on multiple layers of the application's architecture, it's easy to accidentally reintroduce violations of the layer boundaries, even if you originally wrote the application using clean, well-designed interfaces.
Say project Layer2Implementation depends on project Layer1Interface, as it should, but also depends on Layer1Implementation, possibly due to a transitive dependency that's not immediately obvious. But the build system and IDE know the project dependency is there, and now it's all too easy to refer to a Layer1Implementation internal helper function or have one suggested to you via autocomplete, even if you didn't include it in your top-of-file "open" declarations. And because you were just fixing up something else in Layer 1 an hour ago, you think to yourself "aha, I know that function, it's useful!" and plug it right in to your new Layer 2 code.
This may be why some languages are so anal about requiring explicit public/private/internal accessibility modifiers on everything, but those are highly susceptible to code rot anyway. First the real conceptual interface ends up scattered among a bunch of different files. Then under time pressure, more and more things get mindlessly marked public just so a developer can get a feature into an easily testable state. Module interface files are a pain too if they're optional (as in, say, F#).
You really want to have one tangible, named entity that groups together the functionality you actually want to expose to other layers. You want to make it impossible to refer to things you shouldn't be referring to, without making accessibility hygiene a maintenance burden in its own right. I feel like ML-style modules and abstract data types are great for this, although I've never written a sizable application in any ML dialect. The new static interface stuff in .Net is a nice step in that direction though.
I suspect it is something close to ML modules. But I have a hard time seeing them as much better than the equivalent OOP-style of encapsulation. I figure I must be missing something but nothing seems to jump out at me upon repeated investigations.