6 ms·
This way of writing Haskell, from my experience, results in write-only code. While it's nice that the compiler helped you figure out which obscure operators to
by q3k 7y ago
This way of writing Haskell, from my experience, results in write-only code. While it's nice that the compiler helped you figure out which obscure operators to use for your code to actually compile, it sucks for the next person trying to find the bug in your code. And just because the types check out doesn't mean it's free from bugs.
Code is also meant to be read and reasoned about by humans, not just by the compiler.
- thestoicattack 7y agoI agree that the result, while cool, is not readable. An obvious next step would be to demonstrate how the type-checked lambda-heavy implementation can be refactored into something that also makes sense to a human. > just because the types check out doesn't mean it's free from bugs That's certainly true in the general case. In fact, the author gives a type-checked-but-wrong example of "we need an Int, so just return zero." But in some specific cases, as the author writes, "One of those facts we can infer [from a type signature] is often the the only possible implementation." The article demonstrates that "possible" includes "also makes use of all the values available (or else the signature would be simpler)."
- eterps 7y agoIn less expressive statically typed functional programming languages this way of writing works better IMO.
- _bxg1 7y agoRich Hickey once said something (with regards to TDD) like, "We have guard rails on highways, but that doesn't mean we just take our hands off the wheel because the rails will keep us on the road. Guard rails don't know where we actually want to go. We drive the car where we're trying to go, and the rails are only there for when things go wrong." I think a version of this applies to static types as well. Even under normal circumstances, I've noticed at times my own thoughts getting lazy when refactoring statically-typed code. I don't "load" as much of the program into my brain as I would otherwise; I just change the part I want to change and then fairly thoughtlessly fix all the type errors. I don't think this is a good thing. The OP takes this to an absolute extreme. Static types are there to document and to catch bugs early; they cannot be used as a complete verification that your code is correct, much less a generative tool for writing logic where "you're not entirely sure how" it works.
- jerf 7y ago"I don't think this is a good thing." How good a thing it is depends on how right you are when you do it. If your language is statically typed, but not very well, then it's a bad thing. But it's not just abstractly a bad thing because you are a lazy person who didn't choose to do useless cognitive work, it's a bad thing because you may be wrong. To give a specific example, if your type system forcibly adjoins NULL to all pointer types, you can't afford to take your hands off the wheel when dealing with pointer types and neglect that case. You must think about it when using pointer types. On the other hand, if you take your hands off the wheel for a moment, but you perform only type system manipulations that you have good and correct reasons to believe won't affect the behavior of the underlying code... then what's wrong with that? I'm guessing you've used the former type of language more than the latter? Me too. I think you've turned the justifiable fear you feel in that sort of language into a heuristic, which makes it easy to then forget the underlying problems. But there's no particular virtue to having to understand more of the program to do a manipulation than having to understand less. Proof: If that was always true, then that recurses on itself until you shouldn't ever make a change until you understand the whole program completely in your head at once (if not the whole system it is running on). Even if that's true, it doesn't matter; we humans don't have the cognitive capacity for that, we must window our understandings. It isn't just the micro task of refactoring one particular function; it's completely common for me to undertake a type-driven refactoring of code where just the type-driven aspects are plenty to fill my cognitive window. Especially since I'm usually in a looser language and I really have to watch those darned forcibly-adjoined NULLs. If I'm somehow "obligated" to understand more, the result isn't that I'm going to have that understanding, the result is that my hand will be tied and I won't be able to do that refactoring because it'll blow out my cognitive budget.
- _bxg1 7y ago> we must window our understandings I'm not arguing with that. I'm only saying that we shouldn't assume "the types are enough", so long as we have more "cognitive window" available. Here's why: add :: Integer -> Integer -> Integer add x y = x + y add :: Integer -> Integer -> Integer add x y = x - y The type system doesn't have any idea which of these is correct. So if the programmer is "asleep at the wheel", it's still entirely possible for their logic to come out wrong. All I'm saying is we shouldn't needlessly ignore the actual logic in favor of "letting the compiler write it for us". That's what the OP is expressly advocating for.
- saityi 7y agoIt works best if you follow up the compiler assistance with a human-readability pass. Use the techniques in this post to guide you to a solution, which I find immeasurably helpful, then rewrite it by pulling it out piece by piece to make it readable with let/where blocks that describe the various steps.
- whateveracct 7y agoIn my Haskell experience, there's no substitute for Taste :)
- agentultra 7y agoCode is also meant to be read and reasoned about by humans, not just by the compiler. And thinking with types helps you to do that. Satisfying the type checker doesn't guarantee a program is functionally correct but it does reject a large number of obviously incorrect programs from being considered. And with holes it can guide use towards the likely correct program. From TFA: > I’m not going to say that blindly filling in type holes always works, but I’d say maybe 95% of the time? It's a tool. If you don't know how to implement something a good first approximation is understanding it's shape and filling in the holes. I think this is the spirit of what the article is demonstrating. Writing the program so that it's clear is the next step. The final example, `zoop` is renamed to `foldr` once we realize what we're working with. This is how I often work with Haskell in practice: I know the types and shapes of values I need to produce and I work with the compiler to write an expression that fits the type. Then I rewrite for clarity and intent.
- SquishyPanda23 7y agoI think to some extent, the preference of some Haskellers to write obscure code is orthogonal to having the types help your development. Most Haskell code I read in the wild is borderline incomprehensible because of things like poor naming choices. Typically I can rewrite the code to be clearer, but it's a lot of effort. I think Haskell would benefit from some style guidelines that emphasize that your code ought to be understandable by others. That would at least counteract the natural tendency to believe that terseness is a virtue or a goal. You can write incomprehensible C too. It's just that most people have stopped doing that.
- continuational 7y agoIt's quite common to see one or two letter variables in C too unfortunately, and typically they will have the most unhelpful type like "int" that says very little about it.
- chowells 7y agoI think you are talking about a different domain than the author. The author is talking about writing plumbing code - the parts where everything is reusable and multipurpose. In contrast, you seem to be focusing on the business logic portion of the domain where all complexity is necessary. In the reusable abstraction space, what you're talking about just doesn't happen. There aren't bugs that need to be hunted down by careful inspection in these things because the methods described in the article are sufficient to catch them. They'll fail to type check or have obvious flaws of the sort -Wall catches. In the business logic domain, there is a lot more room for errors that can't be caught by those approaches. But code in that domain also can't be written like this in the first place, so it's kind of a strange criticism to say "the code I can't write like this contains bugs that are hard to track down when I try to write it like this." I can sense one additional objection coming: "If this isn't for business logic, what value is it? I write business programs." To that, I would answer that most programs are stuffed full of redundant non-business logic, and you don't even realize how bad it is. It took roughly 30 years of c-like languages for people to be fully on-board with the idea that 3-clause for loops are error-prone boilerplate separate from the business logic and worth abstracting out. If you use Haskell enough you'll realize a lot of your code in most languages is there to fiddle with implementation details instead of working over the actual problem space. A lot of Haskell code takes the form of super-generic abstraction plumbing bits that serve to isolate a common pattern and make sure the edge cases are covered. Those bits of code are the ones usually called "impossible to understand", which are fully described by their types, and which can be implemented with this approach.
- whateveracct 7y ago> Those bits of code are the ones usually called "impossible to understand", which are fully described by their types, and which can be implemented with this approach. Once I let go of needing to understand how a piece of code turns into CPU instructions, I found that this code is actually the easiest to understand!
- kccqzy 7y agoEven these code that has nothing to do with business logic benefits from being readable, from being written with a purpose in mind. This is so that even when a typed hole suggests something fishy, you can immediately spot it. This is coming from something who tried that approach and had a bug in production.
- whateveracct 7y agoIf you think that about the first example in this blog post, then I think you're looking at the wrong part. This is code within a library that does one specific abstract thing. The term-level isn't important - look at the type signature. I am familiar with free monads/extensible effects, so I know the idioms at play in that 1-argument function. We're taking an extensible effect, "peeling" State off it (this is an idiom of EEs) and returning the rest of the effects within StateT. I can now this is a way to interop between the State effect in the effect stack & the StateT monad transformer. I don't give 2 shits about the implementation - I can use this to solve my problems just fine. EDIT: HAHA - I just realized that this function is called "hoist" which is exactly the name I would've given it based on common Haskell idioms. I didn't even read the name - just the type. That's the essence of Haskell :)
- tome 7y agoAs much as I am in favour of readable code, the point of the article is that you do not need to read the function! There is only one sensible thing that a function of that type can do. The type really is, in this case, the best technical explanation for what the code does. (One might want to add comments that help you understand the meaning but the implementation is really irrelevant.)
- agumonkey 7y agowhat about typed eDSL ? you get aided type fixing but above a human digestible ontology. only asking as I don't have experience in this
- abacate 7y agoAny language with a large enough lexical set or enough meta-programming capabilities can be classified as "write-only" from the point of view of non-experienced users. It's the everlasting battle between learning curve and expressiveness.
- cogman10 7y agoNot just Haskell. I've done a very similar thing in Java which I regret. The trick is to make sure that as soon as you've written this garbage, you go back, split things up into proper methods with descriptive names. Even things like introducing intermediate variables goes a really long way towards improving readability. To me, this is a starting point, not the finish line.
- monadic2 7y agoI don’t see this as much an effect of haskell so much as a younger culture of coding standards. I’ve certainly found trivially readable complex haskell problems before, but naming and library operators are a massive issue in the community.
- conjectures 7y ago> This way of writing Haskell, from my experience, results in write-only code. That's fine because you can enter zen state and refactor it using compiler hints ;)