So this is a bit ranty and I hope it doesn't come off as too rude, but here goes:

Let's talk about functions. Why is id . f = f . id = f? id . f is a function which takes some argument, pushes it on the stack, calls f, pushes the result from f on the stack and calls id. This is two function calls and you go two stack levels deep. Just f is one function call and one stack level deep. If f and id are real functions executing on real hardware, then id . f is not strictly equal to f due to unavoidable side effects even if id and f are both pure functions, e.g. you may have not enough stack space to execute id . f, but you could execute just f. Real functions executing on real hardware aren't exactly the same like the pure mathematical notion of a function. If Haskell is a language for writing computer programs executing on actual computers, then id . f ≠ f; and if it isn't, then it's not very useful.

Abstractions are well and good, but I must be able to reason how the machine will work while executing my program. Take the official Python implementation - CPython for example. It's nowhere near as fast as C if you write your code naively, but there is very little surprising about it. Or take C. If you write e.g.

int id(int x) { return x; }
int f(int x) { return 2*x; }
int id_f(int x) { return id(f(x)); }

Then the compiler is free to compile id to a noop and id_f to be exactly the same as f, but it is not required. Most people won't write a program which requires that behaviour by the compiler in order to work correctly.

If the Haskell compiler is required to perform such optimisations, then I would concede id . f = f; just like tail recursion optimisation is required in Scheme, so tail recursion is actually equivalent to loops, rather than being just mathematically equivalent.

The broader point I want to make is that a computer programming language is inescapably tied to the reality of how hardware executes instructions and one must have some guarantees on how what is written will translate to machine execution.

Another thing is that once you reach a certain level of abstraction, you lose your ability to reason about the concrete things in that class. I'll illustrate what I mean.

Let's construct the set of Handleds. A handled is a thing that has a handle, and a handle is a cylindrical protrusion affixed to the object, that can be grasped by an average adult human hand and pushed or pulled along an axis perpendicular to the cylinder's height. So, a TNT detonation device is a handled, since you have a handle on the plunger you press to detonate the TNT. A door also has a handle. Your laptop bag has a handle. A table leg also technically counts as a handle and you can use it to move the table. The thing is, while you would have a theory on how to make handles, such that they would be easily operated by people, the fact that an object has a handle tells you nothing of the object, other than it can be manipulated with a hand.

Pretty much the same goes for a category. Yes, you can compose two elements of the category into a third. So... what does it do? You don't know. The problem is, the two lumps of fat that sit in your skull need to affix some concrete properties on the object in order to think about it. Functions are series of instructions that transform values. Or maybe they're relations, if you're a mathematician. But the essence of a function isn't in its composability (at least for programmers), it's in its usage as a unit of execution. Maybe also in its usage as a way to organise your source code.

There's no point in explaining something as "it's a category" any more than in a car salesman starting out by pointing all the handles on the car and how the bumpers can be grabbed as ersatz handles in case you need to get a handle on the situation. Saying something is a category is a deferred meaning that you can only understand once you know what that something is, so it's more of a mental burden than an explanation of anything.

Category properties may be simple, but their relation to the actual execution of a program is anything but. If you want to explain Haskell, maybe start off with the simple parts. Like how what you write executes on the actual machine and work your way up.