For the most simple CRUD apps, there is almost zero business logic. So your whole application is basically IO. What's the point of Haskell's IO type when every function will have to be IO?
For the less simple apps, IO can be hidden behind interfaces for complex business logic. Here's an imaginary example:
def getPriceForShoppingCart(product: Product, user: User, getDiscount: (User) -> Percentage): Price {
if (user.isPremium) {
product.price * getDiscount(user)
} else {
product.price
}
}
How do we put the IO on the edges? Does this function just disappear and we have an `if` statement in our impure outer shell? That smells like business logic in the impure shell to me. Do we go ahead and call `getDiscount` in the outer shell and pass in the discount to the function? That also sucks because what if we made that trip to the database when we didn't need to (because the user is not a premium user)?
I feel like every time I try to be religious about IO at the edges and not using impure dependency injection tricks, I end up running into a bunch of cases like this.
On the other hand, if I stand back and look at a function like this objectively... It's easy to understand, it's still quite easily testable. What's the problem?