The useful thing about functors isn't that they are particularly complicated, it is that they are a useful interface to be able to write things generically over. It's very similar to iterators. Being able to present items out of some collection one at a time is "so what?" It is not interesting on its own. But having a common interface over them allows for a lot of good code to be written that you can't write in a language that lacks them, and have to one-off ad-hoc all the time.
That a Parser is a Functor means you can apply all the functor tools in writing them. This is one of the appeals of parser combinators, which is again not that they are some sort of brilliant insight on their own, but that they work well with the generalized tools rather than needing specific implementations of them.
Asking “so what?” about iterators has plenty of useful answers that will aide you in programming. The same is not really true about functors.
Iterators are important... but they are not conceptually complicated. They're just a way of getting a "next element". Behind that may be a lot of complexity for some particular iterator, but that complexity should be accounted to the implementation, not the concept of iterator.
To me, the essence of asking “so what?” is primarily about importance not complexity. When you ask “so what?” You’re essentially asking “Why should I care? What can this do for me?” It doesn’t really matter whether or not the subject is complex.
Which, you know, sounds extremely trivial.
The applicative part is a bit more interesting, it means if you've got a parser that returns a function 'a -> b' and a parser that returns an input 'a' for that function then you can make a parser that returns 'b'. This would have been interesting in general categories, but in Haskell function types with currying and application isn't really a remarkable feature so preserving this structure becomes more of a necessity than an interesting feature.
That always lead to a bunch of annoyingly error-prone boilerplate. But yeah, that's it.
I'd say that half of the Haskell's value is avoiding a bunch of annoyingly error-prone boilerplate... everywhere and recursively.
But sometimes the 'mysticism' gets in the way, this article spends a lot of words on technicalities but very little on the intuition that this should be fairly trivially true.
This might make sense in the context for which this article is written, but why this article is on the front page of HN I don't understand.
After reading this article, the conclusion I drew was, "Cool, so I can `fmap` over my parser now and transform what I parse using functions."
To answer your other questions: I'm not sure it means much for the code that does the actual parsing, nor how you specify the grammar's rules, it's more about being able to transform the output using functions.
If your static analyzer is a function, you could now write `fmap staticAnalyzer myParser`.
Can't one always use functions to transform other functions' outputs?
> If your static analyzer is a function, you could now write `fmap staticAnalyzer myParser`.
rolls eyes
def compile(text):
ast = parse(text)
ir, symtable = static_analyze(ast)
asm = lower(ir, symtable)
return assemble(asm)
Ability to write that in the point-free style is really not that important, IMHO.1. Being within the same monad. For example, you can `bind` a `Parser a` to a function only if it returns `Parser b`.
2. Performing an actual action and breaking it out of its monad.
For example, if you're using the Parsec library, and you have a `Parser Int`, you can't get to that int without using a function like `parse`, performs the actual action of parsing input text.
With a functor, you can compose a `Parser a` within an `a -> b` function, instead of having to return `Parser b` in your function.
So if you have a `Parser Int`, and you want to turn it into a parser that multiplies its parsed input by 2, you can write `fmap (*2) myParser`, instead of having to write `myParser >>= \a -> return (a * 2)`.
Parsers being functors means it's easier to compose them with other things, without having to actually perform the parse until you need to.
This isn't a problem if you're the one writing the function, but if you're using a 3rd party library that doesn't know about your monad, then fmap can be very useful.
class Parser:
def __init__(self, text):
self.text = text
self.result = None
# other inners state/context fields
def parse(self):
self.parse_top_level()
return self.result
def parse(text):
return Parser(text).result
the free function "parse" throws away the inner context. When one uses parser combinators, there is no single monolithic Parser class which one may extend with whatever additional methods necessary (and maybe take some free functions as callbacks, why not); instead there are lots of smaller functions returning the leftover contexts together with the results, and you have to combine them somehow, together with free functions.> without having to actually perform the parse until you need to.
I'd rather parse my input before starting semantic analysis on it. Although if you really want to have a one-pass translator, literally being a composition three functions, `parse`, `analyze`, `emit`, with no visible intermediary structures then yeah, this allows for it.
But I'd argue that's more complicated than having three non-entwined passes with explicit, designed data structures serving as interfaces between them instead of invisibly unevaluated thunks; not to mention the sheer complexity of doing everything in one go (why do people even claim the single-pass compilers are simple?)
Honestly, if I were writing a compiler, I would go with your approach as well.
I was in front-end dev for many years and a lot of the framework features felt like that. Now I'm doing back-end devops stuff and Helm/K8s/Terraform really feel like that. Everywhere you look, it's nothing but hammer factory factory factories.