I used to read and write a lot of Scala and a bit of Haskell code which claims to be very similar to what I think you're mentioning here. There people also start with defining the domain in interfaces (algebras, eDSLs) and data types.
In the end it's still the same indirection and abstraction as in any other Java or Go codebase, and it prevents the developer from easily accessing the actual logic of the program.
What I find difficult to understand here is that the logic of the program, in the case of a Haskell-like language, is encoded in the types.
I don’t need to look at the definition of a Monoid instance for a type (unless in rare cases it matter for some reason). I know there’s an identity element of the type and there’s a binary relation that combines elements of the type. Any type that’s a lawful Monoid works the same way.
And it goes up from there.
Denotational design is significantly different from an operational design. It’s not surprising that most programmers are taught and tend to think in terms of how computations are carried out instead of what computations are needed and the ways we can compose them. I still struggle with it at times.
I classify it separately from “indirection,” because of the laws that govern a design process like that. The same rules used in algebra work in programs where you can rely on being able to substitute terms for symbols representing those terms. And algebra seems to have been a rather successful language for manipulating expressions.
Where it does break down though is at the edges where we need to interact with run time exceptions, the state of resources external to the process, etc. Even in Haskell you can write your code procedurally if you want. You won’t get the benefits of denotational design but at least you’ll still have a pretty decent type system that helps you with refactoring and extracting “logic” later on into something more understandable and easier to work with.
> at the edges where we need to interact with run time exceptions, the state of resources external to the process, etc. Even in Haskell you can write your code procedurally if you want. You won’t get the benefits of denotational design
I'd go further and say that effect systems, MTL etc. implement denotational design for procedural code. You can say exactly what your operations denote: a procedure that can throw exceptions only, update state only, a combination of both but not I/O, etc. etc.
In the end it's still the same indirection and abstraction as in any other Java or Go codebase, and it prevents the developer from easily accessing the actual logic of the program.