Implement with Types, Not Your Brain
reasonablypolymorphic.com
reasonablypolymorphic.com
...
hoistStateIntoStateT
:: Sem (State s ': r) a
-> S.StateT s (Sem r) a
hoistStateIntoStateT (Sem m) = m $ \u ->
case decomp u of
Left x -> S.StateT $ \s ->
liftSem . fmap swap
. weave (s, ())
(\(s', m') -> fmap swap
$ S.runStateT m' s')
(Just . snd)
$ hoist hoistStateIntoStateT x
Right (Yo Get z _ y _) -> fmap (y . (<$ z)) $ S.get
Right (Yo (Put s) z _ y _) -> fmap (y . (<$ z)) $ S.put s
...I definitely need to use my brain to read that code though.
Haskell feels a lot like that. That code snippet gave me the same feeling of having to climb a conceptual mountain to appreciate the metaphorical view at the top. I have no doubt that there is something beautiful there, but of all of the conceptual, programming related mountains I need to climb, the language abstraction one is pretty low priority, because the language I am using really is not my limiting factor. I appreciate that others who care spend a lot of time thinking about this problem, but Haskell's got a long way to go before I'm convinced that it's worth using in everyday contexts.
I also find that code quite abstract, it looks to be solving some kind of problem that usually the language designer (if say you are in C#) would be solving. Haskell seems to be something that leaks it's mechanics into your code, where as C# keeps it for the most part nicely separated from your code, albeit with the effect of giving the programmer less power. No typeclasses etc.
"Gee, that’s complicated! I must be really smart to have
written such a function, right?
Wrong! I just have a trick!"
Seems like the author deliberately picked an example that looks hard but isn't once you understand what he wants to say.Nothing new under the sun. How about APL?
https://medium.com/@gordonguthrie/the-beam-needs-an-apl-y-la...
A professor of mine used to write commercial software in APL. (Including business apps!)
I take an old familiar tool and I instantly remember how to use it.
What gives?
When I was a boy, wise practitioners told me that being a programmer is a path of constant learning. They were right. And since then, I take learning as a natural part of the profession. I suspect it also has something to do with a high salary it commands.
Somewhere down closer to #15 is "learn Haskell," which comes after learning Rust/Go. Learning a "real" functional language would be much higher, but I already did that early in my career.
I only meant to notice that there's no impenetrable barrier to mastery of that, when desired. I don't think that to learn Haskell at a high level is harder than to do the same with C++. The latter just had more adoption for last 25 years, for a number of reasons.
In any case, some (cursory) knowledge of Haskell improved my Java, Python, and JavaScript quite a bit, and is directly applicable to Scala in many aspects.
- Harold Abelson
Looking at that code, it looks like it's working on a tree of some sort, but what kind? Something to do with Sem and State and Yo, I guess. At a wild guess, it's something to do with ASTs and DSLs or similar. But code that is dramatically inscrutable without metric shit tonnes of context, is bad code.
Really? I must be misunderstanding what you mean by "operation", or maybe this is the part of functional programming that hasn't clicked for me yet... my understanding is that the type signature tells you what kinds of things the function works with, but it doesn't tell you how the function maps the input to the output. When I hear "operation", I think of the latter. What are you referring to?
For example, if a function's type is `a -> b -> (a, b)`, there's really only one function fitting that shape: cons. What else could a (pure) function possibly do, with that typing, other than "exactly what cons does"?
There is, however, a gotcha along these lines: Haskell has a value called ‘undefined’ (along with some other issues collectively called “bottom” for CS theory reasons[1]) which can take any type. So ‘foo x y = (x, undefined)’ is a legal implementation of that function will will compile, but crash if you try to do anything with the ‘undefined’ result.
In practice, knowing your function has essentially one implementation because it’s sufficiently polymorphic (no concrete types), is still a great trick for getting the compiler to enforce certain kinds of correctness.
Do functional programmers consider the type of a variable to be more important (in some sense) than its value? If so, that could explain my confusion here, and that would be a pretty important thing to understand.
Edit: This idea -- "the type of the function completely determines the operation" -- seemed counter-intuitive to me at first, but I think that's actually a sign that it's a key concept I need to understand to really grok functional programming in a deeper way. (I think this is the case for counter-intuitiveness more generally... it's a sign that you've found the missing link in your understanding of something.) Thanks for the replies, this has been helpful.
A (somewhat faulty analogy) is imagine a method that takes in two variable odds type Object and returns a tuple of Objects. You can call any instance method on the object class, and any static method, but can't reference any class variables. And you can do any casts to or from Object. There is almost nothing left you can do but return a tuple of them. The types constrained your implementation.
Haskellers read some texts and got used to the idea of type manipulations. They know, for example, there is a type "Void" which doesn't contain any values - that is, by definition no object has this type - and therefore any function which has this type of argument can't be invoked. Because to invoke the function, it has to be passed a value of the given type, and there aren't any.
Haskellers got used to the idea that if we have function from type a to the same type a, and there are absolutely no restriction on what the type a is, then it should be identity function - they just sat on this idea, pondered it for some time and got to this conclusion. For non-Haskellers it's not clear - and even the idea that types can restrict code so tightly as to make code unique (up to isomorphisms) can be novel to them.
Does it make a bigger cognitive load? Probably not for those who got used to type properties like this. Is it hard for non-Haskellers to see this feature and try to use it? Well, many Haskellers passed through this stage, so it ought to be doable. Most importantly, can they agree on this?..
If I have ((a,b)->b), it behaves precisely the same way regardless of what those variables represent.
Do I need to do null check? Irrelevant. B is being reused, do I do a deep or a shallow copy? Does it have a copy constructor? Irrelevant.
Frame it as a game: give me a function definition and I will give you a pair of types for which that function is statically ill-defined. You must give me a function that works, without changing how it’s defined, for whatever pair of types I tell you.
Counter-intuitively, it’s actually because of ambiguity that you don’t have as much choice in how your function is implemented. If you know nothing about your types a and b, there is nothing you can do with values of those types to create new values (excluding oddities with “undefined” and the like, which I’ll ignore for the rest of this comment). The only things you can do are “structural” operations that use only some or all of the inputs you’ve been given, verbatim.
For example, given one value each of types a and b, you could have a function with a return type of a that just chose that input value and gave it back, but what other function of type a -> b -> a could you possibly have?
Now, extend that idea. Suppose you have a function of type a -> b -> (a, a). This needs to return a pair of a values, but we still only have one such value that we know. We don’t know how to make another a from an a, because we don’t know anything specific about that type. Similarly, we don’t know how to make an a from a b, or from an a and a b for that matter. So the only possible implementation of this function (with the caveat above) would be the one that returns a pair where each element is the a it was given as input.
Let’s extend that idea again. This time, suppose we have input values of types a and b, but we also have an input function of type b -> c. That is, we start with two values of ambiguous types, but we do know how to turn one of those values into a third type. In this case, we could write a function of type a -> b -> (b -> c) -> (a, c), for example, because although we don’t have an input value of type c directly, we do have a b and we know how to make a c from it. But, we have exactly one way we know to do that, so there is still only one possible implementation of a function of this type: we must take the a we started with, and we must take the b we started with and convert it to a c using the b -> c function we started with, and then we return a pair with the two final values.
When can we have more than one possible implementation? Essentially, when we have been given more than one way to make at least one of the required output values. For example, suppose we have a function of type a -> b -> (a -> c) -> (b -> c) -> (c, c). This time, we start with an a and a b, but we also know how to convert either of them to a c. We need two c values in our output, but nothing here says they have to be the same, so each of the output values could come from either of the a and b inputs, giving four possible implementations (but only four).
As a final example, as surprising at it might seem at first sight, there is no possible implementation at all of a function of type a -> b (again, excluding funny games with “undefined” and the like). We simply don’t have any way to make a b without knowing anything about the type itself and without being supplied with either a b value directly or some way to make one.
If you found that interesting and really want to blow you mind, you might enjoy the Curry-Howard isomorphism. The Wikipedia page isn’t a good introduction if you’re trying to understand it, IMHO, but Wikibooks has a nice introduction:
https://en.wikibooks.org/wiki/Haskell/The_Curry%E2%80%93Howa...
Leveraging this rules out bugs.
hoistStateIntoStateT
:: Sem (State s ': r) a
-> S.StateT s (Sem r) a
?Sorry even that'll get my brain CPU on 100% for a few minutes.
I have never user Polysemy, any effects library, or type-level lists before, but I'm reasonably sure my guess is right, despite not understanding any of the code.
The reason is simple: as a developer looking at the code written a week ago by myself, using "normal" language, I'm able to somewhat recognize what's going on, after a month or two it's something produced by aliens. With code depicted above this time will shorten to what? hours? days? Additionally it will require additional effort for developer himself or anyone else to decrypt it after a while.
Now we currently use Java here. I worked with other programming languages in companies too, for instance C and C++ which were a nightmare with no equal. Don't get me wrong, when I was in college I just loved creating crazy shit with template metaprogramming and preprocessor metaprogramming. Once you mature out of it, this is just the most craziest thing you could ever do.
The reason is simple. Even plain, stupid Java is already incredibly hard to verify during a CodeReview. Spend 2 hours in a row trying to find problems with other's people well written Java code and your brain feels like it explodes. Now with C++ that same task is almost impossible and will likely require 8 hours of your day for the same code volume. With Haskell I would go out on a limb here and say it would require 24 hours of your daily time to handle the same code volume...
Not to mention that most programmers even at Google and Amazon would not even be able to either read or write such code and people like me, who could and did in the past would just shake their heads and move on. Nobody wants to deal with such code outside of university...
Hello, I am at Amazon. I spend a decent portion of my time working with an in-house Clojure library (not entirely dissimilar to Haskell) which handles some mission critical business logic my team owns.
It terrifies me. I hate it.
For every hour I spend working with it, I spend another hour trying to convince everyone we need to rewrite it in Java, immediately, or replace it with a different library. None of us have a clue how it works. We can't debug it. The guy who wrote it quit three years ago.
I mean, it works. It works very well. But the day it doesn't work, we're not going to have a clue why that is. And then we're in real trouble.
Bottom line: Pick the language most teams at your company are familiar with. At Amazon that simply is Java for anything that is not frontend, period. Even getting into Kotlin presents a major obstacle. Clojure? Yeah right.
Edit: Just to clarify. Working in a big company is not meant as a restriction. In fact, having worked in several startups before, these would do well to adopt the same principles for programming and prohibit this "university graduate" way of developing software. In the end, if your startup is going to survive, it's going to become a big company itself someday.
Only learning corporate-approved languages is a bit shortsighted. Tech companies, development paradigms and programming languages all have shorter lifespans than humans. COBOL and Fortran, once very successful and popular languages have stopped being mainstream for a while. And those two were the survivors of their batch. Who knows how many then-popular languages have faded away to obscurity?
Also you have to keep learning even if you are to stick to the same language. Java is not 1.6 anymore. Languages keep evolving by borrowing good ideas from other languages. So why not use your learning time to stay ahead of the curve?
Edit: All languages have their places, even in big companies. Could Whatsapp and Discord have grown at the same pace while using Java/C#? BEAM is the right tooling for them and it has paid off.
Arguably, yes. The JVM and .NET CLR are both superior platforms when it comes to performance. They also both offer actor models with supervision (e.g. Akka/Akka.NET).
But see, someday, I won't be on this team anymore. And then all the time I spent all learning how this beast works will be wasted and some new person will be scratching their head at it.
That is far from accurate for more left-field languages. Should every new hire spend three months meditating and becoming one with the functional gods? Seems a bit silly, almost as silly as trying to hire a programmer that is a fit into the company, and knows the relevant domain AND programming language/ paradigm as dictated by someone long gone.
Clojure is actually very different from Haskell. The pervasive use of "dynamic" features, including the lack of a satisfactory type system, does mean that Clojure code can be expected to be inherently less reviewable, surveyable and auditable, which is exactly what you found.
The article was about "virtues of Haskell's strong type system" but you seem to have read it as "Why you should use Haskell and Type driven development in place of your existing C++ or Java Stack."
The author has published a computer science book. He probably doesn't work in a big company, but lots of people work in big companies and still appreciate FP / type theory / computational theory and forays into those realms.
Not to mention... A little bit of stuff like this and a lot of Rust makes more sense.
Yeah, look! I started with a function signature and a little _. Lo and behold, fifty seven holes later, I have a graph-coloring optimal register allocator for MIPS for my compiler back end. Don't ask me how it works, but the type system assures it.
Dear everyone, code like this is not where Haskell starts to get useful, it's useful much earlier. I won't reproduce it here but I just wrote a post with some practical code (and how haskell helped) taken from a project of mine: https://news.ycombinator.com/item?id=20260095.
If you are using Coyoneda in your code, please do not use it to extol the virtues of Haskell, except if you're sure your audience is expert haskellers/mathematicians/researchers/others who can actually gain something from the post, or if the writing is so sublime that it has reduced the complexity to near zero.
Please OP, you're actively hurting the adoption of Haskell by posting stuff like this in the wrong places/context.
The author of the article (Sandy Maguire) created a library called polysemy that improved on the efficiency of using a concept called Free Monads in Haskell -- their main benefit is that they let you write functions that deal only in your business domains. A rough corrolary would be a dependency-injected function that could not do anything outside of what the dependency injected pieces do, but also allows for different dependency implementations to be mocked out entirely within the languag (i.e. without the XML/annotation hell that is Spring DI magic).
There are plenty of submissions on HN that are aimed at specialists of a field. They get to the front page because people are curious about specialized fields. So, even though I disagree from the pits of my soul with the content of the submission (as a professional Haskell developer, I should add), I think you're being very unfair to say that stuff like this shouldn't be shared with a broader audience.
- You're absolutely right about specialist-aimed submissions on HN, it's a large part of the reason I keep coming back to HN, I should have thought about that a bit more. I think this post was a little too far into the very deep end of what haskell can do.
- Stoking curiosity about Haskell is a wanted/welcome result, and I think part of my annoyance was that this article was very unlikely to do that, in this context.
- I agree with the fact that deeply technical posts like this should be shared with a broader audience -- I didn't mean to imply that all posts shouldn't be shared, but the audience needs to be taken into account a little more. r/haskell is very different audience from HN.
While we're here, I'd love to get your opinion on polysemy -- it looks like the best thing since sliced bread for free(r) monads, the combination of the usability of freer-simple and the efficiency gains of fused-effects.
I've long secretly held the believe that mtl-style and free-monads were roughly equivalent in power and efficiency equality was only a matter of time, but I am really looking to the free approach because effect algebras seem to be a fantastic tool -- they compose more cleanly than mtl does.
> While we're here, I'd love to get your opinion on polysemy -- it looks like the best thing since sliced bread for free(r) monads, the combination of the usability of freer-simple and the efficiency gains of fused-effects. > I've long secretly held the believe that mtl-style and free-monads were roughly equivalent in power and efficiency equality was only a matter of time, but I am really looking to the free approach because effect algebras seem to be a fantastic tool -- they compose more cleanly than mtl does.
I've not tried polysemy and I don't think I will unless it comes up on the job. I'm inured to effect systems after the fifth or sixth flavor. I've heard it's nice from people who've wanted to use it though. I've written my share of production Haskell and I haven't found mtl to be as bad about composition as people tell me it is.
Now, as someone who evaluates resumes and interviews people, the only Haskell specific thing I care about is making sure the candidate knows how to use the core of the language, which is basically Haskell2010 + some indispensable extensions. I expect fancier stuff to require training, and to be honest it's sometimes better to properly contextualize that stuff for someone instead of having them come in already thinking they know how to use the footguns :^). My company asks candidates to attach or link code samples if they have them. If they don't, we ask them to write a small Reflex [0] app, because we're primarily a Reflex shop.
Now, every shop is going to be different and expect different things from their candidates. That's one reason why getting involved in the community is so useful. If you hear directly from people who employ Haskellers in your area what they want, then you can build your credentials for that. Besides that, you just gotta apply. If you're a strong candidate in another tech stack, you're a strong candidate for a Haskell stack too if you can demonstrate you can use the language. There's currently a remote job posting on r/haskell if you want to give it a shot. I wish you the best of luck :)
- https://wiki.haskell.org/Jobs
- https://angel.co/haskell/jobs (if you're into startups)
- r/haskell on reddit
I do not work in haskell at my day job but I've found writing side projects with it has done wonders for my understanding -- I've been able to work through the trends in how people are being productive with haskell (mtl, readert, free monads, vinyl, datakinds shenanigans), just by trying to make stuff with haskell.
If someone's put off learning Haskell because they are (terrifyingly!) exposed to a single obviously non-beginner's article (on HN not the Daily Mail!) -- well, they must be too incurious to be worth recruiting to the language anyway.
I know nothing about Haskell (and am unlikely ever to learn it for unrelated reasons), but I read the article, understood little of it, and survived with my interest if anything somewhat piqued.
Great things happen when barriers to entry are low, why would you want to make the barriers seem higher (despite the fact they didn't actually change)? You're going to scare off someone who could have contributed a simple driver/client/whatever (the kind of libraries that make thriving ecosystems) that doesn't require any of this type tomfoolery (I use that term jokingly not pejoratively), because they wrote off haskell early.
I don't know about 'using'. I think it just is one - learning is possible but terribly inefficient in its absence.
There's also nothing to suggest that someone who isn't curious today can't become more curious as time goes on
Very true & thanks for reminding me. That we can & do change (and must leave space for each other to) is one of the drums I like to bang now and then.
I definitely agree there -- it's definitely part of the barrier to entry, people have to start looking for other ways of doing things before they'll land on haskell since it isn't main stream.
The things from haskell that are trickling out into the mainstream should (hopefully) be pointing people back at haskell, if it's at least mentioned in passing
> Very true & thanks for reminding me. That we can & do change (and must leave space for each other to) is one of the drums I like to bang now and then.
Yeah I really felt this when I watched Sandy's talk on polysemy (https://www.youtube.com/watch?v=-dHFOjcK6pA0
There were parts he didn't quite fully understand and he mentions it
Also to temper my previous comment, I want to be clear that I don't necessarily want every single able bodied coder to necessarily join the haskell ecosystem -- I am at least a little cynical in that I believe if there exist good contributors (that you do want) there are probably bad ones (that you don't want).
Is Haskell a language or a cult?
> Sorry but this is just crazy. I always wonder if people writing blog posts like that have ever worked in a big company. You need to spend at least 2-3 hours a day doing code reviews and most of the rest you are writing code, which subsequently involves reading the code you wrote yourself and others have written.
> Now we currently use Java here. I worked with other programming languages in companies too, for instance C and C++ which were a nightmare with no equal. Don't get me wrong, when I was in college I just loved creating crazy shit with template metaprogramming and preprocessor metaprogramming. Once you mature out of it, this is just the most craziest thing you could ever do.
These impressions of Haskell are wrong Haskell is not crazy, it's actually insanely reasonable -- so reasonable in fact that it will not let you compile code that is crazy.
This is the message getting lost in crazy type signatures (which admittedly often do cool/useful things if you undertsand them) and overly verbose/complex examples.
> Hello, I am at Amazon. I spend a decent portion of my time working with an in-house Clojure library (not entirely dissimilar to Haskell) which handles some mission critical business logic my team owns.
> It terrifies me. I hate it.
I do not use/encourage use of Clojure any more, and this comparison scares me -- Haskell is not clojure, but it's getting lumped together and this really worries me because it should not be.
Haskell helps you write clear code -- type signatures, pure functions, immutable values, typeclasses, algebraic data types are all tools to make your code clearer and more obviously correct. If this isn't getting across haskell is in trouble/has a big marketing problem.
I also remember reading your comment the other day, and I thought it was great too.
Personally, I clicked through expecting to find out about some new way haskell can help me use my types to verify how implementations work (the title is fantastic in this sense, and I saw the domain), but I was instantly hit with that bit of code that's somewhat inscrutable and doesn't do enough to set up the context of what's happening and why. Then the general point of the article basically was to use polymorphism and type holes to work your way to what you need. I felt like this wasn't what I signed up for, and wondered what others thought, only to go back and see upvoted comments bashing haskell based on this one example.
This is what lead me to make my comment -- I think this article is too advanced to post here without some really good prose/writing. Not blaming sandy, as of course he doesn't control how his content spreads but I thought it just wasn't right for this context and sharing something that makes haskell seem harder than need be is damaging IMO.
The case that "haskell will tell you what types it expects" is so much easier to make without the monstrous signatures in that post, or without type level computation (the ':).
I think what's really hurting the adoption of Haskell is Haskell. This blog post represents Haskell quite well. Haskell will never be "popular", just like category theory will never be "popular".
It happens with small "misunderstood" groups. People in such groups start doing things to purposely set themselves apart and distinguish themselves from the outside majority. It seems partly subconscious or instinctual, though it's also expressed as conscious actions and can involve quite a bit of thought and rationalization.
In particular, it happens with programming communities.
> This post is not for beginners/non-haskellers, I don't understand why it's being posted here.
Plenty of interesting topics related to specific languages get posted here all the time, I am under no assumption that 100% of HN is haskellers, but there is still enough that people find it worthwhile to share and upvote on here.
> Is this intentional or are people in the haskell community this tone deaf?
Why would you blame the community on this? If anything, the need to create rifts with comments like yours, is something the community deeply needs to move on from.
The reason for the posting is that it’s an interesting exploration of type holes and their usefulness. As you may have noticed several places in this thread, the concept that the more polymorphic the type, the more restricted you actually are, seems to have peaked interest and opened some eyes to new concepts.
> It's like we're determined to prevent new people from joining.
Not at all. What people find interesting is very subjective, and one of the things I sorely miss from daily working in other languages, is being able to work with the compiler as a pal that helps you out. This post demonstrates one way to make Haskell work for you, and if you’ve honestly read the post, Sandy does a good job of explaining the concept on simpler functions.
> If you are using Coyoneda in your code, please do not use it to extol the virtues of Haskell, except if you're sure your audience is expert haskellers/mathematicians/researchers/others who can actually gain something from the post, or if the writing is so sublime that it has reduced the complexity to near zero.
Sorry, but I’m starting to feel like you didn’t read the article, but maybe just the comments here. At no point did the post care about Coyoneda. It started out with demonstrating how a complex piece of code could be reached with the method (type holes), and then went on to demonstrate the method on simpler code, removing the need to know anything else. If you focused on understanding the first piece of code, then you’ve completely missed the authors intent.
> Please OP, you're actively hurting the adoption of Haskell by posting stuff like this in the wrong places/context.
That’s a fair opinion to have, but not one I can agree with at all. With a wealth of languages to pick from, why should anyone pick Haskell? Some of what drives people’s interest are these demonstrations of what is possible, when you have a more expressive type system. That are tons of other reasons too, but as a “selling point”, it’s not “look how simple Haskell code can look”, because that is sure as sh*t like lying people straight up in their face—that’s not where Haskell differentiates itself.
I’ll close out with adding that Polysemy is very cool, and I’m personally looking forward to seeing it ready for usage :)
https://www.reddit.com/r/haskell/comments/c5dfuw/implement_w...
Surely this is the difference of (more) appropriate context & audience.
Maybe this is just a case of the loudest people on HN being the most disapproving of haskell somehow, but just about all the top level comments except for mine and the bottom one are critical of haskell because of the perceived complexity of the code that was posted.
Blah blah ...15 meters per second... blah blah ...after 10 minutes... blah blah ...how far... blah blah => aha! I probably want to multiply 15 by (10 × 60).
Does this actually work out for Haskellers? I mean, I guess I'll believe you if you tell me it does, Haskell is different enough from the kind of programming I do that I don't think I know anything at all about it.
Okay, this may be (probably is) a stupid question, because I know no Haskell whatsoever, but... if there's really only one thing it can do given the types, why does the code have to be there at all? Why can't you just write "Do the only thing you can do given these types"? If it's code humans aren't meant to read anyway, and it really is the only thing that could be done... why can't the compiler just do it without you having to put the non-human-intelligible code in your source files?
Technically there is more than one possible implementation, but you would also have to go out of your way to get it wrong. Automatically determining what constitutes "going out of your way" so that the code can be generated automatically is an active area of research (program synthesis), but as this post already shows, for "simple" cases, we're getting pretty close.
As the post mentions, there are certain dead giveaways of an incorrect implementation, such as unused variables. Conversely one may still have some rough idea of the desired code to guide the implementation. The point is not about entirely removing one's mind from the process, but to allow oneself to only think about the choices that matter, which are few.
Alternatively, another way to look at the problem is that these types, while already being quite precise, are still not as precise as they could be, because the type system is not sufficiently expressive. And even with the ability to describe the desired properties to uniquely determine the implementation:
- it may not be obvious that the implementation is in fact uniquely determined;
- a solution, unique or not, may not be easy to find automatically. Program synthesis is essentially the same problem as proof search (c.f. Curry-Howard correspondence); knowing that there is a proof of Fermat's last theorem is not sufficient to construct an actual proof of it from scratch.
For these reasons, it may still be desirable to nail down the implementation explicitly even if it is hard to read.
It's also worth considering the fact that polysemy (the package that the controversial snippet at the beginning of the post comes from) is very much an implementation of a state-of-the-art effect system using state-of-the-art features of Haskell's type system, so it can be expected that the abstractions to make this code more digestible (for the right audience) are still missing, because no one has ever thought of how to express them yet.
What if the code you wrote gave you guarantees about the things that it did, as long as you trust the compiler/proof-checker/other tools that used to verify the correctness of the code?
This is basically what testing does-it gives you proof. As long as the things you test are "pure" or functional in the sense that they will do the same thing every time, given that they are given the same inputs, you have proof that the code is correct.
The problem with testing is that you can't test everything because of combinatorial explosion. However, there's a trick to this where you can collapse states together and prove something using that collapsed state. For example, you collapse all 32-bit positive integers {1,2,3,...} into the state {N} and now as long as you prove something that holds for {N} you also proved something that holds for all 2^31-1 positive integers. You just have to be very careful and very precise about what you're doing, and that's where your compiler comes in to help.
(So this next part is kinda butchering the math behind it and all sorts of programming language theory, apologies in advance.) You can kind of think of the state {N} as a type [2], and {1,2,3,...} as instantiations of that type. You can do something like "prove" that if you're given {f(a,b) => a+b} and {a=1, b=2} then you can conclude {f(1,2) == 3}, and throw that under some test. The test instantiation where you run 'assertEquals(f(1,2), 3)' is basically a concrete instance of your proof that 'f' does what it should. You just have to trust that your runtime environment is behaving correctly (no bugs).
You can also consider functions that can be defined in your language as types (e.g. f: int -> int, a function that takes an int and gives you an int). That's the 'function signature' or 'function declaration'--it's type. And you can consider instantiations of this type to be the same thing as a function definition --where you define what the function does. As long as your function definition doesn't throw any compiler errors (that is, it passes your type-checker and there are no bugs in your toolchain), then you have provided an instance of a proof that the function definition is of the type of the function declaration. This might not sound like much ("great, the function definition has a certain type...didn't we already know that?"), but as long as your function definitions are pure functions (no external state change), you've just guaranteed type safety in your program [3]. Your program will never crash because you input a string when you should've input an int (hello JavaScript...). The type-checker won't allow you to write such a program.
You can get a lot more guarantees than what I just mentioned, and there is a ton of active research in this area [4].
So to answer your first question, yes, they are suggesting writing code that you don't necessarily understand because you "offload" parts of your understanding to the compiler (type-checker). It's like when you do an automated refactor--if your code and the auto-refactoring tool are written correctly you can just trust that it did the right thing and doesn't cause any bugs.
As for your second question, I can't answer it because I haven't used Haskell.
[1] https://en.wikipedia.org/wiki/Operational_semantics
[2] https://en.wikipedia.org/wiki/Type_theory#Basic_concepts
And then it starts with "Let's go through an example together. Consider the random type signature that I just made up:"
Hmm...I never have a type signature as a starting point, random or not. I have some sort of requirement, some sort of functionality I want out of the code. Soft fuzzy requirements.
And the types rarely if ever tell me what the function does. Heck, a function that's int x int -> int could be just about anything. How about string -> int? Does it convert the string to an integer or count the vowels?
But mostly I like Haskell's type system for the high degree of confidence it gives me while refactoring. Type Tetris is a side benefit on that, IMO.
That's because using types like "int" and "string" is not a real, valid use of a good type system. Instead, you'd typically have a function type of `newton -> sqMeter -> pascal`, or `username -> accessLevel`. All these types would be implemented through basic int and string types, but declaring them explicitly with very limited conversions inbetween would actually use type system to verify correctness of your code.
An integer can represent all sorts of things, so forever programmers have been using integers to represent all sorts of things.
If instead of “int age, int size, int color” you have “age_t age, size_t size, color_t color”, and a type system that helps keep these apart, so much confusion can be avoided...
Arithmetic of adding up kilograms and kilograms together is automatically inherited when you declare a derived type.
Types are great imo for checking against primitives because the semantics of int string array etc are strong but most languages are encouraging wrapping up primitives in some user defined wrapper then the semantics of the wrapper are implicit, brittle and known only to the author
The semantics of your Bob class is more likely to change as it's used over many different contexts than the int type is, if the int type is being swapped out it's usually because the wrong data is being used, which is what we are using types to protect against, it's not usually because there's a problem with what int is
Would refinement types help in that respect? For example, the F* language. https://www.fstar-lang.org/
I don't have any experience with "business" code, but my naive understanding is that application-level code is typically very monomorphic which is exactly where refinement types are useful, whereas the OP leverages polymorphism to a large extent (not necessarily), and that works well for general-purpose libraries which don't and must not care about their users' data.
I find it slightly silly because one still has to read such functions so it is still hard to know that they do the right thing. For example, consider the function in the article which has type:
Sem (State s ': r) a
-> S.StateT s (Sem r) a
It seems pretty obvious what this morally should do.In some cases it is possible for polymorphism to force a value of a certain type to always behave a certain way. E.g. a value of type [a] must be the empty list because that is the only value with that type. On the other hand there are multiple values of type [[a]] (I.e. an empty list or an infinite list of empty lists or anything in between). A more complicated example is that the only thing a function with the signature of compose ((b -> c) -> (a -> b) -> (a -> c)) can do is compose functions whereas a function of type (Int-> Int) -> (Int-> Int) -> (Int -> Int) can do just about anything. Outside of abstract Haskell libraries, code tends to look more like the second case than the first.
In this case it is hard to be sure that the function must behave in the correct way because of its type. I think it hinges on whether it is possible for Put to change the type of the state. If it can then types force you to always take the newly put value and return it with Get, otherwise it would be possible for Put to eg only sometimes work. So if one has to check that the code is correct, one must either read very carefully and check the code, or one must very carefully read the type signature and deduce that the code is correct.
In the first case the fact that the compiler made the code easy to write doesn’t really help much. Perl5 taught us that just because a program was easy to write it does not mean it will be easy to read/check.
In the second case, why do we have a program at all? If there is only one correct program we could have written then it seems to me that the program is in fact the type and the compiler ought to have been more clever and written it itself. (There are of course plenty of issues with that statement).
It seems to me that claiming “Haskell is great because it helps automatically write difficult functions which are impossible to check” or “Haskell is great because it makes me do the manual steps in writing a function that can only ever do one thing and mixes that pointless code in with the thing I care about” is a bit silly.
This all being said, I do think holes are a useful feature but I don’t think they are useful for entirely writing a function for you. They are particularly useful in proof assistants like Agda where one must manually exhibit a million trivial propositions and holes help fill in all the boring gaps. Agda doesn’t really suffer from the “multiple things a type could mean” issue.
I would argue that in this case it is quite easy to ensure that it does the right thing, because even if the implementation is not unique, the number of possibilities is extremely limited. In the case of `Put` you can return either the new state or the old one. It takes a single unit test to ensure it's putting the new one, with parametricity to generalize from one case to all cases, still without looking at the implementation.
> In the second case, why do we have a program at all? If there is only one correct program we could have written then it seems to me that the program is in fact the type and the compiler ought to have been more clever and written it itself. (There are of course plenty of issues with that statement).
Program synthesis being basically proof search, even after the right automation is developed it may still be more practical to write the program by hand.
> They are particularly useful in proof assistants like Agda where one must manually exhibit a million trivial propositions and holes help fill in all the boring gaps.
I think the same mechanisms and benefits are at play here in a non-dependently-typed setting, even considering "entirely writing a function for you" as an unwarranted exaggeration.
Does such a thing exist?
For example, for this post's "jonk" example I just had to copy the type into emacs, reformat it a bit into Agda syntax, then press C-c C-a and it automatically figured out the solution that the author worked through manually: λ z z₁ z₂ → z₁ (λ z₃ → z₂ (z z₃))
See the full list of commands at https://agda.readthedocs.io/en/v2.5.2/tools/emacs-mode.html
Snake Oil.
Apply this claim to any set of real world functions, however well designed they are.
This is classic Haskellism. Obsessed with the lattice of types in the room. Door closed. Real world outside.
Otherwise you hope that someone has written in a doc somewhere that bar is a string. And judging by npm docs I've seen - that is very unlikely. So you debug at runtime, head over to the github repo, or do an console.log(typeof(... hmm!
My experience with TS has been great until I need to deal with data that comes from an external system (API, client, database). Then I either have to cast the type (which can lead to bugs where compile and run time types don't match and IMO isn't an option server-side) or I have to effectively duplicate my types by writing type guard functions.
>Change a front-end React prop, then follow static type errors through the statically typed API, into the backend, all the way down into the database where they force a change in the Postgres schema. The system won't even compile unless React can talk all the way down to Postgres.
https://twitter.com/garybernhardt/status/1140695491685933060
The success here lied in using various tools, applied pragmatically at each step according to the given context. Probably the most important tool is defensive coding and building up a system from decoupled, almost visibly-correct components. Then some nice technical tools thrown in: dynamic development with hot loading, some modest storybooking of components, a repl, good backend libraries, conscious and measured unit testing, strong build/deploy/release tools.
The ability to turn on a dime and incrementally refactor things was paramount to the success and efficiency of this development.
Then someone says we need to use strong static typing. All the way. No judicious application of this tool, we'll use it everywhere. We'll make everything cohere to the type system. This is no pragmatic choice. This is religion. You know its religion because its an ideology that pervades the whole system. There is no judicious application. The extreme tax of contending with types as your system evolves is high. I've only ever seen strong typing enthusiasts deny this truth. Will one enthusiast be honest here? You can literally stand over a strong typing enthusiasts shoulder as they spend an hour running all over their system adjusting their types for one minor change in business requirements and they'll still say, "No, no, types don't cost anything..."
Static analysis doesn't solve all your problems, but it solves enough of them to be a very useful technique.
And often -- very often -- especially in strongly typed languages -- the static checker will reject code that is otherwise valid. First point.
Second point: Statically typed PLs (especially strongly typed ones) enforce the verifications across the code, with no (at least non-ridiculous) way for the coder to be judicious about things.
Third point: statically typed languages have weaker runtime features/abilities: polymorphism, homoiconicity, true REPL (ie true read->eval), etc.
All of these points add up to MORE cognitive complexity for the coder who is trying to develop non-trivial business applications -- not less cognitive complexity.
It's not being smart. It's learning. Once you learn algebra, say, many classes of problems without algebra would be too much cognitive complexity.
Type systems are focused on one thing: type coherence. But the real world demands runtime dynamics are what the customer is paying for; being as close to that need as possible IS lower cognitive impedance.
I've worked with a ton of Python code that looks like this:
def foo(bar):
# do stuff
return baz(bar)
What does `foo()` return? I have to go read the body (or docstring if I'm lucky) of `baz()` to know that. And `baz()` might be another level of indirection to `quux()`. If I change what `baz()` returns, now I need to grep my codebase for all call sites of `baz()` and verify that the new return type is acceptable at each of them, and make changes if not. This is super time-consuming and error-prone, especially when a compiler (especially with an IDE refactoring tool) could do take care of it for me in seconds. It's easier if I have a good test suite, but that means I'm just implementing static type checking with runtime tests, which is more code that I have to maintain.Why does the blame always go to lack of types?
Whether or not you have types you're still going to suffer if you don't have the other things I mention. And if you have the other things I mention then static typing become much more of a nuisance. Ergo...
Here's how you know what I'm saying is true. Because you can go across the landscape of systems in the real world and see both crappy systems falling over and rock-solid systems humming along happily -- and the differentiation between these two categories is NOT a type system. It's one thing: it's who wrote it and what was their level of experience and what was their value system.
I actually agree with your second paragraph, and I like to think the systems I build fall in the latter category, but I'm still not too proud to take whatever help the compiler can give me.
- It made you top heavy and more likely to crash
- It reduced your speed in half
The tacit implication (or elephant in the room) when someone argues for types and the presumed safety they bring is the severe cost they impose. Now they save some costs too, for sure. But the balance that I see is far, far in the direction that they save much less than they cost. (When applied universally as they almost always are when applied.)That said, even if I have to write type guards at the edges (which I'd do in JS anyway, just because you aren't using types doesn't mean you don't need to enforce schemas), I think being able to rely on compiler typechecking inside the codebase boundary is really nice for making module interactions more predictable.
There are some really great things about types as documentation, though:
1) It's machine-checked, so it's probably not out of date or mistaken, which can be worse than no documentation.
2) The interaction with my code is checked, so if I misunderstood the compiler often has my back.
3) With inference, I can often ask for docs for code I just wrote.
None of this is to say that there aren't usually other forms of documentation that should be produced.
(a -> b -> c) -> b -> a -> c
The more concrete the type signatures become the less they become documenting.
A signature of a -> a defines the id function because the type is so generic. A function of String->String can do pretty much anything.
A type signature of Seed -> String -> Hash is pretty enlightening!
1. Call the function 2. error 3. infinite recursion
Technically correct - there are infinite possible error messages.
But we could potentially have some list of exceptions.
So one valid implementation would be: If c is string we always return hello world elseif a, b and c are integers we add the two ints. Elseif a and b are the same type we call the function with the argument order swapped. And finally, if none of these are the case, we call call the function with the arguments.
Dynamic types are sometimes called “tags” to distinguish the case in which this information _does_ exist at runtime.
How about calling the function derivative?
Sounds like you could have used real documentation.
How would you write this with no constraints on the types involved?
In any case this is precisely the trouble I believe the typer enthusiast get into. You can keep pulling this yarn, refining your types to match the world as it exists today; meanwhile the dynamic typers forgot about this function days ago and are eating your lunch in productivity.
To be concrete about it, that's exactly what I'm suggesting.
Please demonstrate, in any programming language, a derivative function that is parametrically polymorphic - that is, the same machine code will handle absolutely any type that's thrown at it - floating point, integral, string, function, map, set, graph, etc. I do not believe it's possible, unless you have some very different notion of derivative in mind. I would be delighted to learn it is possible, and mildly pleased to learn of a reasonable use of the phrase "derivative function" that's other than what I'm thinking and which makes sense of what you've written above.
It's a lesson that could be better understood by parts of the Haskell community, to be sure, but it's not always clear how useful a tool can be when you don't really know how to use it.
And here, you have repeatedly shown that you really don't know what you're talking about - take the opportunity to learn.
For one.
Now tell me: are types the best documentation?
f :: (a -> b -> c) -> b -> a -> c
result in f (+) 1 2
evaluating to "cookie"
? For one, the types (obviously, to me at least) don't work out.You're pattern matching on a number and returning a string. The function might be handed a function and expected to return a file handle for all you know from inside the function.