What sold me on applicatives was being able to write "traverse", and similar operations, that would be m * n if you had to write them for each (traversable, applicative) pair but can be m + n instead ( http://m50d.github.io/2013/01/16/generic-contexts.html ). That's a very concrete code saving. Having three examples of things that form categories is great - but can we show a real-world function that we can write that takes a generic category, that does something useful with each of these three examples? That would demonstrate that we're getting real value (i.e. a code saving) out of the abstraction.
The generic benefit of a category is the proof that it satisfies the identity and associativity laws. This lets you write "generic proofs" that work for any category.
For example, take the following example type:
newtype Example f c a b = Example (f (c a b))
instance (Applicative f, Category c) => Category (Example f c) where
id = Example (pure id)
Example f . Example g = Example (liftA2 (.) f g)
You can prove that if `c` satisfies the `Category` laws and `f` satisfies the `Applicative` laws, then `Example f c` also satisfies the `Category` laws.
This in turn lets you take any `Category` and keep extending it with as many `Applicative`s as you want in any order, confident in the knowledge that your extended `Category` still satisfies the `Category` laws.
This is a common pattern in code that is organized around category theory and/or abstract algebra and I wrote a post showing a simpler example of this phenomenon (using `Monoid` instead of `Category`): http://www.haskellforall.com/2014/07/equational-reasoning-at...
You're just telling me how the abstraction gives rise to more abstractions. That seems to be getting even further away from where the rubber hits the road. I want to see a program that does something directly useful, something there's an actual business case for, that would take more code to implement without the Category abstraction.
Categories are just an utterly pared-down notion of composability, so most of the time you don't even recognize them in the wild (like the associativity of monads, which on an abstract level says that only the sequence of instructions matters, but not their grouping - it's something that you expect to be true in any imperative language!), or only recognize the absence of categories as horrible design.
If you haven't already, you should play around with the pipes library, because it makes very heavy use of categories and it's an extreme positive example of composability.
As another practical example, Haskell's class constraints form a thin category, with constraint entailment as arrows. This means that instance resolution never depends on ordering of imports, local bindings, or basically anything other than the raw set of available instances, and also any two instances at the same type must be equal. This buys us great scalability and refactorability, most notably in comparison to Scala, where alphabetizing module imports sometimes breaks programs.
That said, in category theory one rarely works with vanilla categories but rather other abstractions formulated in categorical language. Similarly, in Haskell vanilla categories aren't as useful as the many well-known categorical abstractions (basically, the core Functor - Applicative - Monad hierarchy is directly category-theory inspired, and they've proven to be insanely useful).
I don't think the class example works, because you wouldn't operate on a class hierarchy in code. Pipes is all well and good but unless I'm implementing it myself I don't need to care about the examples.
If you want to sell this to ordinary programmers you need to take another step down to the concrete level. Show two generic functions that take a category and show how they are useful for two different categories (heck, I think I can think of one myself: it should be easy to write a function that takes a type-aligned list [c a b, c b d, ... c y z] and combines them as a single c a z). If it's a useful abstraction then that really shouldn't be hard.
We want to call them one after another, and return a failure if any of them fails, success if they're all succesful
I didn't find that example particularly motivating since I can easily think of a simpler solution: exceptions. (Alternatively, if I was using C, it'd be setjmp()/longjmp(), which perform a very similar style of control flow.)
Exceptions are a more complex solution. They're much harder to reason about, and you can't abstract over them because they're special-cased in the language. I've literally seen Java libraries offer an interface something like this:
<T> T doSomething(Callback<T> callback)
<T, E extends Exception> T doSomethingThrow(CallbackThrow<T, E> callback) throws E
and even that doesn't properly cover the cases, because ideally you'd like to be able to pass a callback that throws two or more unrelated exceptions as well. Setjmp/longjmp is "simple" like goto is simple; if you want to refactor a function that uses them you have to think very carefully.
If we have \/, the possibly-error is just a value. We can use it with generic functions in the normal way. We can refactor them in the obvious way and it will just work correctly, and the type system can assure us we've done it right.
Are they really? I'd like to see the actual code which gets generated from that ultra-abstract solution, since I already know what creating an exception frame or setjmp/longjmp turns into.
If we have \/, the possibly-error is just a value.
If you use pointers, then an error can also be signified with a value (0), and there's also this very simple but possibly slightly less efficient way (in the success case):
It's only "simpler" if you approach programming from a low-level, non-functional perspective, which seems to be where you come from. The parent is talking about complexity in terms of solution complexity (avoiding undesireable states and writing fail-proof software), not in number of machine-code instructions generated, or not having to learn a new abstraction.
The people interested in functional programming and compile-time guarantees want to work at higher-levels of abstraction,
where Exceptions are just hard to typecheck crutches that effectively implement the Option/Either types but outside of control of you compiler (at the call stack level).
> Are they really? I'd like to see the actual code which gets generated from that ultra-abstract solution, since I already know what creating an exception frame or setjmp/longjmp turns into.
Do you know the micro-ops that that "actual code" will be executed as? The voltages that will pass through each logic gate? Or do you trust that the processor will behave the way that it's specified to and not worry about how it actually implements it?
Have you seen how Smalltalk does control flow? if or while are just ordinary (virtual) functions that take a function as an argument. If you understand the concept of passing a function to a function, and the concept of calling a virtual function, then you don't need to have a separate concept for if or while, they're just ordinary functions written in the language that you can read the code of. That to me is real simplicity, and either gives you the same thing.
> If you use pointers, then an error can also be signified with a value (0), and there's also this very simple but possibly slightly less efficient way (in the success case)
I like that. Genuinely. And at an implementation level that's pretty much what Option is. There are a couple of awkwardnesses: the vns would have to be declared beforehand, making it possible to use them uninitialized (though I guess that's always a danger in C?), and you can't extend it to something like Either where your error can contain data, but as a first cut it's pretty cool.
The advantage of using an applicative or monad over that is that you get access to an existing library of functions. E.g. traverse (apply a possibly-error function to each element of a list, returning either a list of successful values or a single error), or the various control flow operators like ifM (execute one or other of two possibly-error functions, where the boolean we use to decide might already have been an error). The standard library could define all these functions just for possibly-zero-pointer, but it's nicer to define them generically so that they can be reused for possibly-error, async-operation, operation-with-programatically-accessible-log, operation-depending-on-value-I'll-supply-later, operation-that-needs-to-be-inside-a-database-transaction and so on, in the same way that e.g. sort is defined generically rather than just on integers.
At a more advanced level you can also define your own generic operations. E.g. I have a generic Report class; one of the subclasses uses async I/O (because it needs to call a web API) and also needs to record some statistics about the rows, another does the per-row piece of the report in a way that might fail, another needs to write to the database on each row. By writing my (abstract) Report class in terms of a generic monad (with abstract methods that return that monad), I can reuse the same logic for all these implementations.
What sold me on applicatives was being able to write "traverse", and similar operations, that would be m * n if you had to write them for each (traversable, applicative) pair but can be m + n instead ( http://m50d.github.io/2013/01/16/generic-contexts.html ). That's a very concrete code saving. Having three examples of things that form categories is great - but can we show a real-world function that we can write that takes a generic category, that does something useful with each of these three examples? That would demonstrate that we're getting real value (i.e. a code saving) out of the abstraction.