1. 41
  1. 23

    Great talk! I’ve watched a lot of Hickey’s talks over the years, and thought he might have “run out of steam” by now. But there are some new insights here that I enjoyed.

    One of his main points is that Maybe is context-dependent, and other aspects of schemas are as well. This is a great and underappreciated point.

    It’s related to a subtle but important point that Google learned the hard way over many years with respect to protocol buffers. There’s an internal wiki page that says it better, but this argument has spilled out on to the Internet:

    https://stackoverflow.com/questions/31801257/why-required-and-optional-is-removed-in-protocol-buffers-3

    In proto3, they removed the ability to specify whether a field is optional. People say that the reason is “backward compatibility”, which is true, but I think Hickey’s analysis actually gets to the core of the issue.

    The issue is that the shape/schema of data is an idea that can be reused across multiple contexts, while optional/required is context-specific. They are two separate things conflated by type systems when you use constructs like Maybe.

    When you are threading protocol buffers through long chains of servers written and deployed at different times (and there are many of these core distributed types at Google), then you’ll start to appreciate why this is an important issue.

    I think a lot of the disagreement in this thread comes down to a difference between thinking about code vs. thinking about systems.

    When you’ve spent a lot of time with long-lived, evolving systems, as Hickey evidently has, then you’ll be hesistant to repeat naivities about strict types being an unalloyed good. There are downsides to strict types, and he points out a good one here.

    1. 6

      The issue is that the shape/schema of data is an idea that can be reused across multiple contexts, while optional/required is context-specific. They are two separate things conflated by type systems when you use constructs like Maybe.

      Can you elaborate, perhaps with an example, about this conflation? Maybe it’s hard to do in isolation if this is heavily tied to large systems, but I don’t see why types like Maybe shouldn’t be thought of as something that contributes to the shape (and schema) of data. I’ve certainly always seen it that way…

      Also, are there perhaps different concerns in play when thinking about the problems that protocol buffers solve and the problems that any arbitrary type definition might solve?

      1. 15

        You can model it as part of the type system if you want, but the argument that systems become brittle and harder to maintain if you do. There is less reuse of schemas. That’s what Hickey was saying, and that matches my experience with protobufs.

        Hickey gives an example in his talk – the user/address one. It could sound trivial if you haven’t personally experienced this problem, but IME minor changes in the definition of core types have a big architectural effect on the system over time.

        Protobufs are similarly central to nearly all Google code, certainly in search and ads, and to a lesser extent Chrome/Android. In a very real sense Google code doesn’t use the type system of C++ or Java; it uses the protobuf type system. I think of it as “extending your system over the network” (which has both good and bad properties).

        I left a few years ago, but there are core types for log entries and search requests that have lasted 10-15 years. Maintaining these schemas well is important because there are terabytes/petabytes of data encoded in that format.

        There are really two separate cases – when you’re persisting protobuf data, and when you’re sending it to another server as a message. Required fields are bad in both cases. Whether something is required shouldn’t be part of the schema. It’s an orthogonal concern to the shape of the data, because every binary has different requirements.

        The schema is universal, but optional/required is relative to a particular piece of code, which you may write and deploy at different times. You can’t upgrade every binary atomically. It has to be done with backward compatibility for years and years. The point of the schemas is to share some information between different binaries operating on the same data, of course.

        The issue with protobufs is perhaps a worse version of what happens in Clojure, but it sounds like the use case of “long-lived information systems that model the real world” is very close. What I’d say is that the design decision after ~15 years of using them ended up being the same as what Hickey proposed: in proto1 and proto2 you were forced to specify whether a field was required, optional, or repeated. In proto3 the concept of optional/required went away after some debates.

        You can think of it as “every field is optional”, which how Clojure works. Every field of a map is optional, although this doesn’t necessarily characterize the way you write code. There are higher-level conventions than that.


        And as I noted in my other comment, the applicability of types is very domain-specific. So I’m not saying Maybe is bad in all cases – just that you have to think about it. People seem to get a lot out of the type system in Elm, in its very different domain of single-page apps.

        It makes a very “closed world” assumption and the benefits outweigh the costs in many those cases. (Although honestly I’m hearing a lot about the downsides of this strictness too, probably from people with more open problems.)

        If you have an open world, which lasts over many years, then you should be careful about using every bell and whistle of the type system to model the world in a fine-grained / strict fashion. I don’t think it is even limited to Maybe or required/optional, but that is a great example, and he explained it well.

        1. 6

          Interesting. Thanks for the thoughtful reply. I will definitely ruminate on it. I certainly don’t have any experience with Google scale systems, so I’m out of my depth there. I think if I were going to push back at all, I think I might try to explore whether your concerns are more applicable to a system’s serialization boundaries rather than to types in general. That is, if you have core data types shared across many processes over network boundaries, then I can definitely believe there might be different concerns there with respect to how much you use the type system. But do they apply to types that, say, don’t get directly serialized? Perhaps that’s what you were hitting on with your open/closed world distinction.

          Also, on another note, I got a chance to read the re2c paper on tagged DFAs you pointed me to. I don’t grok everything yet, but it’s got my creative juices flowing. :-) Thanks for that!

          1. 4

            I think a lot of companies and codebases have problems that Hickey’s ideas address, because the “scale” that matters isn’t necessarily the number of requests (which Google gets a lot of), but really the age of the codebase, and diversity of users, data it deals with, and systems it talks to, etc.

            I think banks and insurance companies are perfect examples. They are super old and accrete functionality over time. Amazon is apparently the same way – according to a talk by Werner Vogels I just watched, they have 3 main types that have lasted decades: users, orders, and products. Hickey’s example seems like a toy but you can imagine it on Amazon’s scale easily.

            I agree that serialization boundaries make things worse (since I said that protobufs have a “worse version” of the problem). But I think the systems that Hickey is thinking about are largely the same, from watching his talks. They are long-lived systems that model the world with data. That describes most “web” apps, and many other apps.

            I don’t think there are many closed world problems left. You might start with a closed-world problem that you can model in strict types, but as soon your application becomes successful, you have to talk to more things. There are few desktop apps that don’t talk to some kind of network service these days. Embedded systems not only talk to networks now, but their code is frequently updated!


            If I had a bunch of free time I would try writing some code in Elm, since it’s very strongly typed and people seem to like it a lot. But to me it’s a bad sign it has problems with JSON [1]. Just a little data from the outside world causes problems. (That’s what I mean by “closed world”).

            In fact strong types and external data in some way remind me of this statement I just read about blockchain.

            I always say “the moment you need to verify that a human did something outside the system, your blockchain is broken.”

            I extend it further: the moment you need to verify that something happened outside the system, your blockchain is broken.

            https://news.ycombinator.com/item?id=18573966

            Types have this problem with data !! And that is crazy. This is not theoretical – I’ve seen binaries crash in production because people didn’t realize that changing their schema and recompiling the code does not change data stored on disk. :) Yes I’m serious.

            I didn’t understand “the map is not the territory” until I had been programming for awhile. Type systems are a map of runtime behavior. They are useful up to that point. Runtime behavior is the territory; it’s how you make something happen in the world, and it’s what you ultimately care about. A lot of the arguments I see below in this thread seemingly forget that.

            (I don’t want to pile onto this, but the Pony divide by zero thing was a similar mistake… they were making the runtime behavior wrong in order to satisfy the type system (the map), which is backwards.)

            [1] https://medium.com/@eeue56/json-decoding-in-elm-is-still-difficult-cad2d1fb39ae


            Glad you found the regex paper useful! Feel free to ping me if it leads to anything. I haven’t gotten back to regexes yet but I will probably mention it on a blog post at some point, e.g. the research on derivatives and some of my experiments.

            1. 2

              Thanks again for the reply. I don’t think I quite agree with everything you’re saying, but I don’t necessarily strongly disagree either. I can definitely see your perspective. I don’t think I’ve quite come to appreciate it yet; perhaps I may not until I deal with systems at that scale. (And yeah, I totally get your point on the meaning of “scale”; we’re on the same page there.)


              the research on derivatives

              Ah right right! I re-read Repy’s paper on derivatives, and I spent some time thinking and re-crystallizing the questions I had when I read the paper the first time around (many moons ago):

              • The paper does address Unicode, but it addresses it by talking about the increased alphabet size. This is generally not how non-toy DFA implementations handle Unicode. Namely, they don’t expand the alphabet to all Unicode codepoints, they keep the alphabet restricted to 256 bytes (or even smaller, using some tricks with a match-time cost). They achieve this by building knowledge of the encoding into the automaton. This is why, for example, re2c lets you specify the encoding when building the automata. Try using -8 for example. The regex derivative paper goes out of its way to talk about large Unicode alphabets and how those can be handled well, but doesn’t talk about how the large alternations are handled that ultimately result from building, say, UTF-8 into the DFA using a normal 256-byte alphabet.
              • The paper doesn’t really talk much about other practical issues, such as finding the start of a match or encoding match priority. e.g., a|ab matches a in ab when using leftmost first semantics (e.g., Perl) but will match ab when using leftmost longest semantics (e.g., POSIX). Happily, Trofimovich dives into this topic in quite a bit of depth in his tagged DFA paper.

              Anyway, I’m not necessarily looking to restart our regex discussion. :-) I just wanted to throw these at you as two concerns that popped out at me from an implementor’s perspective. These concerns may not matter for your use case, which might make regex derivatives a good fit without needing to figure anything else out!

              1. 2

                Yeah I think it’s very domain-specific. I was going to add that I’m writing a Python VM now and C++ types work great there. I’m sure the same is true of Rust types and a regex engine.

                But VMs and regex engines are very particular kinds of software. I think the “open world” problems are very common, and existing type systems don’t handle them very well.

                Thanks for the notes on regexes. For the Oil lexer, all the significant characters are in the ASCII range, so I think I can just pass through UTF-8 and not do anything special.

                For the runtime, Unicode is one of the main reasons I am avoiding implementing my own globs and regexes for as long as possible :) I want to lean on libc a bit. Most shells actually implement their own, and it wasn’t too hard to find cases where they disagree, because character classes are a pain to implement.

        2. 6

          I realized I could have said this in a more concisely… Basically the idea is the the shape is a global property of the system. All binaries operating on the data share knowledge of the shape.

          But whether an individual field is used or not isn’t a global property. Binary X might use field A and not B; the opposite is true for binary Y. So it’s better to handle optional/required at runtime in the code for that binary, rather than in the global schema.

          This is not a pedantic point – it comes up a lot in practice. Some people are just writing a simple client-server architecture with protobufs, and it doesn’t come up then. But e.g. web search isn’t exactly client/server – there are dozens or even hundreds of binaries operating on the same protobufs. Often they look at a small subset of data and pass it on to other services.

          This was several years ago, but as far as I remember, the person who argued the most strongly against putting “required” in schemas was Kenton Varda, author of proto2 and capnproto, who was working on such servers in search. I think he wanted to get rid of the idea of optional/required in proto2, but it was eventually removed in proto3 after he stopped working on it. capnproto doesn’t have this concept either.

          The reason it’s a long argument is that there are workarounds for making things required, and probably slight (local) benefits… but the claim is that your code becomes messy over time, with duplicated schemas, bloat from required fields that aren’t actually required, generated schemas to express slight differences, etc.

          So I would say there is a tradeoff between locally reasoning about code, and evolving systems in a global fashion. The latter is more important, but you may not realize it yet … :)

        3. -1

          I agree that he’s a productive philosopher, but I think it’s funny that it’s been 13 years since he introduced Clojure and he’s still lauding RDF in his talks. We get it already!

        4. 14

          Rich has been railing on types for the last few keynotes, but it looks to me like he’s only tried Haskell and Kotlin and that he hasn’t used them a whole lot, because some of his complaints look like complete strawmen if you have a good understanding and experience with a type system as sophisticated as Haskell’s, and others are better addressed in languages with different type systems than Haskell, such as TypeScript.

          I think he makes lots of good points, I’m just puzzled as to why he’s seemingly ignoring a lot of research in type theory while designing his own type system (clojure.spec), and if he’s not, why he thinks other models don’t work either.

          1. 15

            One nit: spec is a contract system, not a type system. The former is often used to patch up a lack of the latter, but it’s a distinct concept you can do very different things with.

            EDIT: to see how they can diverge, you’re probably better off looking at what Racket does than what Clojure does. Racket is the main “research language” for contracts and does some pretty fun stuff with them.

            1. 4

              It’s all fuzzy to me. They’re both formal specifications. They get overlapped in a lot of ways. Many types people are describing could be pre/post conditions and invariants in contract form for specific data or functions on them. Then, a contract system extended to handle all kinds of things past Boolean will use enough logic to be able to do what advanced type systems do.

              Past Pierce or someone formally defining it, I don’t know as a formal, methods non-expert that contract and type systems in general form are fundamentally that different since they’re used the same in a lot of ways. Interchangeably, it would appear, if each uses equally powerful and/or automated logics.

              1. 14

                It’s fuzzy but there are differences in practice. I’m going to assume we’re using non-FM-level type systems, so no refinement types or dependent types for full proofs, because once you get there all of our intuition about types and contracts breaks down. Also, I’m coming from a contract background, not a type background. So take everything I say about type systems with a grain of salt.

                In general, static types verify a program’s structure, while contracts verify its properties. Like, super roughly, static types are whether a program is sense or nonsense, while contracts are whether its correct or incorrect. Consider how we normally think of tail in Haskell vs, like, Dafny:

                tail :: [a] -> [a]
                
                method tail(s: seq<T>) returns (o: seq<T>)
                requires s.Len > 0
                ensures s[0] + o = s
                

                The tradeoff is that verifying structure automatically is a lot easier than verifying semantics. That’s why historically static typing has been compile-time while contracts have been runtime. Often advances in typechecking subsumed use cases for contracts. See, for example, how Eiffel used contracts to ensure “void-free programming” (no nulls), which is subsumed by optionals. However, there are still a lot of places where they don’t overlap, such as in loop invariants, separation logic, (possibly existential contracts?), arguably smart-fuzzing, etc.

                Another overlap is refinement types, but I’d argue that refinement types are “types that act like contracts” versus contracts being “runtime refinement types”, as most successful uses of refinement types came out of research in contracts (like SPARK) and/or are more ‘contracty’ in their formulations.

                1. 3

                  Is there anything contacts do that dependent types cannot?

                  1. 2

                    Fundamentally? Not really, nor vice versa. Both let you say arbitrary things about a function.

                    In practice contracts are more popular for industrial work because they so far seem to map better to imperative languages than dependent types do.

                    1. 1

                      That makes sense, thanks! I’ve never heard of them. I mean I’ve probably seen people throw the concept around but I never took it for an actual thing

              2. 1

                I see the distinction when we talk about pure values, sum and product types. I wonder if the IO monad for example isn’t kind of more on the contract side of things. Sure it works as a type, type inference algorithms work with it, but the sife-effect thing makes it seem more like a pattern.

              3. 20

                I’m just puzzled as to why he’s seemingly ignoring a lot of research in type theory

                Isn’t that his thing? He’s made proud statements about his disinterest in theory. And it shows. His jubilation about transducers overlooked that they are just a less generic form of ad-hoc polymorphism, invented to abstract over operations on collections.

                1. 1

                  wow, thanks for that, never really saw it that way but it totally makes sense. not a regular clojure user, but love lisp, and love the ML family of languages.

                  1. 1

                    So? Theory is useless without usable and easy implementation

                    1. 1

                      So? Theory is useless without usable and easy implementation

                      No. Let me setup a thought experiment to show the flaw in this argument…

                      Let’s say this sequence of events happens over time:

                      1. Theory X is developed. Think of it as general theory
                      2. Theory Y, which builds on top of X, is developed. Think of it as being more specific.
                      3. An implementation of Y happens. Call it imp(Y).

                      By the logic quoted above:

                      • Theory Y is now useful because of imp(Y).
                      • Theory X is not useful because there is not yet any imp(X).

                      Such a scenario can happen when an implementer thinks something like “Theory X is quite general; I’m not sure I even can understand its terminology; I certainly don’t know how to apply it to my work.” However, once Theory Y comes along, it is easier for implementors to see the value, and, voila, you get enough interest to generate imp(Y).

                      However, Y could not have happened without X, so X must be valuable too. This leads to a contradiction: Theory X is both useful and useless.

                      The problem arises out of an overly narrow definition of “useful” (commenter above).

                      In truth, the word “useful” can mean many things in different contexts. One kind of use is implementation (i.e. a library). Another kind of use is to build additional thinking around it. Yes, I’ll say it – theory is useful. It may be common to bash on “theory”, but this is too often an unfair and misattributed attack.

                  2. 7

                    I don’t see anything of substance in this comment other than “Haskell has a great type system”.

                    I just watched the talk. Rich took a lot of time to explain his thoughts carefully, and I’m convinced by many of his points. I’m not convinced by anything in this comment because there’s barely anything there. What are you referring to specifically?

                    edit: See my perspective here: https://lobste.rs/s/zdvg9y/maybe_not_rich_hickey#c_povjwe

                    1. 3

                      That wasn’t my point at all. I agree with what Rich says about Maybes in this talk, but it’s obvious from his bad Haskell examples that he hasn’t spent enough time with the language to justify criticizing its type system so harshly.

                      Also, what he said about representing the idea of a car with information that might or might not be there in different parts of a program might be correct in Haskell’s type system, but in languages with structural subtyping (like TypeScript) or row polymorphism (like Ur/Web) you can easily have a function that takes a car record which may be missing some fields, fills some of them out and returns an object which has a bit more fields than the other one, like Rich described at some point in the talk.

                      I’m interested to see where he’s gonna end up with this, but I don’t think he’s doing himself any favors by ignoring existing research in the same fields he’s thinking about.

                      1. 7

                        But if you say that you need to go to TypeScript to express something, that doesn’t help me as a Haskell user. I don’t start writing a program in a language with one type system and then switch into a language with a different one.

                        Anyway, my point is not to have a debate on types. My point is that I would rather read or watch an opinion backed up by real-world experience.

                        I don’t like the phrase “ignoring existing research”. It sounds too much like “somebody told me this type system was good and I’m repeating it”. Just because someone published a paper on it, doesn’t mean it’s good. Plenty of researchers disagree on types, and admit that there are open problems.

                        There was just one here the other day!

                        https://lobste.rs/s/dldtqq/ast_typing_problem

                        I’ve found that the applicability of types is quite domain-specific. Rich Hickey is very clear about what domains he’s talking about. If someone makes general statements about type systems without qualifying what they’re talking about, then I won’t take them very seriously.

                    2. 6

                      seemingly ignoring a lot of research in type theory

                      I’ve come to translate this utterance as “it’s not Haskell”. Are there languages that have been hurt by “ignoring type theory research”? Some (Go, for instance) have clearly benefited from it.

                      1. 14

                        I don’t think rich is nearly as ignorant of Haskell’s type system as everyone seems to think. You can understand this stuff and not find it valuable and it seems pretty clear to me that this is the case. He’s obviously a skilled programmer who’s perspective warrants real consideration, people who are enamored with type systems shouldnt be quick to write him off even if they disagree.

                        I don’t like dynamic languages fwiw.

                        1. 3

                          I dont think we can assume anything about what he knows. Even Haskellers here are always learning about its type system or new uses. He spends most of his time in a LISP. It’s safe to assume he knows more LISP benefits than Haskell benefits until we see otherwise in examples he gives.

                          Best thing tl do is probably come up with lot of examples to run by him at various times/places. See what says for/against them.

                          1. 10

                            I guess I would want hear what people think he’s ignorant of because he clearly knows the basics of the type system, sum types, typeclasses, etc. The clojure reducers docs mention requiring associative monoids. I would be extremely surprised if he didn’t know what monads were. I don’t know how far he has to go for people to believe he really doesn’t think it’s worthwhile. I heard edward kmett say he didn’t think dependent types were worth the overhead, saying that the power to weight ratio simply wasn’t there. I believe the same about haskell as a whole. I don’t think it’s insane to believe that about most type systems and I don’t think hickey’s position stems from ignorance.

                            1. 2

                              Good examples supporting he might know the stuff. Now, we just need more detail to further test the claims on each aspect of languge design.

                              1. 3

                                From the discussions I see, it’s pretty clear to me that Rich has a better understanding of static typing and its trade offs than most Haskell fans.

                        2. 11

                          I’d love to hear in a detailed fashion how Go has clearly benefited from “ignoring type theory research”.

                          1. 5

                            Rust dropped GC by following that research. Several languages had race freedom with theirs. A few had contracts or type systems with similar benefits. Go’s developers ignored that to do a simpler, Oberon-2- and C-like language.

                            There were two reasons. dmpk2k already said first, which Rob Pike said, that it was designed for anyone from any background to pick up easily right after Google hired them. Also, simplicity and consistency making it easy for them to immediately go to work on codebases they’ve never seen. This fits both Google’s needs and companies that want developers to be replaceable cogs.

                            The other is that the three developers had to agree on every feature. One came from C. One liked stuff like Oberon-2. I dont recall the other. Their consensus is unlikely to be an Ocaml, Haskell, Rust, Pony, and so on. It was something closer to what they liked and understood well.

                            If anything, I thought at the time they shouldve done something like Julia with a mix of productivity features, high C/Python integration, a usable subset people stick to, and macros for just when needed. Much better. I think a Noogler could probably handle a slighty-more-advanced language than Go. That team wanted otherwise…

                            1. 2

                              I have a hard time with a number of these statements:

                              “Rust dropped GC by following that research”? So did C++ also follow research to “drop GC”? What about “C”? I’ve been plenty of type system conversation related to Rust but nothing that I would attribute directly to “dropping GC”. That seems like a bit of a simplification.

                              Is there documentation that Go developers ignored type research? Has the Go team stated that? Or that they never cared? I’ve seen Rob Pike talk about wanting to appeal to C and C++ programmers but nothing about ignorning type research. I’d be interested in hearing about that being done and what they thought the benefits were.

                              It sounds like you are saying that the benefit is something familiar and approachable. Is that a benefit to the users of a language or to the language itself? Actually I guess that is more like, is the benefit that it made Go approachable and familiar to a broad swath of programmers and that allowed it to gain broad adoption?

                              If yes, is there anything other than anecdotes (which I would tend to believe) to support that assertion?

                              1. 10

                                “That seems like a bit of a simplification.”

                                It was. Topic is enormously complex. Gets worse when you consider I barely knew C++ before I lost my memory. I did learn about memory pools and reference counting from game developers who used C++. I know it keeps getting updated in ways that improve its safety. The folks that understand C++ and Rust keep arguing about how safe C++ is with hardly any argument over Rust since its safety model is baked thoroughly into the language rather than an option in a sea of options. You could say I’m talking about Rust’s ability to be as safe as a GC in most of an apps code without runtime checks on memory accesses.

                                “Is there documentation that Go developers ignored type research? Has the Go team stated that? Or that they never cared?”

                                Like with the Rich Hickey replies, this burden of proof is backwards asking us to prove a negative. If assessing what people knew or did, we should assume nothing until we see evidence in their actions and/or informed opinions that they did these things. Only then do we believe they did. I start by comparing what I’ve read of Go to Common LISP, ML’s, Haskell, Ada/SPARK, Racket/Ometa/Rascal on metaprogramming side, Rust, Julia, Nim, and so on. Go has almost nothing in it compared to these. Looks like a mix of C, Wirth’s stuff, CSP like old stuff in 1970’s-1980’s, and maybe some other things. Not much past the 1980’s. I wasn’t the first to notice either. Article gets point across despite its problems the author apologized for.

                                Now, that’s the hypothesis from observation of Go’s features vs other languages. Lets test it on intent first. What was the goal? Rob Pike tells us here with Moray Taylor having a nicer interpretation. The quote:

                                The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.

                                It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical.

                                So, they’re intentionally dumbing the language down as much as they can while making it practically useful. They’re doing this so smart people from many backgrounds can pick it up easily and go right to being productive for their new employer. It’s also gotta be C-like for the same reason.

                                Now, let’s look at its prior inspirations. In the FAQ, they tell you the ancestors: “Go is mostly in the C family (basic syntax), with significant input from the Pascal/Modula/Oberon family (declarations, packages), plus some ideas from languages inspired by Tony Hoare’s CSP, such as Newsqueak and Limbo (concurrency).” They then make an unsubstantiated claim, in that section at least, that it’s a new language across the board to make programming better and more fun. In reality, it seems really close to a C-like version of the Oberon-2 experience one developer (can’t recall) wanted to recreate with concurrency and tooling for aiding large projects. I covered the concurrency angle in other comment. You don’t see a lot of advanced or far out stuff here: decades old tech that’s behind current capabilities. LISP’ers, metaprogrammers and REBOL’s might say behind old tech, too. ;)

                                Now, let’s look at execution of these C, Wirth-like, and specific concurrency ideas into practice. I actually can’t find this part. I did stumble upon its in-depth history of design decisions. The thing I’m missing, if it was correct, is a reference to the claim that the three developers had to agree on each feature. If that’s true, it automatically would hold the language back from advanced stuff.

                                In summary, we have a language designed by people who mostly didn’t use cutting-edge work in type systems, employed nothing of the sort, looked like languages from the 1970’s-1980’s, considered them ancestors, is admittedly dumbed-down as much as possible so anyone from any background can use it, and maybe involved consensus from people who didn’t use cutting-edge stuff (or even much cutting-edge at 90’s onward). They actually appear to be detractors to a lot of that stuff if we consider the languages they pushed as reflecting their views on what people should use. Meanwhile, the languages I mentioned above used stuff from 1990’s-2000’s giving them capabilities Go doesn’t have. I think the evidence weighs strongly in favor of that being because designers didn’t look at it, were opposed to it for technical and/or industrial reasons, couldn’t reach a consensus, or some combo.

                                That’s what I think of Go’s history for now. People more knowledgeable feel free to throw any resources I might be missing. It just looks to be a highly-practical, learn/use-quickly, C/Oberon-like language made to improve onboarding and productivity of random developers coming into big companies like Google. Rob Pike even says that was the goal. Seems open and shut to me. I thank the developers of languages like Julia and Nim believing we were smart enough to learn a more modern language, even if we have to subset them for inexperienced people.

                            2. 4

                              It’s easy for non-LtU programmers to pick up, which happens to be the vast majority.

                              1. 3

                                Sorry, that isn’t detailed. Is there evidence that its easy for these programmers to pick up? What does “easy to pick up” mean? To get something to compile? To create error-free programs? “Clearly benefited” is a really loaded term that can mean pretty much anything to anyone. I’m looking for what the stated benefits are for Go. Is the benefit to go that it is “approachable” and “familiar”?

                                There seems to be an idea in your statement then that using any sort of type theory research will inherintly make something hard to pick up. I have a hard time accepting that. I would, without evidence, be willing to accept that many type system ideas (like a number of them in Pony) are hard to pick up, but the idea that you have to ignore type theory research to be easy to pick up is hard for me to accept.

                                Could I create a language that ignores type system theory but using a non-familiar syntax and not be easy to pick up?

                                1. 5

                                  I already gave you the quote from Pike saying it was specifically designed for this. Far as the how, I think one of its designers explains it well in those slides. The Guiding Principles section puts simplicity above everything else. Next, a slide says Pascal was a minimalist language designed for teaching non-programmers to code. Oberon was similarly simple. Oberon-2 added methods on records (think simpler OOP). The designer shows Oberon-2 and Go code saying it’s C’s syntax with Oberon-2’s structure. I’ll add benefits like automatic, memory management.

                                  Then, the design link said they chose CSP because (a) they understood it enough to implement and (b) it was the easiest thing to implement throughout the language. Like Go itself, it was the simplest option rather than the best along many attributes. There were lots of people who picked up SCOOP (super-easy but with overhead) with probably even more picking up Rust’s method grounded in affine types. Pony is itself doing clever stuff using advances in language. Go language would ignore those since (a) Go designers didn’t know them well from way back when and (b) would’ve been more work than their intent/budget could take.

                                  They’re at least consistent about simplicity for easy implementation and learning. I’ll give them that.

                              2. 3

                                It seems to me that Go was clearly designed to have a well-known, well-understood set of primitives, and that design angle translated into not incorporating anything fundamentally new or adventurous (unlike Pony and it’s impressive use of object capabilities). It looked already old at birth, but it feels impressively smooth, in the beginning at least.

                                1. 3

                                  I find it hard to believe that CSP and Goroutines were “well-understood set of primitives”. Given the lack of usage of CSP as a mainstream concurrency mechanism, I think that saying that Go incorporates nothing fundamentally new or adventurous is selling it short.

                                  1. 5

                                    CSP is one of oldest ways people modeled concurrency. I think it was built on Hoare’s monitor concept from years before which Per Brinch Hansen turned into Concurrent Pascal. Built Solo OS with mix of it and regular Pascal. It was also typical in high-assurance to use something like Z or VDM for specifying main system with concurrency done in CSP and/or some temporal logic. Then, SPIN became dominant way to analyze CSP-like stuff automatically with a lot of industrial use for a formal method. Lots of other tools and formalisms existed, though, under banner of process algebras.

                                    Outside of verification, the introductory text that taught me about high-performance, parallel computing mentioned CSP as one of basic models of parallel programming. I was experimenting with it in maybe 2000-2001 based on what those HPC/supercomputing texts taught me. It also tied into Agent-Oriented Programming I was looking into then given they were also concurrent, sequential processes distributed across machines and networks. A quick DuckDuckGo shows a summary article on Wikipedia mentions it, too.

                                    There were so many courses teaching and folks using it that experts in language design and/or concurrency should’ve noticed it a long time ago trying to improve on it for their languages. Many did, some doing better. Eiffel SCOOP, ML variants like Concurrent ML, Chapel, Clay with Wittie’s extensions, Rust, and Pony are examples. Then you have Go doing something CSP-like (circa 1970’s) in the 2000’s still getting race conditions and stuff. What did they learn? (shrugs) I don’t know…

                                    1. 12

                                      Nick,

                                      I’m going to take the 3 different threads of conversation we have going and try to pull them all together in this one reply. I want to thank you for the time you put into each answer. So much of what appears on Reddit, HN, and elsewhere is throw away short things that often feel lazy or like communication wasn’t really the goal. For a long time, I have appreciated your contributions to lobste.rs because there is a thoughtfulness to them and an attempt to convey information and thinking that is often absent in this medium. Your replies earlier today are no exception.


                                      Language is funny.

                                      You have a very different interpretation of the words “well-understood primitives” than I do. Perhaps it has something to do with anchoring when I was writing my response. I would rephrase my statement this way (and I would still be imprecise):

                                      While CSP has been around for a long time, I don’t that prior to Go, that is was a well known or familiar concurrency model for most programmers. From that, I would say it isn’t “well-understood”. But I’m reading quite a bit, based on context into what “well-understood” means here. I’m taking it to me, “widely understood by a large body of programmers”.

                                      And I think that your response Nick, I think it actually makes me believe that more. The languages you mention aren’t ones that I would consider familiar or mainstream to most programmers.

                                      Language is fun like that. I could be anchoring myself again. I rarely ask questions on lobste.rs or comment. I decided to on this occasion because I was really curious about a number of things from an earlier statement:

                                      “Go has clearly benefited from “ignoring type theory research”.

                                      Some things that came to mind when I read that and I wondered “what does this mean?”

                                      “clearly benefited”

                                      Hmmm, what does benefit mean? Especially in reference to a language. My reading of benefit is that “doing X helped the language designers achieve one or more goals in a way that had acceptable tradeoffs”. However, it was far from clear to me, that is what people meant.

                                      “ignoring type theory research”

                                      ignoring is an interesting term. This could mean many things and I think it has profound implications for the statement. Does ignoring mean ignorance? Does it mean willfully not caring? Or does it mean considered but decided not to use?

                                      I’m familiar with some of the Rob Pike and Go early history comments that you referenced in the other threads. In particular related to the goal of Go being designed for:

                                      The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.

                                      It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical.

                                      I haven’t found anything though that shows there was a willful disregard of type theory. I wasn’t attempting to get you to prove a negative, more I’m curious. Has the Go team ever said something that would fall under the heading of “type system theory, bah we don’t need it”. Perhaps they have. And if they have, is there anything that shows a benefit from that.

                                      There’s so much that is loaded into those questions though. So, I’m going to make some statements that are possibly open to being misconstrued about what from your responses, I’m hearing.

                                      “Benefit” here means “helped make popular” because Go on its surface, presents a number of familiar concepts for the programmer to work with. There’s no individual primitive that feels novel or new to most programmers except perhaps the concurrency model. However, upon the first approach that concurrency model is fairly straightforward in what it asks the programmer to grasp when first encountering it. Given Go’s stated goals from the quote above. It allows the programmers to feel productive and “build good software”.

                                      Even as I’m writing that though, I start to take issue with a number of the assumptions that are built into the Pike quote. But that is fine. I think most of it comes down to for me what “good software” is and what “simple” is. And those are further loaded words that can radically change the meaning of a comment based on the reader.

                                      So let me try again:

                                      When people say “Go has clearly benefited from “ignoring type theory research” what they are saying is:

                                      Go’s level of popularity is based, in part, on it providing a set of ideas that should be mostly familiar to programmers who have some experience with the Algol family of languages such as C, C++, Python, Ruby etc. We can further refine that to say that from the Algol family of languages that we are really talking about ones that have type systems that make few if any guarantees (like C). That Go put this familiarity as its primary goal and because of that, is popular.

                                      Would you say that is a reasonable summation?


                                      When I asked:

                                      “Is there documentation that Go developers ignored type research? Has the Go team stated that? Or that they never cared?”

                                      I wasn’t asking you for to prove a negative. I was very curious if any such statements existed. I’ve never seen any. I’ve drawn a number of conclusions about Go based mostly on the Rob Pike quote you provided earlier. I was really looking for “has everyone else as well” or do they know things that I don’t know.

                                      It sounds like we are both mostly operating on the same set of information. That’s fine. We can draw conclusions from that. But I feel at least good in now saying that both you and I are inferring things based on what appears to be mostly shared set of knowledge here and not that I am ignorant of statements made by Go team members.

                                      I wasn’t looking for proof. I was looking for information that might help clear up my ignorance in the area. Related to my ignorance.

                                      1. 3

                                        I appreciate that you saw I was trying to put effort into it being productive and civil. Those posts took a while. I appreciate your introspective and kind reply, too. Now, let’s see where we’re at with this.

                                        Yeah, it looks like we were using words with a different meaning. I was focused on well-understood by PLT types that design languages and folks studying parallelism. Rob Pike at the least should be in both categories following that research. Most programmers don’t know about it. You’re right that Go could’ve been first time it went mainstream.

                                        You also made a good point that it’s probably overstating it to say they never considered. I have good evidence they avoided almost all of it. Other designers didn’t. Yet, they may have considered it (how much we don’t know), assessed it against their objectives, and decided against all of it. The simplest approach would be to just ask them in a non-confrontational way. The other possibility is to look at each’s work to see if it showed any indication they were considering or using such techniques in other work. If they were absent, saying they didn’t consider it in their next work would be reasonable. Another angle would be to look at, like with C’s developers, whether they had a personal preference for simpler or barely any typing consistently avoiding developments in type systems. Since that’s lots of work, I’ll leave it at “Unknown” for now.

                                        Regarding its popularity, I’ll start by saying I agree its simple design reusing existing concepts was a huge element of that. It was Wirth’s philosophy to do same thing for educating programmers. Go adopted that philosophy to modern situation. Smart move. I think you shouldn’t underestimate the fact that Google backed it, though.

                                        There were a lot of interesting languages over the decades with all kinds of good tradeoffs. The ones with major, corporate backing and/or on top of advantageous foundations/ecosystems (eg hardware or OS’s) usually became big in a lasting way. That included COBOL on mainframes, C on cheap hardware spreading with UNIX, Java getting there almost entirely through marketing given its technical failures, .NET/C# forced by Microsoft on its huge ecosystem, Apple pushing Swift, and some smaller ones. Notice the language design is all across the board here in complexity, often more complex than existing languages. The ecosystem drivers, esp marketing or dominant companies, are the consistent thread driving at least these languages’ mass adoption.

                                        Now, mighty Google claims they’re backing for their massive ecosystem a new language. It’s also designed by celebrity researchers/programmers, including one many in C community respect. It might also be a factor in whether developers get a six digit job. These are two, major pulls plus a minor one that each in isolation can draw in developers. Two, esp employment, will automatically make a large number of users if they think Google is serious. Both also have ripple effects where other companies will copy what big company is doing to not get left behind. Makes the pull larger.

                                        So, as I think of your question, I have that in the back of my mind. I mean, those effects pull so hard that Google’s language could be a total piece of garbage and still have 50,000-100,000 developers just going for a gold rush. I think that they simplified the design to make it super-easy to learn and maintain existing code just turbocharges that effect. Yes, I think the design and its designers could lead to significant community without Google. I’m just leaning toward it being a major employer with celebrity designers and fanfare causing most of it.

                                        And then those other languages start getting uptake despite advanced features or learning troubles (esp Rust). Shows they Go team could’ve done better on typing using such techniques if they wanted to and/or knew about those techniques. I said that’s unknown. Go might be the best they could do in their background, constraints, goals, or whatever. Good that at least four, different groups made languages to push programming further into the 90’s and 2000’s instead of just 70’s to early 80’s. There’s at least three creating languages closer to C generating a lot of excitement. C++ is also getting updates making it more like Ada. Non-mainstream languages like Ada/SPARK and Pony are still getting uptake even though smaller.

                                        If anything, the choices of systems-type languages is exploding right now with something for everyone. The decisions of Go’s language authors aren’t even worth worrying about since that time can be put into more appropriate tools. I’m still going to point out that Rob Pike quote to people to show they had very, very specific goals which made a language design that may or may not be ideal for a given task. It’s good for perspective. I don’t know designers’ studies, their tradeoffs, and (given alternatives) they barely matter past personal curiosity and PLT history. That also means I’ll remain too willfully ignorant about it to clear up anyone’s ignorance. At least till I see some submissions with them talking about it. :)

                                        1. 2

                                          Thanks for the time you put into this @nickpsecurity.

                                          1. 1

                                            Sure thing. I appreciate you patiently putting time into helping me be more accurate and fair describing Go designers’ work.

                                            1. 2

                                              And thank you, I have a different perspective on Go now than I did before. Or rather, I have a better understanding of other perspectives.

                            3. 4

                              I don’t have a good understanding of type systems. What is it that Rich misses about Haskells Maybe? Does changing the return type of a function from Maybe T to T not mean that you have to change code which uses the return value of that function?

                              1. 26

                                Does changing the return type of a function from Maybe T to T not mean that you have to change code which uses the return value of that function?

                                It does in a way, but I think people sometimes over-estimate the amount of changes that are required. It depends on whether or not really really care about the returned value. Let’s look at a couple of examples:

                                First, let’s look at an example. Let’s say that we had a function that was going to get the first element out of a list, so we start out with something like:

                                getFirstElem :: [a] -> a
                                getFirstElem = head
                                

                                Now, we’ll write a couple of functions that make use of this function. Afterwards, I’ll change my getFirstElem function to return a Maybe a so you can see when, why, and how these specific functions need to change.

                                First, let’s imagine that I have some list of lists, and I’d like to just return a single list that has the first element; for example I might have something like ["foo","bar","baz"] and I want to get back "fbb". I can do this by calling map over my list of lists with my getFirstElem function:

                                getFirsts :: [[a]] -> [a]
                                getFirsts = map getFirstElem
                                

                                Next, say we wanted to get an idea of how many elements we were removing from our list of lists. For example, in our case of ["foo","bar","baz"] -> "fbb", we’re going from a total of 9 elements down to 3, so we’ve eliminated 6 elements. We can write a function to help us figure out how many elements we’ve dropped pretty easily by looking at the sum of the lengths of the lists in the input lists, and the overall length of the output list.

                                countDropped :: [[a]] -> [b] -> Int
                                countDropped a b =
                                  let a' = sum $ map length a
                                      b' = length b
                                  in a' - b'
                                

                                Finally, we probably want to print out our string, so we’ll use print:

                                printFirsts =
                                  let l = ["foo","bar","baz"]
                                      r = getFirsts l
                                      d = countDropped l r
                                  in print l >> print r >> print d
                                

                                Later, if we decide that we want to change our program to look at ["foo","","bar","","baz"]. We’ll see our program crashes! Oh no! the problem is that head doesn’t work with an empty list, so we better go and update it. We’ll have it return a Maybe a so that we can capture the case where we actually got an empty list.

                                getFirstElem :: [a] -> Maybe a
                                getFirstElem = listToMaybe
                                

                                Now we’ve changed our program so that the type system will explicitly tell us whether we tried to take the head of an empty list or not- and it won’t crash if we pass one in. So what refactoring do we have to do to our program?

                                Let’s walk back through our functions one-by-one. Our getFirsts function had the type [[a]] -> [a] and we’ll need to change that to [[a]] -> [Maybe a] now. What about the code?

                                If we look at the type of map we’ll see that it has the type: map :: (c -> d) -> [c] -> [d]. Since both [[a]] -> [a] and [[a]] -> [Maybe a] satisfy the constraint [a] -> [b], (in both cases, c ~ [a], in the first case, d ~ a and in the second d ~ Maybe a). In short, we had to fix our type signature, but nothing in our code has to change at all.

                                What about countDropped? Even though our types changed, we don’t have to change anything in countDropped at all! Why? Because countDropped is never looking at any values inside of the list- it only cares about the structure of the lists (in this case, how many elements they have).

                                Finally, we’ll need to update printFirsts. The type signature here doesn’t need to change, but we might want to change the way that we’re printing out our values. Technically we can print a Maybe value, but we’d end up with something like: [Maybe 'f',Nothing,Maybe 'b',Nothing,Maybe 'b'], which isn’t particularly readable. Let’s update it to replace Nothing values with spaces:

                                printFirsts :: IO ()
                                printFirsts =
                                  let l = ["foo","","bar","","baz"]
                                      r = map (fromMaybe ' ') $ getFirsts' l
                                      d = countDropped l r
                                  in print l >> print r >> print d
                                

                                In short, from this example, you can see that we can refactor our code to change the type, and in most cases the only code that needs to change is code that cares about the value that we’ve changed. In an untyped language you’d expect to still have to change the code that cares about the values you’re passing around, so the only additional changes that we’ve had to do here was a very small update to the type signature (but not the implementation) of one function. In fact, if I’d let the type be inferred (or written a much more general function) I wouldn’t have had to even do that.

                                There’s an impression that the types in Haskell require you to do a lot of extra work when refactoring, but in practice the changes you are making aren’t materially more or different than the ones you’d make in an untyped language- it’s just that the compiler will tell you about the changes you need to make, so you don’t need to find them through unit tests or program crashes.

                                1. 3

                                  countDropped should be changed. To what will depend on your specification but as a simple inspection, countDropped ["", "", "", ""] [None, None, None, None] will return -4, which isn’t likely to be what you want.

                                  1. 4

                                    That’s correct in a manner of speaking, since we’re essentially computing the difference between the number of characters in all of the substrings minutes the length of the printed items. Since [""] = [[]], but is printed " ", we print one extra character (the space) compared to the total length of the string, so a negative “dropped” value is sensible.

                                    Of course the entire thing was a completely contrived example I came up with while I was sitting at work trying to get through my morning coffee, and really only served to show “sometimes we don’t need to change the types at all”, so I’m not terribly worried about the semantics of the specification. You’re welcome to propose any other more sensible alternative you’d like.

                                    1. -3

                                      That’s correct in a manner of speaking, since …

                                      This is an impressive contortion, on par with corporate legalese, but your post-hoc justification is undermined by the fact that you didn’t know this was the behavior of your function until I pointed it out.

                                      Of course the entire thing was a completely contrived example …

                                      On this, we can agree. You created a function whose definition would still typecheck after the change, without addressing the changed behavior, nor refuting that in the general case, Maybe T is not a supertype of T.

                                      You’re welcome to propose any other more sensible alternative you’d like.

                                      Alternative to what, Maybe? The hour long talk linked here is pretty good. Nullable types are more advantageous, too, like C#’s int?. The point is that if you have a function and call it as f(0) when the function requires its first argument, but later, the requirement is “relaxed”, all the places where you wrote f(0) will still work and behave in exactly the same way.

                                      Getting back to the original question, which was (1) “what is it that Rich Hickey doesn’t understand about types?” and, (2) “does changing the return type from Maybe T to T cause calling code to break?”. The answer to (2) is yes. The answer to (1), given (2), is nothing.

                                      1. 10

                                        I was actually perfectly aware of the behavior, and I didn’t care because it was just a small toy example. I was just trying to show some examples of when and how you need to change code and/or type signatures, not write some amazing production quality code to drop some things from a list. No idea why you’re trying to be such an ass about it.

                                        1. 3

                                          She did not address question (1) at all. You are reading her response to question (2) as implying something about (1) that makes your response needlessly adverse.

                                    2. 2

                                      This is a great example. To further reinforce your point, I feel like the one place Haskell really shows it’s strength in these refactors. It’s often a pain to figure out what the correct types should be parts of your programs, but when you know this and make a change, the Haskell compiler becomes this real guiding light when working through a re-factor.

                                    3. 10

                                      He explicitly makes the point that “strengthening a promise”, that is from “I might give you a T” to “I’ll definitely give you a T” shouldn’t necessarily be a breaking change, but is in the absence of union types.

                                      1. 2

                                        Half baked thought here that I’m just airing to ask for an opinion on:

                                        Say as an alternative, the producer produces Either (forall a. a) T instead of Maybe T, and the consumer consumes Either x T. Then the producer’s author changes it to make a stronger promise by changing it to produce Either Void T instead.

                                        I think this does what I would want? This change hasn’t broken the consumer because x would match either alternative. The producer has strengthened the promise it makes because now it promises not to produce a Left constructor.

                                        1. 5

                                          When the problem is “I can’t change my mind after I had insufficient forethought”, requiring additional forethought is not a solution.

                                          1. 2

                                            So we’d need a way to automatically rewrite Maybe t to Either (forall a. a) t everywhere - after the fact. ;)

                                    4. 2

                                      Likewise, I wonder what he thinks about Rust’s type system to ensure temporal safety without a GC. Is safe, no-GC operation in general or for performance-critical modules desirable for Clojure practitioners? Would they like a compile to native option that integrates that safe, optimized code with the rest of their app? And if not affine types, what’s his solution that doesn’t involve runtime checks that degrade performance?

                                      1. 7

                                        I’d argue that GC is a perfectly fine solution in vast majority of cases. The overhead from advanced GC systems like the one on the JVM is becoming incredibly small. So, the scenarios where you can’t afford GC are niche in my opinion. If you are in such a situation, then types do seem like a reasonable way to approach the problem.

                                        1. 3

                                          I have worked professionally in Clojure but I have never had to make a performance critical application with it. The high performance code I have written has been in C and CUDA. I have been learning Rust in my spare time.

                                          I argue that both Clojure and Rust both have thread safe memory abstractions, but Clojure’s solution has more (theoretical) overhead. This is because while Rust uses ownership and affine types, Clojure uses immutable data structures.

                                          In particular, get/insert/remove for a Rust HashMap is O(1) amortized while Clojure’s corresponding hash-map’s complexity is O(log_32(n)) for those operations.

                                          I haven’t made careful benchmarks to see how this scaling difference plays out in the real world, however.

                                          1. 4

                                            Having used clojure’s various “thread safe memory abstractions” I would say that the overhead is actual not theoretical.

                                      2. 2

                                        Disclaimer: I <3 types a lot, Purescript is lovely and whatnot

                                        I dunno, I kinda disagree about this. Even in the research languages, people are opting for nominal ADTs. Typescript is the exception, not the rule.

                                        His wants in this space almost require “everything is a dictionary/hashmap”, and I don’t think the research in type theory is tackling his complaints (the whole “place-oriented programming” stuff and positional argument difficulties ring extremely true). M…aybe row types, but row types are not easy to use compared to the transparent and simple Typescript model in my opinion.

                                        Row types help o solve issues generated in the ADT universe, but you still have the nominal typing problem which is his other thing.

                                        His last keynote was very agressive and I think people wrote it off because it felt almost ignorant, but I think this keynote is extremely on point once he gets beyond the maybe railing in the intro

                                      3. 22

                                        I’d take Rich Hickey’s opinions on type systems with a grain of salt, a hefty lemon wedge, and about a pint of vodka.

                                        a to a… List of a to list of a… It means nothing! It tells you nothing!

                                        — Rich Hickey, Effective Programs.

                                        I understand that he’s everyone’s hero, but to be a programmer of such stature and to fail to grok parametricity that badly is frankly embarrassing. If anyone else were to spout such nonsense, they’d be immediately dismissed as a charlatan.

                                        Luckily, I do know some hardcore Clojurists who were also confused and disappointed by that part of his talk.

                                        There is definitely a cult following around Hickey, as has been pointed out to me by some Clojurists. Should we worship him? Maybe Not.

                                        1. 13

                                          Not a Hickey disciple, but… I don’t think he’s talking about parametricity at all—the specific form of the type signature is irrelevant to his point. He’s saying that any type signature doesn’t tell you what the function does; only the name tells you that. Saying that the signature tells you nothing is hyperbole, but he’s emphasizing that to humans, information in the name >> information in the type signature.

                                          1. 7

                                            He’s saying that any type signature doesn’t tell you what the function does

                                            I’m kind of a static-typing weenie, but to be fair to Hickey, this is more true in Clojure than it is in Haskell, if you imagine a hypothetical version of Clojure that is statically typed but which still doesn’t track effects using the type system. (This is, admittedly, kind of a strawman.)

                                            Take the type signature [a] -> [a]. In Haskell, there aren’t many possibilities for what this function could be; it could be id or reverse, and it could be something that selects/reorders elements of the list based purely on their indices, but it couldn’t be a random shuffle function, because that would have signature [a] -> IO [a] or similar. But in a language where you can have IO anywhere, [a] -> [a] could just as well be a shuffle function. So what I’m trying to say is, Haskell gives you more static guarantees than Clojure along multiple axes, not just in types ;-)

                                            That being said, even a powerful type system does not excuse you from using descriptive names for your identifiers!

                                            1. 12

                                              Sure, I’ll buy that, to a degree. Names are important.

                                              However, he uses a function name reverse as his example. Funnily enough, the function you use to reverse a list in Haskell is called… reverse.

                                              Furthermore, he goes on to say that modelling the real world with types doesn’t work in practice, and that you need tests instead. Since when are these two tools mutually exclusive? Does he not know that we also write tests in Haskell?

                                              he’s emphasizing that to humans, information in the name >> information in the type signature.

                                              I’d say this is certainly true of people who haven’t invested any time in learning what types mean and what constraints they enforce. To the people who have though, it provides a wealth of information, e.g., that the first type signature example that slipped out of Hickey’s mouth — a -> a — is an incredibly specific signature that can only have one possible implementation.

                                              1. 11

                                                I agree with you, but just want to be sure we’re arguing about what he’s actually saying. The a -> a is pretty clearly just him misspeaking when reading [a] -> [a] off the slide. You’re confirming his point – you know that reverse reverses the list because of the name, not the type signature. The type signature of a function called reverse couldn’t be anything else.

                                                He doesn’t see sufficient value in the additional information from the type signature to compensate for what he sees as the overhead of keeping types fully conformant. So much so that he hyperbolically claims the additional information is “nothing”.

                                                1. 7

                                                  But the type [a] -> [a] tells us something very important about the function. It encodes that this function doesn’t care about the type of the elements. That no ordering properties matter about these elements. Now we don’t know if the list will be truncated, extended, reversed, or anything like that. But, we know if it were extended and there are no other arguments of type a floating around those extra elements must come from the input list.

                                                  We can further refine the time to encode the length of the list, something like List a n -> List a n. In this case, we know even more about the function. This process can be repeated a few times to eventually include nearly all properties we want the function to obey. Do I think that’s a substitute for tests, the name of the function, or even documentation for the function? Of course not. But I do see how keeping a type checker happy is any more onerous than passing unit tests, doc tests, style guidelines, etc.

                                                  1. 3

                                                    Sure, and I think he chose a bad example to make his point (more hyperbole), because [a] -> [a] really doesn’t have that many choices for useful things it could do. (I just checked Hoogle and there’s cycle, init, tail, and reverse. I guess there could also be shuffle.)

                                                    If the example was something like GeneralLedger -> String -> CurrencyAmount -> [Transaction] his point would be better served.

                                                    But a few minutes later he presents his example of adding a new value to a union type and not wanting to waste time updating all the case statements for that type, because only a few functions will ever see the new value in real operation. I think that brings out a difference in implementation philosophy that is underlying the whole argument. In a strongly-typed system you want to prove to the compiler exhaustively that no error case exists. In a weakly-typed system you only have to prove that to yourself, and if you “know” a code path will never receive a certain combination of data, you don’t have to do any work to support it. The strong-typing advocate would say the only reason you’re saving work is that you designed the types wrong in the first place and need to fix them. The weak-typing advocate would ask why they even have to do that much work when the code is doing fine, as verified by their tests.

                                                    1. 3

                                                      But a few minutes later he presents his example of adding a new value to a union type and not wanting to waste time updating all the case statements for that type, because only a few functions will ever see the new value in real operation.

                                                      When he made this point, it reminded me of this great point that Kris Jenkins made in his recent talk:

                                                      When you have a type error, and you always get a type error, the question is whether you get it from QA or your users or your compiler.

                                                      I think rebeccaskinner also made a good point earlier in this thread that the cost of making these changes is massively overstated, and I think this is people’s instinct after working with technology that doesn’t make change easy. If you want to make changes across a system in a dynamic language, you need to rely on human discipline to have written all the tests. Writing these tests manually is never going to be as quick as a compiler writing them for you. With a good compiler [and I know you know this — I’m just thinking out loud in a friendly way :) ], these kinds of changes go from being nearly impossible to just tedious.

                                                      1. 3

                                                        For sure — the counterargument in the talk is that the strongly-typed compiler makes me do work (specifically, adding a case for the new union type alternative to all the places that are never going to receive that alternative) that I would never have done or tested, because I “know” it will never happen. (Note how I keep putting “know” in quotes. :) )

                                                        It seems like the real philosophical difference is more about whether you want to be forced to write a program that provably covers every situation, or you want the freedom to fail to cover some situations you “know” aren’t relevant to actual usage. Kind of similar to error returns vs. exceptions. Or heap vs. static string buffers. And in all those cases the intuition about the pro/con tradeoff is different.

                                                        1. 1

                                                          I’d argue types only force you to cover the cases you are choosing to enforce. While actively discouraged, nothing is stopping you from converting all your values to something like Strings and manipulate them that way. Even in Haskell, nothing is stopping you from dropping in a catch-all _ -> error "FIXME" Anecdotally, any time I’ve been too lazy to handle a case, it has always bitten me down the line.

                                                          1. 2

                                                            While this is true, imagine how much more failure you would encounter when applying the same human laziness to a technology which enforces far fewer constraints.

                                                      2. 1

                                                        I guess there could also be shuffle.

                                                        Think about this for a moment. How would this work? Given referential transparency, what would happen on subsequent calls to this function?

                                                        For it to be shuffle, it would need a random seed. You could either pass that in, which would make it something like f :: Seed -> [a] -> [a], or you would have to do it in IO, which would make it f :: [a] -> IO [a].

                                                        1. 1

                                                          Yeah, something was bugging me about that. which is why I weaseled with “I guess”. :) On the other hand, I think Hickey is arguing against even less-pure systems than Haskell. [a] -> [a] with invisible effects allowed could be a lot of things!

                                                  2. 13

                                                    Since when are these two tools mutually exclusive? Does he not know that we also write tests in Haskell?

                                                    People getting into types vs tests arguments should always keep in mind that the Haskell community invented property based testing!

                                                    1. 2

                                                      They made it popular. It was called specification- or model-based test generation before that label. It was sporadic, though. People doing contracts call it contract-based. This problem is why you see me using 2-4 terms when I say it here.

                                                      1. 1

                                                        To my understanding model-based testing is different property-based testing in roughly the same way end-to-end testing is different from unit testing.

                                                        I’m also unfamiliar with contract testing used in that way. Do you have any good links?

                                                        1. 2

                                                          The models are often for a subset of the functionality or attributes. Gets them closer to unit testing. Comparisons depend on model coverage. Another thing is Model-Driven Development hit buzzword status covering everything from UML tools to Alloy extensions. That expanded what people called model-driven/based… anything.

                                                          Far as contracts, pretty similar to how people are doing property-based testing. Just much broader since contracts got picked up by many crowds. Just typing any of this into DuckDuckGo and Google will get you examples. Try this for example. Another thing I do is add current year in quotes, do a few pages, subtract one, repeat,…, repeat. Works for many types of CompSci once you know subfield’s buzzwords.

                                                    2. 6

                                                      [a] -> [a] could be identity, could be reverse, could be tail, could be “every other element”, could be “list where it’s the first element repeated over and over again”

                                                      It does reveal a bit in that you know that stuff happening in there depend only on list operations and not the element, but his argument (which is pretty true in “enterprise-y software”) is that in practice you have such a large possible set of functions inhibiting certain types that it doesn’t bring that much info to the table.

                                                      For example, if I give you a signature BlogPost -> String -> BlogPost, you might be able to say “well this probably isn’t doing any IO”, but it could be about setting a title, it could be about swapping the content, if the object contains a list of comments it could be appending to that…. Lots of “real world”[0] domain objects are huge and have a lot of data built in. This also explains his complaints about place-oriented programming (your object has 100 fields, do ADTs provide value at that level? You end up wanting for nicer tools to work on that)

                                                      I think he has a thesis he is convinced about, but the fact he’s also spending so much time on spec shows that he gets that there’s…. something there. The bashing gets a bit tired though

                                                      [0]: “Real world” like “sofware built to run some internal business operations”. He used to work on something about scheduling radio broadcasts? Where messy constraints came in all the tim

                                                      1. 4

                                                        [a] -> [a] could be identity, could be reverse, could be tail, could be “every other element”, could be “list where it’s the first element repeated over and over again”

                                                        Technically, [a] -> [a] could have infinite implementations. I believe also, that technically a function with this signature could have the side effect of crashing your program through infinite recursion.

                                                        That said, I don’t think Rich Hickey compares the two approaches on fair terms. It isn’t fair to say “type systems don’t help because if you try hard enough you can break them. What you need to do instead is [lots of hand-waving here] make things simple!”

                                                        There are definitely a few logical fallacies being made, and I’m struggling to keep up with them all.

                                                        The example I outlined above: is that a Straw Man, or is it Special Pleading, or perhaps something else?

                                                        When Rich Hickey dismisses the value of a powerful type system by saying “oh it doesn’t really work in practice”, is this the Anecdotal logical fallacy? Because anecdotally, this stuff works for me and for many people I’ve worked with, in practice. It could also perhaps be Ambiguity, or No True Scotsman.

                                                        And the people defending his take on this? Appeal to Authority.

                                                        I say this being totally aware that I may be committing the Fallacy Fallacy, but I’m yet to be convinced that an accumulation of design aids does not yield a net benefit.

                                                  3. 6

                                                    Well, it’s true. The type signature tells you nothing. This reminds me… when I checked out Dylan some time ago I was actually rather shocked that in the type signature for a function definition there’s a spot to name the return value.

                                                    define method sum-squares (in :: <list>) => (sum-of-element-squares :: <integer>)
                                                    

                                                    That name has no programmatic use. It can’t be referred to in the body of the function. It has no special semantics. It is simply documentation. But the fact that it’s there speaks volumes about what was important to the designers (people like David Moon, Lisp veteran and principle designer of the Common Lisp object system).

                                                    Mocking “list a to list a” isn’t out of ignorance and has context. It’s a reference to a very famous, 30 year old paper by Philip Wadler called Theorems for Free. This one paper inaugurated an entire sect of theoreticians proving properties of programs solely based on their type signatures, and led to people making the claim that, yes, there is actually a fair amount that a type signature can tell you, without regard even to what the arguments represent or the name of the function. If there’s a cult, here, it’s this particular cult of type worship. It’s hair shirt junk theory, useless for the working programmer, useless for people interested in making expressive programming languages.

                                                    1. 4

                                                      That name has no programmatic use. It can’t be referred to in the body of the function. It has no special semantics. It is simply documentation.

                                                      I find this surprising and interesting! There are some languages that let you name the output parameter for use in verification, such as to say “the returned z is going to be between inputs x and y.” This is the first I’m hearing of using it purely for documentation.

                                                  4. 6

                                                    My favorite quote from the video so far:

                                                    like all design things […] what was wrong, two things were combined that shouldn’t have been combined

                                                    Yes. Yes, indeed!

                                                    1. 3

                                                      What I find interesting is that the issue of Null/Maybe/Either doesn’t come up when you do imperative design. Imagine a head function that takes a closure that is called when a list has a head and is not called otherwise:

                                                      head([1,2,3], (x) {

                                                      // x will be 1 here, create whatever side-effect you need in this case

                                                      })

                                                      If you need the error case, supply an additional closure as an argument.

                                                      What is distressing to me about this is that functional-style is elegant in so many ways but not in the case of error handling.

                                                      1. 4

                                                        What is distressing to me about this is that functional-style is elegant in so many ways but not in the case of error handling.

                                                        Weird, for me this is actually one of the biggest selling point of functional programming for me.

                                                        1. 3

                                                          I’m struggling to follow this example. You’re essentially providing a default value, which could be expressed as:

                                                          head' x = fromMaybe x . listToMaybe

                                                          What in your view qualifies as elegance in the case of error handling? In what way is the FP approach, i.e. wrapping the value in an algebra to preserve composability, inelegant? The idiomatic way of doing this in FP is to explicitly model and handle the potential for failure. I’m not sure how it could be more “elegant” than this.

                                                          1. 1

                                                            I’m not providing a default value, just an action on success. If the closure is called, it can do anything or nothing with the value (head) it receives.

                                                            Head is a contrived example. The more typical way that this works in OO is to just pass along your results on success to another object. If there’s a problem, you just don’t pass them along. There is no need to check for an error at the other end.

                                                            1. 1

                                                              Head is a contrived example.

                                                              Indeed, and quite confusing.

                                                              It’s difficult for me to argue for or against a particular approach in this case. Without a better example, I would have to ask: Why do you want side effects here anyway?

                                                          2. 1

                                                            Well, given the safe head’ function that returns Maybe a, is the following (but pardon my Haskell, it’s probably wrong) not elegant?

                                                            f :: [a] -> Either a, String
                                                            f l = case (head’ l)
                                                                   when Some a then Left a
                                                                   when Nothing then “error message”
                                                            

                                                            where the name of f probably suggests something about what the function does, especially in case of errors?

                                                            Or are you specifically concerned about the lack of flexibility in the error handling?

                                                          3. 2

                                                            I think he is basically asking for:

                                                            • b : B is implicitly casted to Maybe<B>
                                                            • a : A is implicitly casted to Either<A, B>
                                                            • b : B is implicitly casted to Either<A, B>
                                                            1. 2

                                                              I think the best ideas presented in this talk are the ones about separating optionality out of schemas. clojure.spec seems really nice and it looks like Rich has been very thoughtful about it and is taking it in a great direction.

                                                              Clojure was the first functional programming language I fell in love with, but I haven’t spent much time writing Clojure code in a few years now because I found languages with nice type systems that were so refreshing and liberating for me that it’s hard to go back. Rich’s arguments about how wrong Haskell’s type system is just don’t ring true for me.

                                                              The problem of changing a return value from Maybe a to a isn’t nearly as much of a problem in practice as how he makes it seem. The compiler tells you what you need to change, and you change it. Furthermore, I find it is much more common to need to make the opposite kind of change where return values need to have more possible values (e.g. a to Maybe a). I have found those kinds of changes much more common, and a good type system really helps with those.

                                                              The bit about [a] -> [a] not telling you anything useful is bizarre. I guess he was “just exaggerating” but in the end it does nothing to foster healthy discourse.

                                                              It seems like Rich is very happy with the way he’s able to build things with Clojure and hasn’t found that Haskell’s type system provides a good set of trade-offs for him. Unfortunately the way he expresses these opinions tends to be overstated and absolutist, not really acknowledging that he is talking about trade-offs at all. It gives me the impression that he is trying to convince his audience of Clojure users that they should feel comfortable not exploring other ways of doing things, which I think is an unfortunate message to send.

                                                              1. 2

                                                                Suppose you used Dotty’s union types to implement a Maybe. Doesn’t this fall prey to the same hassle of having to (usually) change the callsite if you remove the ‘null’ type from the union?

                                                                I say usually because I assume that Dotty’s pattern matching must be exhaustive and most people would pattern match on the result.

                                                                1. 3

                                                                  You’ve got it backwards, and doubly so. First - tagged unions with explicit constructors are an imperfect way to implement union types, not the other way around. It seems that Dotty is an extension of Scala so naturally it has both (tagged unions being case classes, if I understand Scala correctly). Second - reducing the amount of acceptable parameters by removing the null option, what Hickey calls “strengthening the requirements” is an incompatible change. Function types are contravariant in their input, remember?