1. 4

    Minor adjustment: what was written here is mostly an interpreter. It’s probably worth noting the idea of a compiler targeting some other language (possibly a machine language). The idea of comparative linguistics and translation is important to introduce in any intro to PLT writeup.

    1. 2

      Good call. When actually writing interpreters (for example, the majority of my PL class), the two terms often get conflated.

    1. 1

      I’ll note the conflict of interest that I work at Cibo and sometimes with Dave, but I also think this is cool tech and possibly of a lot of interest to anyone who does data science in Scala.

      1. 3

        I call this the “template” pattern and it’s super useful!

        1. 4

          Fwiw, we can go even further and define these combinators in terms only of typeclass constraints needed on the return monad, the “finally tagless” or “mtl” style.

          https://hackage.haskell.org/package/parsers-0.12.8/docs/Text-Parser-Combinators.html

          Same comment from HN, but may be more interesting here.

          1. 0

            I have this horrible horrible feeling that Rust is becoming the new Perl. This all reminds me of when Perl added “object orientation” and things became more confusing and hard to understand for passers by like myself.

            1. 5

              Every serious new language needs to be able to solve the c10k problem; we knew Rust would need async I/O sooner or later.

              What is ugly is the proliferation of macros when a proper general-purpose solution is possible. If you look at the final example from the link, async! is fulfilling exactly the same role as the notorious try!; if the language would adopt HKT they could build a single reusable standard form of “do notation” into the language and reuse it for result, async, option, and many other things: https://philipnilsson.github.io/Badness10k/escaping-hell-with-monads/

              1. 6

                It is extremely unclear that a strongly-typed do notation is possible in Rust. It’s also not clear if we’ll ever get HKT directly; GAT gives us equivalent power, but fits in with the rest of stuff more cleanly.

                1. 3

                  What is GAT?

                  1. 2

                    generic associated types

                2. 1

                  I think async! will eventually be made into a language feature (a couple of community members have proposals for this), it’s just that we’re experimenting on it as a proc macro, because we can. It’s way more annoying to experiment with features baked into the language.

                3. 1

                  The only language feature added here is generators (and possibly, async/await sugar), everything else will be a library. Both things are quite common amongst languages; and shouldn’t be too confusing.

                  Everything else listed here is a library that you only deal with when you are doing async stuff. And the complexities listed are largely internal complexities; tokio should be pretty pleasant to work with.

                1. 5

                  Lenses are great fun, glad to see they’re making their way to more communities :)

                  1. 1

                    I’ve been meaning to look into lenses for quite some time. They seem to be useful when working with Redux. But when looking into how to implement them there are a lot of varieties of lenses. Do you know any good explanation of the theory bebind lenses?

                    1. 8

                      There are a few theories, the nicest one is as follows: a value of type Lens s a is a demonstration that values of type s can be split into (x, a) for some unknown type a. If we talk about Iso a b which says that a and b are the same type then Lens s a = exists x. Iso s (x, a).

                      Another way to look at it is that a lens pairs a getter and a setter, so we can say Lens s a is the same as { get :: s -> a, set :: a -> s -> s }.

                      All of these constructions have to follow some rules (the intuitive ones) like “if you set and then get you’ll get what you just set” also “if you set and then set it’s the same as just setting that second time” and also “if you set what you just got it’s the same as doing nothing at all”.

                      If you consider the construction a | b to mean “a or b, and I know which one” so that Int | Int is distinct from Int then we can consider a thing called a Prism s a which, parallel to above, can be considered the same as Prism s a = exists x . Iso s (x | a). In other words, when you’ve got a Prism s a you’ve got evidence for how to see s as either an a or something else.

                      Prisms aren’t really getter-setter pairs, but they’re something close. You need to be able to always convert an a directly into an s, but also maybe pull an a out (unless it’s the other thing). So, we say Prism s a is the same as { retract :: a -> s, extract :: s -> Maybe a } where Maybe means you might fail to produce that a sometimes.

                      This one is really a foreign concept. Here’s an example: there’s a Prism String Int which is a printer/parser pair for integers. In other words, we consider String to be equivalent to x | Int where x stands in for “all of the strings which fail to parse as an int”. So retract just prints an Int out and extract is our (potentially failing) parser. Prisms are printer/parser pairs.

                      All of these things can combine with one another. There are interesting theories around “Profunctor and/or Functor transformations” which make all of the compositions fall out as natural consequences. It’s good for implementers but only so useful for just understanding the thing.

                      There are also “type changing” lenses/prisms which let you handle generic/templated types well and more exotic “optics” like traversals and folds. All of these things continue to compose with one another nicely and fit into the same universe of ideas. It’s really quite rich!

                      1. 2

                        This is the best, most intuitive description of lenses I’ve ever seen. Thank you!

                        1. 1

                          I’m glad!

                        2. 1

                          In the first sentence: “… some unknown type x” not “a”. Whoops!

                    1. 6

                      There is a lot of study in linguistics relating to this stuff. Oftentimes, linguists are logicians and type theorists are all speaking the same language. That said, linguists often have the harder job unless they start turning into a logician or type theorist since actually classifying how human languages really work is pretty hairy.

                      1. 1

                        Any links or pointers to relevant materials?

                      1. 7

                        Just for my own notes really.

                        • Compiled languages are always faster. Compilation always has the opportunity to make things at least no slower, though genuine optimizations require more information and your compiler might not be using it (global optimizations, runtime information).

                        • floating point calculations will introduce non-deterministic errors into numerical results; OK, they introduce some errors into numerical results; alright I understand that floating point calculations are imprecise not inaccurate, Mister Pedantic Blog Author, but I cannot know what that imprecision is. Arithmetic operations on floating points are supposed to model real numbers, but real numbers are terrifically non-computational. If you haven’t explicitly studied computational analysis then you probably are missing a huge set of tools for making numerical computations with more than 1-3 sig figs work.

                        • at least the outcome of integer maths is always defined; fine, it’s not defined. But whatever it was, the result of doing arithmetic on two numbers that each fit in a data register itself fits in a data register. There’s lots of interesting behavior here! Again, though, it largely falls from distinction between our theories for things and our models. What is the right behavior for overflow?

                        • the bug isn’t in the hardware; bug-free computer hardware is completely deterministic; the lines on the hardware bus/bridge are always either at the voltage that represents 0 or the voltage that represents 1. Let’s be clear, there are public stories circulating now that our CPU runs Minix! Let’s also be clear that your hardware is treated as a discrete approximation to the physical/chemical process actually happening in your transistors.

                        • “Complete coverage”. Let’s just stop right here.

                        • “no hacker will target my system; information security is about protecting systems from hackers.” Take a look at your frontline server logs someday.

                        • “any metaprogramming expansion will resolve in reasonable time; any type annotation will resolve in reasonable time.” Metaprogramming is often computationally complete. Type inference/checking sometimes is and usually is at best a unification problem (often exponential).

                        • “OK, well at least any regular expression will resolve in reasonable time; can you at least, please, allow that regular expressions are regular?” DFAs are O(n), sure, and regexes can compile to DFAs sometimes. Basically every regex implementation adds features though that make them no longer representable/compilable to DFAs.

                        1. 1

                          This Ian opaque/transparent existential types. You can see some good exploration of this in OCaml and it works the way you want: it can be either structural or nominative depending on whether the information exists locally to treat the alias as transparent. These aliases work uniformly over any type, function or not.

                          1. 4

                            I think part of point (1) that’s important to address is: this is not programming. Or, to break it down further, formal methods are a different sort of thinking, have a different experience as you work through them, and arrive at different sorts of outcomes than you are accustomed to with many sorts of problem solving and particular with programming.

                            On your HN comment someone asked what they would get out of using Alloy. The best answer I could give was “deep insight” and “maybe a really interesting test suite”. This is sort of true but also not a great way to actually discuss what using these tools will produce.

                            Another part of (1) that’s important to address is: it’s probably going to be a big process of learning that you were wrong about things in really subtle ways. This, I feel, is one of the big barriers to entry on learning proofs generally: until you’ve gotten the hang of formalized reasoning you really, really want to believe that your own intuition is sufficient. If you’re open-minded then you’re in for a lot of stings as you learn a new mechanism of evidence and begin to create a healthy distrust for your intuition. It’s actually emotionally difficult.

                            So, I think all of this leads in to (2): immediate benefits and drives are super necessary to push through the initial emotional and mental difficulty of turning the corner on formal methods. Clearly articulating what “success” is and providing fast wins builds confidence. I aimed at this a bit above with “test suite” in that it’d be cool to not just identify flaws but be able to produce counterexamples that can be valuable when sharing what you learned with others.

                            Generally, I think a good foal for starting someone off on formal methods is: use them to be able to quickly take some kind of uncertainty you personally feel and translate it into a tool for building confidence in your uncertainty and communicating it with others. Type systems are, I think, particularly bad for this. They don’t have to be, but the fast compiler feedback loop is too tempting. The dependent systems get it right where they force you to go through multiple iterations of logical design as you build out an interface, but they’re waaaaay too difficult to get started on. Model checkers OTOH are great as I think many have discovered.

                            (3) I think is worth poking at as well. Things can take a long time to learn and someone should be aware that the rabbit hole exists. On the other hand, there are lots of TLA+ success stories where people get whole teams on board relatively quickly. You can have fast success and if that’s what you want then: go get it.

                            The goals of the student thus become important: how deep do they want to go? Discuss this with them and scale it appropriately. On one side, you can start off reading about logical foundations, constructivism, constraint solving. On the other, you can learn a convenient modeling language and delve into ever more complex practical situations.

                            I think all routes here are harder than they ought to be, though. Ironically, the “if you want to build a type system, first you must invent the universe” direction is probably the “easiest” in that it’s been done over as a school curriculum for years. Other routes have faster bang-for-buck but require delving through more partial texts. I think this is often what someone is asking for when they “want to learn formal methods”.

                            1. 1

                              Yeah, thinking of it as programming made some of it hard for me. Very different mindset. I probably need to look into explaining your great points about how the methods challenge people’s intuition. That’s what good science does in other areas. Like with some experiments, the results of the formal methods fight with what people expect. They might reject good data or internalize failings too much.

                              Your point about counterexamples for others was one of reasons formal methods were applied in high-assurance security and systems. The idea is the certification would product concrete evidence of certain properties that the supplier could hand to customers. This would be descriptions, formal specs/proofs, and tests in combination. Prior work shows this is helpful if the reviewing party takes the time to thoroughly read and check all that.

                              No 3 was about things like Coq and Isabelle, not TLA+. The easy stuff like TLA+ was No 2. I’ll try to clarify that in next version. I agree with your point on TLA+. If I’m reading you correctly, I also agree most people wanting to learn formal methods want practical techniques to improve their programming instead of the foundational stuff you describe. That might interest them but we need to be sure it’s the journey they want. Hence, my proposal. The partial texts problem is a big problem that combines rather painfully with the semi-complete tooling that Sophistfunk described. The good news is the lightweight ones have one set of good docs (Alloy) and one that’s in progress courtesy of a Lobster. ;) Once anything hits mainstream, I expect a lot of resources to emerge organically as people help each other and share models.

                              The heavyweight stuff still needs work, though. It’s too foreign to me for me to even say exactly what to do. Current stuff seems to pile things on people. Need a bunch that try to do it like the better books on programming. I will say the requirement is incremental process of building up skills with series of practical exercises.

                            1. 1

                              These concepts don’t really come from category theory, but definitely definitely applaud sharing this information!

                              Also, Box can be a type (or at least a value in the type language) if your language supports higher kinded types.

                              1. 21

                                I think this is a nice argument [1] and gets to the crux of some things. One of the most interesting is the tradeoff between time preference and explicitness. Without a doubt, if you and your team possess strong and relevant convention and the problem is simple enough then you can win on time by being implicit. Essentially: why write things everyone already knows.

                                What comes to mind suddenly is a perhaps-useful relation to the styles of two famous mathematicians. I’ll take a little bit of artistic liberty here, but…

                                Paul Erdös was an itinerant mathematician who traveled the world, crashing on other mathematicians’ couches and writing beautiful, small papers with them. His particular joy was tackling “small” outstanding “problems” in interesting ways (particularly in combinatorics). In other words, he didn’t build theories as much as he cleverly tackled problems people didn’t realize they could already solve. In the small, his work perhaps feels unimportant but over his entire career his techniques were refined by himself and others and built into practices and theories that got the ball moving in areas people where people were stuck.

                                Alexander Grothendieck was an profound motivator of 20th century mathematics where he worked at his peak over just a couple years embedded in a cabal of French mathematicians who he inspired to rewrite… everything. He compared his approach to other mathematicians with a metaphor of cracking a nut: some mathematicians would find the biggest hammer they had and whack at it hoping to crack the shell and then wresting it open; he sought instead to raise the level of the sea until the water would surround it and soften the shell making it trivial to open gently. He built master theories of category theory and used it to rewrite algebraic geometry, moving the field to a new era of exploration. His theories permeated other branches of mathematics and were both criticized for being “abstract nonsense” and also adopted as being better foundations for work and inspiring new revolutions in many other branches (logic, topology, probability, algebra, all over).

                                Both are considered wildly successful and vital forces for mathematical development, but both also approached the different sorts of problems with very different techniques.

                                Erdös could solve 30 small problems before Grothendieck got out of bed all while illustrating a great space of opportunity that people couldn’t see. His leadership was implicit. Grothendieck would then step in and eliminate the reasons we even believed those 30 problems were problems in the first place and connect Erdös’ implicit vision to every other branch of mathematics as the obvious, if abstract, bedrock. He explicitly sought to improve the tools all mathematicians use. Erdös worked alone and in very small collaborations and is known as prolific in many fields. Grothendieck commanded a following that publicly revolutionized one field and laid the bedrock for 30 other revolutions elsewhere.

                                So which sounds more like the problem solving you do? Most likely none of us are as influential as either of these examples, but I can sketch out a continuum between their approaches. What kinds of problem solving would work best for the kinds of outcomes you are seeking?

                                [1] although I caution the reader around some of the discussion of monoids which is slightly but not materially wrong

                                1. 5

                                  For those who want to learn a bit more about Grothendieck’s rather unusual life and writings: http://www.ams.org/notices/200808/tx080800930p.pdf (I found this after @tel’s comment piqued my curiosity).

                                1. 17

                                  Types let you represent how much information you have. If you want to deal with “arbitrary JSON which might be missing information at various points” there’s a type for that called Json and it supports automatic merges. If you want to inspect that type and learn small bits of information about it you have types for that as well, for instance

                                  isTopLevelObject :: Json -> Maybe (Map Text Json)
                                  isTopLevelArray :: Json -> Maybe [Json]
                                  isString :: Json -> Maybe String
                                  

                                  This “attempt to move up the information hierarchy to something more strict but possibly fail” is a very general pattern that usually goes by the name of “parsing”. In a program that uses types well you are constantly “parsing” your way to more information and typically should do so incrementally. If you “parse” all the way to a “concretion” then you’re asserting that your program is strict/brittle. That might be right… but if it’s not then you’ve just made a design mistake.

                                  ADTs are too heavily used. Row types are clearly a win for many sorts of systems letting types flow more freely when the time is right.

                                  That said, typed languages contain within them pretty much all gradations of untyped languages as well if you want to use those. This is the pragmatic heart of Harper’s much maligned “dynamic types are monotypes” argument.

                                  If you find (static) type information is never useful then you shouldn’t use a statically typed language. If you find it’s sometimes useful then you should use one and build types which represent your uncertainty. All of the “dynamic language tricks” will be available to you to use atop those “uncertain” types.

                                  1. 3

                                    tel: First, let me say that I mostly agree with your comments on this page, even though I’m a fan of dynamic languages over static ones.

                                    This notion of the transition from looser to stricter types as “parsing” deeply matches my intuition. However, my experience has been that most type systems are quite bad for this. TypeScript has been the first type system that I’ve used that isn’t totally unbearable for this task, because of support for associative data, optional keys, union and intersection types, subtyping, etc. This stuff is important when the results of parsing contains substantial partiality and inherit domain complexity.

                                    I’d also like to note that defaulting to any subset of static, nominal, closed, positional, etc. creates a path of least resistance that produces brittleness. You can say “you’ve just made a design mistake”, but even knowing better, I make that design mistake every time and it’s physically arduous to change my program when I need to. Furthermore, the power of a type declaration to constrain my program’s behavior great enough that it often leaves only little holes. It becomes a puzzle to start filling in those holes. That temptation is so strong that even I can’t resist it sometimes, losing hours of work, only to give up and add a cast with a dynamic guard. At least with increasing experience, I can predict that outcome earlier and earlier each time.

                                    1. 2

                                      I would like to second that TypeScript has one of the best type systems for the sort of messiness that arises in the outer layers of programs.

                                      It has extremely powerful tools that let your merge types, easily construct union types without having to add a boilerplate ADT layer, and has a very smart resolver for letting you then drill down to the right types without a bunch of superfluous casts. And it has a la carte exhaustiveness checks! Sometimes you have to trick the compiler since its structural typing for the most part, but you can find safe ways of doing it.

                                      We’re still not to the dream of a type system that lets you easily jump between value and type and handling partial information, but Typescript is getting us there.

                                      1. 1

                                        dream of a type system that lets you easily jump between value and type and handling partial information

                                        What is this dream?

                                      2. 2

                                        I think I’d love to understand your experience with Typescript more. I used it a while back a touch and bounced off (not using types to attack nulls seemed a bridge too far to my intuition, but that may have changed or improved since I last looked).

                                        Generally, I find that a set of lenses atop of a general Json type is an extremely nice place for dealing with things like missing data and the like in a sensible and convenient way. That said, I really like what row types and open sums/products can do and these open doors to very nice structural subtyping similar to what I think you’re discussing w.r.t. Typescript.

                                        So, yeah, can I encourage you to write about this topic through the lens of Typescript? I’d love to better see your perspective here :)

                                        1. 1

                                          not using types to attack nulls seemed a bridge too far to my intuition

                                          You can union null (or undefined) in to any type like this: T | null. However, it’s often preferable to model optionality at the level of the aggregate: {foo?: T} where this essentially means {} | {foo: T}. “Occurrence typing” is how you dynamically cast something like a T | null | undefined to a T: a ? a.b : 'whoops'. This also works with statements, such as an if-block. You can also use Partial<T> to explicitly decorate every key in T with ?, then massage that Partial with a union, intersection, etc.

                                          a set of lenses atop of a general Json type

                                          Lenses are a whole ‘nother topic. I think they’re a great idea, but current realizations have some usability problems that I won’t get in to here. Nor will I get in to how I think lenses could be made more accessible.

                                          can I encourage you to write about this topic through the lens of Typescript?

                                          Do you mean use of open records and subtyping specifically?

                                          1. 1

                                            Yeah, I believe that explicit sort of null handling is newer? Maybe I’m mistaken, though.

                                            I’d like to understand more completely how and why Typescript is good at handling input/boundary data well. I can intuit it from the feature set and believe it from the fact that it’s deployed in Javascript and frontends are full of these problems, but I’d like to understand it better from a practical POV since I think a lot of this comes down to UX.

                                            Maybe it’s: “how do the constellation of type system decisions made in Typescript combine to make it really good at handling messy input data?”

                                            1. 1

                                              I think the answer has to do with 1) JSON is actually quite a good data representation. And 2) The TypeScript team refuses to change the semantics of JavaScript.

                                              Now, JSON has a lot of problems, like the lack of extensibility (see tagged literals in Clojure/EDN); isn’t a great language for config files and such (no comments); no head/body syntactic-term type (like lists in EDN, or normal ADT constructors); and lots more problems. But overall, the presence of open associative structures makes it quite good for representing things and surviving growth: Just add some more keys! This is critically important for future-proofing systems. See Rich Hickey’s “Speculation” talk for more about “Growth”: https://www.youtube.com/watch?v=oyLBGkS5ICk

                                              Second, by insisting on not changing the semantics of JavaScript, the TypeScript team was forced to build type system features that support open and extensible data. For example: With associative data, there is a neat trick that unknown keys are disallowed in literals, but are perfectly allowable in dynamic values. This means that you can survive extra data. Coupled with the reflective capabilities such as Object.keys you can write code that can safely be future proof, but to new data from clients, but also to new requirements internally.

                                              Another thing is the inclusion of type assertions, which let you hint to the type system without being forced to reconstruct a type completely. This means that if you get JSON on the wire, you can write a predicate that asserts new static information about it:

                                              let isStrings = (x: any): x is string[] => {
                                                return Array.isArray(x) && x.every(y => typeof y === 'string');
                                              };
                                              

                                              Now you can do something like:

                                              if (!isString(x)) {
                                                throw Error('expected strings!');
                                              }
                                              x.forEach .... // this is statically-safe now, thanks to occurrence typing.
                                              

                                              To accomplish the same thing in a stricter language, I’d have to make a copy of the underlying array. If I have a deeply nested associate structure, I’d have to write a lot of code to do O(N) work to reconstruct everything. Note that I can also do potentially unsound things like this:

                                              let isStrings = (x: any): x is string[] => {
                                                return Array.isArray(x) && (x.length === 0 || typeof x[0] === 'string');
                                              };
                                              

                                              This does O(1) work and defers errors to later if somebody passes ['foo', 5]. When you trust the client, this is super useful for doing the equivalent of pattern matching. And in fact, you can do switch-on-strings, since literal string types are supported:

                                              let Foo = A | B;
                                              let Bar = B | C;
                                              
                                              interface A { tag: 'A'; blah: boolean; }
                                              interface B { tag: 'B'; }
                                              interface C { tag: 'C'; }
                                              
                                              let f (x: Foo) => {
                                                switch (x.tag) {
                                                  case 'A';
                                                    // here it is safe to use x.boolean
                                                    // I can also get exhaustiveness checks here.
                                                   // note that valid cases are A and B, but not C, even though I reuse the B type in two unions.
                                                }
                                              }
                                              

                                              The syntax isn’t quite as nice as pattern matching, but combined with destructuring, it’s not so bad:

                                              if (x.tag === 'A') {
                                                let {blah} = x;
                                                ...
                                              }
                                              

                                              Ideally, there would be more tools to do more of this in a sound way, rather than rely on you to write correct type assertion functions, but again: They can’t change the semantics of JavaScript. That would require a fair amount of runtime library support for pattern matching and the like. Hard coding some cases like literal string types gets you pretty far.

                                              All this stuff (and plenty more) enables me to write data representations that make sense to a human: as simple JSON encoded data, with human-friendly grammars as types, reusing data definitions in arbitrary positions, future proofed to new data including additional keys, etc.

                                              Furthermore, if you believe in the types as parsing intuition, then you’d want your type system to support the same kinds of things that CFGs support: alternation, concatenation, etc. TypeScript isn’t quite there yet with cat, but at least union types let you emulate alt. Cat will probably eventually come in the form of session types in some languages.

                                    1. 13

                                      Except that, even though you knew the type, you still knew nothing about the structure of that JSON

                                      That’s nonsense, use Generics or TemplateHaskell and make a config that fits the pattern you want and you get automatic JSON parsing from your types in addition to getting to know the structure of the JSON based on the structure of the data type. This isn’t a property some Haskellers actually want, but the option is there if you do.

                                      What we wound up doing was nesting pattern matching to get at the value we wanted. The deeper the JSON nesting, the deeper our pattern matching. We wrote functions to wrap this up and make it easier. But in general, it was a slog. The tools Haskell gives you are great at some things, but dealing with arbitrarily nested ADTs is not one of them.

                                      This is utterly unnecessary for both the JSON handling itself: https://github.com/bitemyapp/bloodhound/blob/master/src/Database/V5/Bloodhound/Types.hs#L1951-L1965

                                      And for working with nested types in general: you can use lenses, but even with a very nested API like Elasticsearch’s just being able to compose accessors is generally enough.

                                      I’m familiar with this guys’ schtick, he did some Haskell back in like 2010 with a Happstack based application and nothing was particularly nice. If I was stuck with Happstack, I’m not sure I would want to use Haskell either.

                                      Rich had some good points about positional semantics. When defining an ADT, positional semantics make it hard to read the code. If you’ve got seven arguments to your ADT’s constructor, you’ve probably got them in the wrong order and you’ve forgotten one.

                                      First, I avoid passing a bunch of the same type in consecutive order to a data constructor.

                                      I avoid Text -> Text -> Int -> Int -> MyType and replace it with: HostName -> Alias -> PortNumber -> WorkerCount -> MyType or some-such, further, if there’s more a few arguments to a data constructor I’ll use record syntax to construct the value:

                                      MyType {
                                           hostname = myHostname
                                        ,  alias = myAlias
                                        ,  portnumber = myPortnumber
                                        ,  workercount = myWorkercount
                                        }
                                      

                                      Then ordering doesn’t matter. I usually set record fields strict, especially if I plan to use the record syntax, so that I can’t forget anything.

                                      The fact is, in real systems, these ADTs do accrete new arguments.

                                      I can’t say I recall the last time Value got a new constructor or one of its data constructors got a new argument, but I’d sure be grateful that my type system would be there to tell me everywhere in my project I need to fix the code if it did.

                                      I don’t have the patience for the rest of this.

                                      — Went from Clojure -> Haskell

                                      1. 8

                                        Don’t waste your breathe dude there is a much simpler rebuttal: static and dynamic types are a false dichotomy so any attempt to discount one in context of the other is an invalid argument

                                        Instead weight the specific features of the type systems against each other. In the case of Haskell vs Clojure, GHC gives you much more feedback than the entire Clojure ecosystem of spec/schema etc so if the criteria for choosing a language is type safety then Haskell wins.

                                        1. 12

                                          static and dynamic types are a false dichotomy

                                          Absolutely. People have written some useful stuff about this:

                                          https://existentialtype.wordpress.com/2011/03/19/dynamic-languages-are-static-languages/

                                          1. 2

                                            I actually don’t totally agree with that article. I’m channeling Foucault here: our understanding of type systems is dependent on the context of type theory. How do we know our type theory is the most true? Sure it allows us to prove lots of things, but that is a pragmatic measure. If type theory has pragmatic value, then our type systems should be measured pragmatically as well. Clojure actually wins in many respects because you get lots of monadic interfaces (the collection abstraction) that allow for reusuable code without the difficulty of the IO monad etc. This is not true in many other dynamic languages, e.g. Python.

                                            So the “false dichotomy” is really apparent when you compare the monadic-ness of Python, C, Haskell, and Clojure. Clearly Clojure and Haskell fall into the “monadic” category and Python and C into the “non-monadic”. This “monadic-ness” distinction tells us more about how composable functions and data structures are than the “static” or “dynamic” distinction, which means that “static” and “dynamic” categories aren’t the most useful categories to put languages into.

                                            1. 3

                                              How do we know our type theory is the most true?

                                              Is this really the question at stake? If a language tells me a variable is a certain type, I tend to believe it. Truth doesn’t seem to be an appropriate axis for evaluating type systems.

                                              There’s definitely lots of variety regarding what a type system can tell you. The static/dynamic distinction is about when you can know the type information. Static languages allow you to know the types at compile time, whereas dynamic languages force you to wait until runtime.

                                              1. 1

                                                Is this really the question at stake? If a language tells me a variable is a certain type, I tend to believe it. Truth doesn’t seem to be an appropriate axis for evaluating type systems.

                                                Within the context of your language, a variable has a type; this is the truthiness of that type. Within the context of type theory, the type system of that language has a certain truthiness to it. Within the context of the real world, type theory has a truthiness to it. The implied question in most static vs dynamic debates is, how true is my type system? Which begs the question, how true is type theory?

                                                The answer is that it doesn’t matter. What matters: how useful is type theory generally? How useful is the specific type system I’m using? The static vs dynamic distinction doesn’t do much to help us answer this usefulness question. Understanding when types become relevant in a language definitely does help you, so I agree with you on that, but there are other more interesting aspects of type systems too.

                                                1. 3

                                                  Maybe it’s just me but I really can’t follow.

                                                  Within the context of your language, a variable has a type;

                                                  Sure, no qualms yet.

                                                  this is the truthiness of that type.

                                                  Completely lost. What is the truthiness? I don’t know what your pronoun “this” is referring to.

                                                  I think I will continue to be lost until you can explain what it means for a type system to be false. “True” does not signify anything to me here.

                                            2. 1

                                              That is the most absurd thing I’ve read. Static typing is necessarily more restrictive because you have to express yourself in a way that can be verified by the type checker at compile time. Thanks to Godel, we know that the set of provable statements is a subset of all valid statements. Since dynamic languages do not attempt to prove correctness, they allow you to make many interesting statements that are impossible to make in a static language. Here’s a trivial example:

                                              (eval ’(defn add-one [x] (inc x)))

                                              (add-one 10)

                                              You should also read this in depth rebuttal to the link you posted http://tratt.net/laurie/blog/entries/another_non_argument_in_type_systems.html

                                              1. 14

                                                That whole rebuttal rests on a conflation of dynamic types and static types. This is the standard stance for folks arguing from the dynamic camp (specifically, they’re two of the same sort of thing) and the opposite is so basic to the mental space of the static camp that it almost always goes unspoken.

                                                In summary, if Bob’s article had explicitly stated that he was solely considering static expressivity… I would have agreed with it.

                                                So, yes, Bob did not state that explicitly but it is taken without need for explicitness within static typing literature.


                                                My take is that there are two “kinds of power” any formal language may be judged upon and that they are in tension. They are: the power to create and the power to analyze (deconstruct).

                                                The more power to create a formal system affords the greater richness of expression exists and the more complex of “values” (whatever that might mean) can be constructed.

                                                The more power to analyze a formal system affords the greater richness of semantics the values of the language have and the greater information can be extracted from each one.

                                                Types are an alternative semantics to runtime evaluation. Thus, asking for types is asking for greater power to analyze and comes at a cost of power to construct.

                                                From here, an argument must move on to tradeoffs between power to analyze and power to construct. Why wouldn’t you want to just side entirely with power to construct?

                                                Most arguments of the static typing apologist end up being arguments for the value of power to analyze. Essentially, having more parallel semantic interpretations makes the system you’re working with more stable (fewer bugs, easier refactoring, partial verified documentation) and also opens up doors for other systems to exploit those other interpretations (typeclass resolution, servant, dependent types at large).

                                                So which is better?

                                                This is just a value judgement now. Nobody can answer for anyone else.

                                                But I think that power to construct/power to analyze tradeoffs happen constantly so it’s a good concept to have an eye on.

                                                1. 1

                                                  I pretty much agree with what you’re saying, and that’s ultimately the tradeoff between static and dynamic typing. Depending on your situation one or the other might be preferable.

                                                  1. 4

                                                    I think an extension on this is that the technology already exists to make those tradeoffs locally within a single program. Haskell excels at this

                                                    • the ability to embed DSLs and effect contexts lets you pick and choose this power tradeoff
                                                    • the existence of the IO monad (as much as it is sometimes maligned) means that there’s an “escape hatch” to compare all of your more analyzable structures against
                                                    • the existence of the Dynamic type as well as other “semi-dynamic” types like Json gives you opportunity for greater power at the cost of analyzability

                                                    I think what’s most interesting is that the third point about is rarely explored. The real place where this exploration seems to happen is in row typing/structural typing a la Purescript or OCaml, but we could be building more and more sophisticated ways of working with the Json type without constraining its type.

                                                    I think the lack of this development is why Haskell is really bad for exploratory data analysis, but it’s mostly the lack of exploration/development rather than a total failure, I believe.

                                                    On the other hand, gradual typing systems also are an attack at the middle ground but I’d place a smaller bet on them. Ultimately, I think it’s easier to “forget” analytical power and regain constructive power than it is to go the other way. I’d be willing to put some money on this being a very fundamental logical tradeoff.

                                                    So: where are the research projects for EDA in Haskell? I’d love to see them.

                                                    1. 2

                                                      While this is all true, the actual workflow is quite different than it is in a language like Clojure. A lot of the time I don’t know the solution up front, and I don’t know what the types are going to be in the end. It’s actually valuable to me to be able to work with an incomplete solution. With Clojure, I can use the REPL to explore the shape of the solution. I can also do a workflow that’s very similar to type driven development with Spec as seen here.

                                                      Personally, I find that I generally want a specification at the API level, as opposed to having to structure all my code around types. For me, Clojure default is simply more ergonomic. Meanwhile, Spec provides me both with a sufficient guarantees regarding what the code is doing, and a meaningful semantic specification.

                                                2. 3

                                                  Any dynamic procedure can be implemented in any turing-complete static language; you just have to express your uncertainty about its runtime behavior in the type system. Even in a total language, you can build your entire program out of partial functions just by modeling that partiality with Maybe return values. Even in a pure language, you can build your entire program out of impure functions just by modeling that impurity with an IO monad or something similar. Even in a language with strong data types, you can write all the dynamically-typed code you want just by creating a large sum type of possible cases and using those overly-broad types everywhere (which will almost certainly require modeling some partiality as well). In general, you can write arbitrarily dynamic code if you’re willing to get very few useful guarantees from your type system. That may sound bad, but it’s no worse than writing in a dynamic language, where you get no static guarantees at all from your language (except maybe some very broad invariants about, for example, memory safety).

                                                  1. 2

                                                    And everything you can do in a language like Haskell, can be done using assembly. Yet, I think we’ll agree that ergonomics of writing assembly are not the same. It’s simply not interesting to discuss what’s possible in principle, it’s the actual workflow that the language facilitates that matters. The way you express yourself in a dynamic language is very different from the way you express yourself in a static one.

                                            3. 7

                                              I watched Rich’s talk a couple of times, and I think that positional issues can’t be dismissed. Even if you have record syntax for data, to my knowledge you don’t have this for types. Every time I pull up one of the more complex Haskell libraries, I first have to spend a bunch of time deciphering what I need to put in each position of the types. You end up having to rely on convention and hoping people don’t reorder.

                                              named arguments are somewhat self-documenting and help with the debugging scenario as well.

                                              I like that ADTs end up breaking all over the place in code when I change things, but I also like how my Python code and things likes dictionaries mean that I can work on objects by concentrating only on the parts that “matter”. It’s a tension, but I don’t think it’s a dicothomy and we can have solutions.

                                              ADTs that represent objects (Money or Complex) are definitely of the kind where you want to reconfirm everything, but what about ADTs for configuration objects? Does adding another configuration parameter really require auditing everything? The canonical solution might be to decompose that a bit further to avoid this, then you run into the acretion problem. I’m writing wrappers upon wrappers for things.

                                              It’s a shame that Rick decided to be so rant-y, because there were some valid points in there. The common ADT patterns force us down a very specific kind of static verification that end up shoehorning us into verbose patterns. I disagree about overall maintenance, but it does lead us to boilerplate.

                                              1. 7

                                                The issue with losing positional arguments in types is that it’s desirable to be able to talk about partial application without creating new type names (as equality in types gets more complex you run into hairy, very difficult problems quickly, so: if you solve partial application with aliases, how do aliased types equate?).

                                                For instance, Scala has the worst of both worlds: positional types and very, very painful partial application.

                                                With respect to your earlier statements though I agree a lot. ADTs are overly strict in that they represent a demand of both existence and completeness. Most Python idioms rely on an informal “structural subtyping” formalism (what people call duck typing sometimes) which is verrrry nice.

                                                This works very well in the statically typed world: see OCaml or Purescript’s record types (and open variants).

                                                1. 1

                                                  Most Python idioms rely on an informal “structural subtyping” formalism (what people call duck typing sometimes) which is verrrry nice.

                                                  I guess you’re referring to stuff like __iter__ and __str__ etc? It’s been a long time since I used Python, but I did like those “duck typed interfaces”.

                                                  But did you know there’s something similar in Clojure, with “protocols”? :)

                                                  1. 2

                                                    I meant to point more at the heavy use of dictionaries in Python, though the more I think about it the less it seems like a clear example.

                                                    Clojure really is one of the clearest examples. Idiomatic Clojure just involves passing dicts around and assuming you’ve got the right keys, hoping that there’s a solution (is there an intersection between the keys produced and the keys needed across all points in dataflow?). This is quintessentially “duck typing” and I think it’s a practice that’s well explored in the world of row types.

                                                2. 2

                                                  It’s a shame that Rick decided to be so rant-y

                                                  How exactly was he ranty?

                                              1. 4

                                                The premise of this article is invalidated by the observation that “static vs dynamic” types are a false dichotomy

                                                1. 1

                                                  Why is that a false dichotomy?

                                                  1. 6

                                                    There are many ways you could put a language’s type semantics into a category. You could say it is based on Hindley-Milner inference like Haskell, or sequent calculus like Shen. You could say it allows arbitrary side effects like OCaml or it supports managed side effects like Haskell. Even the differences between Purescript and Elm are enough to be two different categories of type systems. You could say it supports custom types like Clojure (deftype), or it supports classes like Python. You could say it supports objects as types like Ruby or as prototypes like Io.

                                                    Given the multiplicity of forms that a type system can take, why do we put all type systems into either a “static” or “dynamic” category? Are these specific categories useful? I really don’t think so. I love writing both Clojure and Haskell because I think both languages are well designed for very different reasons. Both languages have monadic abstractions as part of the standard library (which Python/Ruby/JS do not have), and when look at it from that perspective Clojure and Haskell seem a lot more similar than they do different.

                                                    1. 2

                                                      I definitely buy that there are other dimensions for evaluating languages, but existence of a rich static type system is a giant one. That said, I basically don’t see substantial similarity between static types and dynamic types. To my experience and understanding, types solve very different problems than classes or “tags” (like deftype).

                                                      Maybe not “false” dichotomy, more “one of many”.

                                                      1. 2

                                                        Whatever you want to call it I still don’t think “static” vs “dynamic” is that useful, it just doesn’t tell me much about the language. Also consider my second paragraph here: https://lobste.rs/s/l9foze/clojure_vs_static_typing_world#c_htqfpw

                                                        1. 1

                                                          We’ll have to agree to disagree. Fwiw, I don’t think Clojure captures a monadic interface at all. You can do it a little bit (see funcool/cats) but it’s difficult and doesn’t really capture the abstraction well. I’ve never seen a dynamic language talk about monads effectively.

                                                      2. 2

                                                        Certainly there are many different kinds of type systems. But that doesn’t mean static/dynamic is a false dichotomy. What it’s really getting at is “Can I know the type of this variable at compile-time? Or do I need to run the program in order to find out?”

                                                        There is some crossover. From the dynamic camp, Python and Clojure are experimenting with gradual type systems. From the static camp, C# has added the dynamic keyword.

                                                        1. 4

                                                          As long as you can construct a type hole, or use a similar trick, Dynamic can almost always be built in a “static typed” language even without a keyword.

                                                          1. 2

                                                            Okay but, unless I’m reading you incorrectly, your implied definition of “type system” derives from assuming that static types are the “true” types, and dynamic types are just “static types that we call eval on” or something like that.

                                                            This is why I mentioned Foucault in the other comment because the trueness of your static type system is only relevant in the context of your type theory. If you have a different type theory, your type system won’t have as much truthiness to it. This is why Shen is so interesting because the type system subsumes Hindley-Milner. Most static-type-fanatics only know HM type systems, and they perceive HM as the one true type system. If Shen’s sequent calculus type system can subsume HM, when what does that say about the truthiness of HM?

                                                            The problem is the truthy claim of any type system over another, hence the false dichotomy. So forget about static or dynamic. Your question, “Can I know the type of this variable at compile- or run-time?” is a better question, though I would replace it with “When do types become relevant in this language? Is it only compile-time (Haskell), only run-time (Python), or both (Shen)?” In addition, I would encourage considering other aspects of type systems, such as whether or not they allow arbitrary side-effects, whether or not they are built around monads. Don’t just jump to “is it static or dynamic?”

                                                            tl;dr focus on the functional qualities of type systems in languages, not whether they conform to the “true type theory”.

                                                            1. 2

                                                              I think there’s a huge distinction because “interpretation” or “reduction” or “runtime” has wildly different properties than static analysis. Even dependent types which “blur the boundary” don’t really, they bring small amounts of computational power to static analysis.

                                                              What makes runtime so much more complex? The easy answer is “side effects”, but we can go further and consider the question even for entirely pure languages: what makes runtime so different?

                                                              I like to view it in terms of abstract interpretation. Outside of runtime there are many sorts of approximations you could run and a wide variety of interpretations and expenses that are possible. Runtime collapses all of the “abstract” interpretation into a single concrete one. It feels topological: we’ve moved from discussions of opens to a single point.

                                                              From one perspective this is nice or perhaps even not such a big deal, but those two worlds are different. The topology of abstract interpretations is rich with structure and can be related to other structures (model checkers, logical statements, etc). Runtime kills off all of this structure in favor of concreteness: this is the answer. Of course, as programmers we’re interested in the answer (but not everyone is: see proof checkers).

                                                              It’s on basis of this that I see there to be a huge distinction between static and dynamic. Within the world of “static” there is a wide variation in capability (the entire topology of abstract interpretations) but it is all separated from the world of “runtime”. Within “runtime” there’s also a whole world of power, but each thing behaves totally differently from the elements of the static world.

                                                              In some sense, I visualize this space as a giant general cone: http://philschatz.com/precalculus-book/resources/CNX_Precalc_Figure_10_04_001.jpg

                                                              The spaces of static interpretation and dynamic interpretation are totally distinct touching only at the point of compilation. Each is rich, plausibly unboundedly so, and you can relate the two structures (hyperbola), but they also have their own unique perspectives (circles, ellipses).

                                                              1. 1

                                                                I don’t know what you mean by truth in this context. Are there any false type systems? If Javascript says typeof(2) is “number”, then that’s true in the Javascript context.

                                                                Most static-type-fanatics only know HM type systems, and they perceive HM as the one true type system. If Shen’s sequent calculus type system can subsume HM, when what does that say about the truthiness of HM?

                                                                In this case I would describe Shen’s type system as “more powerful” or perhaps “more descriptive”. It doesn’t make HM false by any means.

                                                                I would encourage considering other aspects of type systems, such as whether or not they allow arbitrary side-effects, whether or not they are built around monads. Don’t just jump to “is it static or dynamic?”

                                                                The trouble with not knowing your types until runtime is they are sometimes path-dependent. A dynamically-typed function might return a number now, and a string later. It’s much harder to analyze the program for problems. When types are known at compile-time, you have the opportunity to check for correctness, and it will apply to all possible runtime configurations.

                                                                1. 2

                                                                  In this case I would describe Shen’s type system as “more powerful” or perhaps “more descriptive”. It doesn’t make HM false by any means.

                                                                  Yes, this is exactly my point. Truth and false aren’t a useful distinction for type systems. The problem is that the way that most static vs dynamic arguments are framed is by referent to type theory as “the one truth”. Then, a “good” type system is one that adheres to the One True Type Theory. Thinking about type systems in this way severely limits your perspective.

                                                                  The trouble with not knowing your types until runtime is they are sometimes path-dependent. A dynamically-typed function might return a number now, and a string later. It’s much harder to analyze the program for problems.

                                                                  Right, this is a problem that certain type systems have. Notice that, in the next sentence, the only solution you can think of comes directly from the static vs dynamic dichotomy that you already setup in your head.

                                                                  When types are known at compile-time, you have the opportunity to check for correctness, and it will apply to all possible runtime configurations.

                                                                  What if there was another way to check for correctness that doesn’t depend on static analysis? Clojure uses spec and test.check to do automated property testing; is this not correctness-checking? What if there was a different way to guarantee safety without analysis? I don’t know the answer, I’m just saying that your framing of the problem is limiting your possible solutions.

                                                                  1. 2

                                                                    Clojure uses spec and test.check to do automated property testing; is this not correctness-checking?

                                                                    This is testing. In contrast, a static type checker proofs correctness. Testing is a proof if it is exhaustive, but that is not feasible for most programs.

                                                                    1. 1

                                                                      Static type checkers don’t prove correctness of the code, only the correctness of the code’s types. It’s telling that one of the big innovations in testing, generative/property-based testing, came from Haskell.

                                                          2. 3

                                                            Because it’s a continuum. Forth has one a total of one type; C pretends to have more than one type but isn’t particularly convincing about it; Ruby has dynamic types and nils are everywhere; Erlang has dynamic types with no nils; Racket can have dynamic or static types without any nils to be seen; Java has static types but every single type is unioned with null so you don’t get very good guarantees about runtime behavior, etc.

                                                            1. 2

                                                              The static v dynamic types themselves are still dichotomous: information exists either at compile time or runtime. Dependently typed langs screw with that but it mostly holds.

                                                              A whole language can take part in both sides and use either side to try to give various kinds of guarantees of various strengths, but behind it all there’s still a strong distinction between dynamic and static.

                                                              1. 2

                                                                Yeah, that’s basically what I was saying. It’s a false dichotomy when you use it to categorize languages into one of two buckets, but it’s a valid criteria for describing a given language, as long as the description has room for more than a single bit.

                                                        1. 4

                                                          Fact #15: if you try to implement a library for recursion schemes in go, rob pike will burn your house down.

                                                          Any body care to elaborate on this? Disclaimer: I know nothing about go.

                                                          1. 12

                                                            Recursion schemes are very general forms of constructing or deconstructing recursive data types. They rely heavily on not only generics but higher-order generics (generics over generics). Rob Pike has repeatedly suggested that generics aren’t worth it for Go and it’s the most constant criticism that arises about the language… so implementing something like recursion schemes swings about as hard as possible in the other direction.

                                                          1. 4

                                                            At the risk of sounding very ignorant, what is Functor Oriented Programming, and what would a language that supported it well look like?

                                                            1. 14

                                                              The author advocates the heavy use of natural transformations, which (using the type system) statically force you to separate concerns. This removes discipline requirements from the human and shifts them to the compiler.

                                                              I haven’t used these in many places, so I can’t speak for the author, but one cool way I’ve seen these used is this: imagine you want the user to provide a function that does something to a tree (e.g. cut off the bottom layer, remove the left side, etc.) but under no circumstances does the function vary its behavior depending on the contents of the tree. This is important to creating well-behaved algorithms sometimes. You could do

                                                              foo :: (Tree a -> Tree a) -> [Tree a] -> [Tree a]
                                                              foo = map
                                                              

                                                              The problem is that the user could easily supply a function like

                                                              foo (fmap (+1)) 
                                                              

                                                              Which is wrong because it inspects the contents of the tree, not just its structure. Instead, if you declare

                                                              foo :: (forall a . Tree a -> Tree a) -> [Tree b] -> [Tree b]
                                                              foo = map
                                                              

                                                              It is impossible to provide a function that actually inspects the contents of the tree. The function must be a “natural transformation” which cannot inspect the tree’s contents.

                                                              You can prove other cool things like this using universally quantified types (the “forall”); this is a very contrived example. Check out stuff like

                                                              https://hackage.haskell.org/package/mmorph-1.1.0/docs/Control-Monad-Morph.html#v:hoist

                                                              https://hackage.haskell.org/package/base-4.10.0.0/docs/Control-Monad-ST.html#v:runST

                                                              https://hackage.haskell.org/package/free-4.12.4/docs/Control-Monad-Free.html

                                                              I’m not sure how you would use this construction all the time, and I don’t know what a language conducive to it would look like. It would probably support impredicative types.

                                                              1. 1

                                                                Thanks, an example like that is missing in the article.

                                                                For Java programmers, this seems to be equivalent to wildcards?

                                                                Tree<B> foo(Function<Tree<?>, Tree<?>> a, Tree<B> b)
                                                                
                                                                1. 1

                                                                  More like polymorphic methods:

                                                                  interface Func {
                                                                      <A> Tree<A> apply(Tree<A> arg);
                                                                  }
                                                                  
                                                                  <B> Tree<B> foo(Func func, Tree<B> arg);
                                                                  

                                                                  Update: Silly mistake.

                                                              2. 8

                                                                One of the big insights of ML-like languages is that you can have a really, really rich language of “algebraic data”. Essentially, this means building rich types up from first creating “patterns” via “AND”-ing, “OR”-ing, functions, constants, and “holes” and then taking their fixed points with respect to those holes.

                                                                Or, as an example, consider the following data

                                                                F a b = Empty | Pair a b

                                                                which reads as either Empty OR Pair which is the “hole” named a AND the “hole” named b. You take the fixed point by repeatedly filling a hole with a new copy of the pattern, so here’s one step of taking the fixed point with respect to b

                                                                F a b = Empty | Pair a (F a b)
                                                                

                                                                and if you do this “infinitely many” times, you end up with a linked list.


                                                                Functor-oriented programming takes this perspective one step further by noting that while we often like to work at the level of the fixed points of these patterns (“functors”), we usually define functionality inductively, one “layer” at a time. If you write all of the functionality of your program directly at this level then you can just compose things together later with some very, very generic and very, very composable glue.


                                                                The issue is that it’s a big kludge to talk about this “layer at a time” programming in Haskell since Haskell really does want to deal with the fixed points instead. While there are plenty of provisions available to talk about “functor oriented programming”, it’s not the exact aim of Haskell and therefore the UX kind of sucks.

                                                                Most importantly, you have to provide a lot of hints to the type system to tell it what you’re doing with regards to that Really General Compositional Glue I mentioned above. This is annoying and sort of unforced. The reason Haskell requires this is because we can add mechanisms for “functor oriented programming” on as a library… and thus none of it is privileged by the compiler.

                                                                1. 4

                                                                  Well a functor is a type with an associated map function. This allows you to take a function from a -> b and map it to M a -> M b, M being your functor type. Good example is List and its associated map function. That all being said I haven’t a damn clue what they’re talking about.

                                                                1. 4

                                                                  What exactly is the takeaway from this rant? “Read warning messages”? “Keep your software updated”?

                                                                  I get that the author’s reason for writing was to tell off the complainers, essentially saying “it’s your fault for trusting library authors; it’s not the library authors’ fault for breaking your application.” But isn’t that logic twisted? If a library maintainer introduces a bug, obviously it’s the library maintainer’s fault. The question is whether the application developer has a Plan B for this type of situation.

                                                                  If “have a Plan B” is indeed the intended takeaway, I wish the author would say that clearly, rather than the blame-shifting approach of “you are incompetent because you trusted open source.”

                                                                  1. 4

                                                                    The takeaway is: Always pin your dependencies (possibly except dev env). We used shrinkwrap for this, now npm provides a lockfile, same for yarn.

                                                                    1. 4

                                                                      The takeaway I read into it is that if your production build process has the ability to install different versions of a million different dependencies than the ones that you previously tested on (for instance because it uses npm install and semver ranges and doesn’t lock things down), then you will at some point get screwed by that fact.

                                                                      You should have the ability to deploy the exact same code (including deps) that you test. You should have the sense to only do this. You should have tests, and they should not suck. If you do this, you won’t be the guy who said “this caused me hours of fumbling while prod was on fire”, you will be one of the people saying “my build broke, I changed the version pin, it’s OK for now”, which is better.

                                                                      If you want to be even more careful, then you don’t use ranges at all; you choose your dependencies, you maintain your own copies (so that no one else’s outages or pwnage spill onto you), and you don’t upgrade anything without having someone competent review the changes for any possible effect they have on you. Because code is so malleable, we as programmers make it a habit to replace the support columns in the basement with the newest high-tech design every time we repaint the cabinets in the kitchen… and then every now and then the house falls down in the process.

                                                                      1. 1

                                                                        Yeah, companies of any meaningful size relying on flaky redistributors like npm has always felt… odd

                                                                        1. 1

                                                                          because it uses npm install and semver ranges and doesn’t lock things down

                                                                          FYI, npm 5 and later generates a lock file automatically, so you’d have to specifically go out of your way to do this now.

                                                                        2. 2

                                                                          I don’t think that logic is twisted at all.

                                                                          There is an implied contract in open source that the maintainer(s) of a project promise constancy of some amount of functionality: that regressions are to be avoided and fixed and even that extensions and intentional changes are broadcasted and controlled. The implied contract holds because there’s an expectation that the maintainer(s) want the project to be successful as measured by the number of people who trust them enough to download and use the project.

                                                                          But that contract is just implied. For all you know, any one of your dependencies is actually has secret goals of being an art project to see how programmers respond during crisis. Or, more to the point, a con.

                                                                          The only way a bug or regression or breaking change is the “fault” of the maintainer(s) is if this implied contract holds.

                                                                          So it’s not so much whether or not you have a Plant B as much as whether you realize that you don’t have actual counterparties in the maintainer(s) who have given you a large amount of functionality you rely on. Whether you, as the responsible party for a service upon which you probably do have a contract for continued existence, support, and provision of services to your customers, are managing systemic risk appropriately.

                                                                          The obvious solution is to lock down dependencies and reduce “counterparty” risk on open source projects you use. More systemically, however, the solution is to be cognizant of assumptions like these and realize that they’re squarely upon the shoulders of the application maintainer.

                                                                          If that’s not good enough, then consider making a contract with the open source maintainers. You’ll probably need to pay them.

                                                                          1. 2

                                                                            This “implied contract” thinking is one of the strangest I’ve seen since joining BigCo. People will ask “is the project well maintained?” and don’t seem to think that the alternative is we write it from scratch – as a tech company, we write a lot from scratch and maintain all that. I’d much rather be forced to maintain a project for our use if upstream goes away then have to write from scratch and maintain!

                                                                            1. 2

                                                                              To be fair, there are lots of cases in modern development where the alternative to taking on a dependency isn’t rewriting the whole thing from scratch; it’s being moderately inconvenienced and writing slightly more code. You use a big framework because one small corner of it makes something you’re doing easier. If it didn’t exist, you would reinvent that small corner. The biggest risk is that you reinvent it with more bugs or a worse interface.

                                                                        1. 7

                                                                          This is just bad semantics. You’d expect that coercing a type to its supertype preserves all properties shared with the supertype including nil. For “implementation reasons” this isn’t happening.

                                                                          1. 2

                                                                            Not so. Printing all the details…

                                                                            fmt.Printf(”%#v %#v %#v %v”, t, t2, thing, thing.P == nil)

                                                                            Provides…

                                                                            &main.T{}

                                                                            (*main.T)(nil)

                                                                            &main.Thing{P:(*main.T)(nil)}

                                                                            false

                                                                            Clearly the interface P in the third position is not nil. Rather it is implemented by a nil *T

                                                                            It’s fair to dislike the Go interface mechanism, but it is not the case that this is type / supertype.

                                                                            1. 1

                                                                              Hm, maybe I’m misunderstanding then, but if isNil is a property of all types in Go and I can silently convert a value to a substantiation of an interface then I’d expect isNil to be preserved. That silent coercion is the subtyping relationship.

                                                                              Now also clearly this doesn’t quite work from a memory perspective since coercing to an interface means we need to reference the methods somehow or another, but that’s why I think this is a convenient-but-wrong kind of behavior.

                                                                              1. 1

                                                                                Think of Go interfaces as a pair of a type tag and a value of that type. In the original code the call…

                                                                                factory(t2)

                                                                                Go implicitly creates a P interface instance as the pair (*T, nil)

                                                                                And the original predicate asks…

                                                                                Given a pair (*T, nil) and the constant nil are they == ?

                                                                                And the answer is false.

                                                                                In another comment on this page I modified the example to…

                                                                                var pnil P = (*T)(nil); fmt.Println(thing.P == pnil)

                                                                                In this case Go implicitly creates a P interface instance as the pair (*T, nil) and the variable pnil is assigned that value.

                                                                                In this example the predicate asks…

                                                                                Given a pair (*T, nil) and another pair (*T, nil) are they == ?

                                                                                In this case the answer is true.

                                                                          1. 9

                                                                            IME, the FP vs OO debate usually has a lot to do with the state of things right now. Maybe the author is correct in that you could do a lot of these things in either. But who knows because it’s not done. Of the three languages I know of that “successfully” combine FP and OO:

                                                                            In Ocaml, objects are almost never used. I do see objects show up now and then but very infrequently.

                                                                            In Scala, I do not have enough experience to comment however people I trust struggle quite a bit with it.

                                                                            In F#, it seems like it tries to be as much like Ocaml as it can get and still survive in .Net. But I don’t have enough experience in it to draw strong conclusion.

                                                                            I think the author also misses the mark as he seems to be comparing languages rather than paradigms. In my experience, if one looks at the successful examples of OO languages doing functional things and people liking it, it’s usually because they are explicitly getting away from the OOP tar pit. The problem with objects is they are too powerful. Each object is its own universe and one rarely wants that. Looking at Java incorporating FP concepts and calling it a win for OOP is missing the point, I think. It’s rather an admittance that usually one probably doesn’t want objects and if you hold your nose and squint really hard you can sort of get Java to do that by convention.</flame inducing statements>

                                                                            Clearly I fall on the FP-side of things.

                                                                            1. 8

                                                                              Of the three languages I know of that “successfully” combine FP and OO:

                                                                              Arguably the reason this is such a big debate is that “OO” refers to a big pile of things, some of which are problematic (inheritance) and others which are no-brainers (hiding implementation details) and some which are somewhere in the middle (polymorphism, message passing).

                                                                              The other reason is that like speed, “FP” is a property of programs, not of languages. A language can only be “more FP” or “less FP” in that it contains the means for making FP programs convenient or awkward.

                                                                              1. 3

                                                                                FP is also a big pile of things, or, rather, several big piles of things that vary depending on who you ask that most people pretend are together just one pile of things.

                                                                                FP isn’t a technical definition. There are a couple good technical definitions for things like “pure” and “value semantics” and sometimes people use these in place of “FP”. There are also cultural movements and quacks-like-a-duck style arguments to be had. They all fall down the same way defining OO does.

                                                                                The author has it dead on: interesting questions only exist beneath the surface here. Beyond that, saying FP or OO is nothing more than stating your nationality.

                                                                              2. 5

                                                                                Rust seems to successfully combine them. If you discount the immutability requirement so does Go. The boundaries for the two paradigms are definitely blurring. C# has lambdas and closures and has had for a long time. Java recently got them. I don’t think the current division will last long.

                                                                                1. 13

                                                                                  Rust is not OO. Rust’s style can be thought of as closer to C-style structured programming with Haskell’s type system.

                                                                                  1. 2

                                                                                    In terms of programming style, Rust code reminds me a lot more of C++ style programming than of C-style structured programming. It’s true that it lacks several of the classic OO features C++ supports, especially the runtime oriented ones like dynamic dispatch on subtypes. But inheritance is deemphasized in modern C++ anyway, and the other features that make C++ style programming considerably different than C-style programming largely are present in Rust: RAII, monomorphized generics, exceptions, destructors, etc.

                                                                                    I don’t think that necessarily makes it OO (arguably certain styles of modern C++ are barely OO either), but it really doesn’t look much like C-style structured programming.

                                                                                    1. 3

                                                                                      C++ is explicitly a multi-paradigm language. One which can be best characterized as “stealing every popular feature from other popular languages since 1983.” You’re right that modern C++ does not match the OO paradigm, but one can still certainly write C++ in an OO style (heck, C++ started as “C with classes,” literally the addition of OO faculties to C).

                                                                                      1. 1

                                                                                        C++ starts with Simula. He wants to add its style to C in form of C with classes. Then, adds stuff from ALGOL68, Ada, CLU, and ML. So, yeah, a pile of features from other languages all thrown into one. I’ll add they’re also from languages that were sometimes really different from one another. It had to be done with close compatibility to C w/ low-cost abstractions. This situation leads to a language that’s ridiculously difficult to compile or even learn easily.

                                                                                        So, yeah, multi-paradigm for sure. Not cleanly like LISP either. ;)

                                                                                    2. 1

                                                                                      Rust is absolutely OO. It has objects with methods. It also has a type system inspired by Haskell. The two are not mutually exclusive. Your statement just makes the OP’s point for him. The lines here are blurring and it’s probably a good thing.

                                                                                      1. 8

                                                                                        Rust does not have objects, nor does it have methods in the way that term is understood in OO.

                                                                                        I can and will get into a technical explanation of why characterizing Rust as OO is wrong, but I think the philosophical perspective is more important:

                                                                                        Programming paradigms are mental models for thinking about programs. When a programming language is said to fit a particular paradigm, it means that the language provides facilities which are conducive to the writing of code which flows neatly from the model that paradigm encapsulates. When I say that a language is functional, I do not mean that it has the particular qualia of functional programming (as any attempt to list out a set of necessary and sufficient conditions for this categorization based on those qualia is doomed to fail), I mean that when I mentally model my program in the manner implies by the paradigm, I can easily convert that model into real code in the given language.

                                                                                        From this perspective, Rust is not OO, as an OO mental model is one where the program is understood as a collection of objects in a hierarchy communicating between each other to facilitate some behavior. This is not the mental model for which Rust is designed. You may be able to translate from such a mental model into a Rust program, but as anyone who has tried will tell you, it is real translation work. Strike one against the notion that Rust is OO.

                                                                                        Now, to some technical points. The difficulty here is that there is more than one characterization of the necessary and sufficient conditions for OO categorization. Rather than wade into that argument, I will take a look at a few of the features commonly agreed to be necessary, if not sufficient, and look at whether Rust meets them.

                                                                                        First, one distinction commonly found between the OO paradigm and the functional paradigm is how polymorphism is provided for. In the world of functional programming, parametric polymorphism is more often found. In OO, it is more often subtype polymorphism. Rust’s focus is clearly on parametric polymorphism, facilitated by the constraint mechanism of traits. Same as Haskell with its typeclasses. Now, Rust does have a limited amount of subtype polymorphism, but is found solely in polymorphism over lifetimes, to allow for the use of something which lives longer than a required constraint. A more general mechanism for subtype polymorphism that one would generally expect in an OO language (usually provided for via inheritance) does not exist in Rust. Strike two against the notion that Rust is OO.

                                                                                        Second, let us return to the notion of methods. Rust does have functions which can be called using the familiar “method notation” of data.function_name() with the data being implicitly passed as a first parameter. But using this syntax does not mean that Rust’s facility here is akin to methods in OO languages. In fact, Rust lacks a key feature here, which distinguishes it. In other OO languages, you have something called “open recursion.” Bob Nystrom has a nice explanation of this term, which I encourage you to read. What this means is that all the methods for a given objects are visible to each other, and that methods defined on a base objects have access to the real receiver of the method call (which may be some subtype of the base object). Rust does not have this, as Rust does not have inheritance, and the extension of this to “open recursion over subtype lifetimes” is essentially meaningless, as the lifetime system doesn’t parameterize functions over concrete lifetimes anyway, but on abstract lifetime constraints (but I digress). Anyway, strike three against the notion that Rust is OO.

                                                                                        I could go on, but I will simply add to this that questions of how to do OO programming in Rust pop up all the time, and the consistent answer is that it can’t be done and you need to figure out another mental model for working with Rust. Either this is true, and Rust is not OO, or this is wrong, and somehow a bunch of people have all missed how Rust is totally OO and workable with that paradigm. Occam’s Razor certainly suggests the former.

                                                                                        1. 4

                                                                                          Both of your technical reasons are just “Rust doesn’t have subclassing/inheritance”. This is often associated with OO, but I disagree that it’s a necessary part of OO. Based on a cursory peek, it seems like many definitions online agree with me on this.

                                                                                          1. 4

                                                                                            Okay, fine. Let’s try some other ones.

                                                                                            • Dynamic dispatch: Rust has this via trait objects, but it’s very different from how dynamic dispatch is done in OO languages. Trait objects undergo type erasure, meaning the full capabilities of the original type are not accessible, and instead only the capabilities provided for in the associated trait may be used.
                                                                                            • Encapsulation: Rust has a privacy mechanism, but it’s at module boundaries, not data boundaries.
                                                                                            • Objects and classes: Rust does not have classes, nor does it have prototypes. These are the two classical systems for modeling the specification of objects.

                                                                                            I could go on. I gave those two examples because they were the first that came to me, not because they are the only ones.

                                                                                            1. 1

                                                                                              I think that perhaps the core of this disagreement is that you have a definition of OO that does not fit with the wider more common definition of OO. Which again is sort of the point of the OP’s article. Being precise about what particular feature/property someone means when they say something is OO or Functional is a far more useful conversation than the general terms OO and Functional can communicate.

                                                                                              For instance, I find Rust’s properties of Immutability by default, Objects with Methods defined on them, Dynamic Dispatch, ADT or Enum datatypes, and Pattern Matching to be quite useful. That is both a more detailed and more useful discussion than whether or not Rust is a Functional or Object Oriented language.

                                                                                              1. 3

                                                                                                The items I listed are literally the heading items from the Wikipedia page for object-oriented programming.

                                                                                                1. 2

                                                                                                  Instead of Wikipedia, I’d say describe which items on your list were present in Simula, Smalltalk, and C++ since they’re the languages that brought us OOP style either inventing it or popularizing practical use of it.

                                                                                                  1. 2

                                                                                                    I’m not going to play the game of which definition of OO is the perfectly right one, trying the proper incantation of features to analyze to show that Rust isn’t OO. I have made my case in terms of the most common features which available references list as representative of the OO paradigm. If you are unpersuaded, fine. I will not follow moving goalposts.

                                                                                                    1. 1

                                                                                                      I didn’t see a game, never mentioned Rust, and wasn’t trying to be persuaded. I just noted that Simula invented OOP with Smalltalk and C++ popularizing it from different angles. It seems what defines OOP would be in those, esp Simula or Smalltalk. Anyone redefining it from there would be likely moving goal posts since those two distilled its essence in powerful form a long time ago.

                                                                                                      And then much conversation followed by all kinds of people who didn’t invent anything like that but copied parts of their concepts. Much debate focuses on those successors when the pioneers might clear some things up.

                                                                                                      1. 3

                                                                                                        You’re commenting in a comment chain about whether Rust is an OO language or not, where I have taken the stance that it isn’t. Each post (yours is the third) asked for an argument based on a different standard. I was willing to do that twice. Not a third time.

                                                                                                        Also, I hardly think that judging OO today with anything other than Simula, Smalltalk, or C++ is moving goalposts. If it is, the goalposts moved so slowly that we’ve all had time to adjust. I have never in my life met a Simula or Smalltalk programmer. The ability to even find people who understand OOP as it was originally done in these languages is limited. It’s simply not a reasonable standard when discussing what OOP is commonly understood to be today.

                                                                                                        1. 1

                                                                                                          It seems what defines OOP would be in those, esp Simula or Smalltalk

                                                                                                          Maybe in some discussion, but the OOP 99% of developers are using today is really disjoint, from Smalltalk at least. Java, C#, and C++ share very little in common with it. I can’t speak for Simula.

                                                                                                          1. 1

                                                                                                            Good point. Far as C++ model, it was inspired by designer’s experience using Simula. He loved it.

                                                                                            2. 1

                                                                                              What you’ve said about “open recursion” is really interesting. Do you have any links to really useful applications of open recursion in terms of object hierarchies?

                                                                                              1. 3

                                                                                                Thanks! I’m not quite sure what you mean by that. Are you asking for examples of where open recursion is required / used in OOP?

                                                                                                Well, first let’s clarify what’s meant by “recursion” here. In this context, “recursion” means that the methods of a class are visible to each other; they can see each other’s names. So, this works:

                                                                                                #!/usr/bin/env ruby
                                                                                                
                                                                                                class Counter
                                                                                                    def initialize
                                                                                                        @value = 0
                                                                                                    end
                                                                                                
                                                                                                    def increment
                                                                                                        set(get + 1)
                                                                                                    end
                                                                                                
                                                                                                    def set(value)
                                                                                                        @value = value
                                                                                                    end
                                                                                                
                                                                                                    def get
                                                                                                        @value
                                                                                                    end
                                                                                                end
                                                                                                

                                                                                                In this example, increment can see the set and get functions without them having to be in a particular order, and without any special care or effort on the part of the programmer. This is “recursion” as it is meant in the term “open recursion.”

                                                                                                So what about the “open” part? Here “open” means “late-bound,” or put another way “including both functions already defined, and functions which be defined in subclasses introduced later.” Here’s what that looks like:

                                                                                                #!/usr/bin/env ruby
                                                                                                
                                                                                                class Counter
                                                                                                    def initialize(value: 0)
                                                                                                        @value = value
                                                                                                    end
                                                                                                
                                                                                                    def increment
                                                                                                        set(get + 1)
                                                                                                    end
                                                                                                
                                                                                                    def set(value)
                                                                                                        @value = value
                                                                                                    end
                                                                                                
                                                                                                    def get
                                                                                                        @value
                                                                                                    end
                                                                                                end
                                                                                                
                                                                                                class LoggingCounter < Counter
                                                                                                    def get
                                                                                                        puts "getting #{@value}!"
                                                                                                        @value
                                                                                                    end
                                                                                                
                                                                                                    def set(value)
                                                                                                        puts "setting #{value}!"
                                                                                                        @value = value
                                                                                                    end
                                                                                                end
                                                                                                

                                                                                                In this example, LoggingCounter inherits from Counter, and redefines get and set to print what they’re doing. If you create an instance of LoggingCounter and call increment on it, you’ll see that it does indeed call the get and set defined in LoggingCounter and not the get and set defined in Counter. This is “open recursion” because the implicit “this” (or “self”) being used to determine the object on which to find the get and set methods is late-bound. Rather than being determined statically, it is determined dynamically. At runtime, the language sees that the object on which the increment method is being called has defined get and set, and it calls those get and set functions, rather than the get and set functions from the original class in which increment was defined.

                                                                                                That’s open recursion!

                                                                                                1. 2

                                                                                                  Does Rust support open recursion via traits? IIRC, in Haskell, open recursion is the one situation where the dispatch table of a typeclass cannot be compiled away, if a compiler were so inclined. Although, I believe open recursion shows up differently in Haskell than in the standard OOP example due to the lack of a type heirarchy, but I don’t have an example handy.

                                                                                                  1. 1

                                                                                                    Are you asking for examples of where open recursion is required / used in OOP?

                                                                                                    Yes, exactly. Your example in Ruby is a very clear example of how subclass methods can be called by superclass methods. It’s hard for me to imagine doing this in a language without implementation inheritance. In fact I think I now understand what the point of implementation inheritance is! For the last approximately 10 years I’ve believed it was useless. On the other hand, I basically never use implementation inheritance, which suggests to me that open recursion really isn’t that necessary is a program design tool.

                                                                                                    So are there designs where open recursion (and therefore implementation inheritance) is critically needed? I can’t think of any off the top of my head. Perhaps something to do with creating new widgets in an GUI system?

                                                                                                    1. 1

                                                                                                      I’m not sure it’s critically needed, but I’ve seen implementation inheritance commonly used in statistical machine learning libraries, at least those of a certain peak-OOP era. There are often variations of a basic method that only differ in a few respects (e.g. a different sampling method), so one implementation technique is to have these variants inherit a base implementation and then override the one or two methods that differ for that variant.

                                                                                                      Obviously there are non-OO approaches to solving that problem as well. The classic C approach would be to just have one big implementation with a bunch of configuration flags (R packages also often use this approach). An FP approach might be to write a generic version of the method parameterized by functions passed as parameters, so e.g. passing in a customized sampling function as a parameter, overriding the default, plays the same role as overriding the sampling method would in the OO version.

                                                                                        2. [Comment removed by author]

                                                                                          1. 4

                                                                                            When I wrote CL for a living (circa 2006-2014), it seemed like the community leaned more toward an OO style (in the sense of using lots of objects and mutable state–of course lambdas were also heavily used.). There was a sense that this was more pragmatic and modern, and purely-functional approaches were dated or overly precious. I’ve heard that this has changed a bit now as FP has grown more popular in general. (It’s also possible that my perceptions were colored by the codebase I worked on, which was already old in 2006 and was heavily OO.)

                                                                                            1. 3

                                                                                              It is true that OO Style is more common in CL, but I’ve never seen functional approaches being referred to as dated. Personally I think reason is that, although CL is multi-paradigm it is really easy to write in an imperative style with small functions.

                                                                                              But there are a lot of examples of FP in CL. For example a parser combinator library, smug, is written using monadic parsers. Scott McKay, of Symbolics fame, is vocal about writing CL in a functional style. He has written Fset, a functional data structure library and a generalization of map designed to replace the use of loop. He even has a small extension that move the local functions after the body which makes them add less noise to the function definition.

                                                                                              You can in SICL’s Loop implementation to see CLOS being combined with parser combinators. The parsers build the AST and generic functions emit the code.

                                                                                              The antagonistic perspective of FP vs OO is detrimental, why [learn from] not both. Recently I was reading up on parser combinators and found the monadic variant of it. Wadler’s How to turn a failurue into a list of successes did a good job explaining the advantages of using a parser monad and how monads (which I now think of as design patterns derived from math) can help the every day programmer. I wouldn’t jump through hoops when a setf does the job and I wouldn’t write tons of mix-ins when I can just compose small functions.

                                                                                              1. 3

                                                                                                I think that’s been true for most of CL’s existence… the people who wanted more of an FP style (mainly immutable data, mainly recursion for control flow, etc.) went to Scheme, while CL was a standardization of the “industrial” dialects of Lisp that didn’t have those goals. Although I believe in Lispland only code that is heavily CLOS based would really be considered “OO”, and there’s plenty of CL code that is neither in an FP style nor CLOS based.