1. 37
  1.  

  2. 5

    A runtime exception seems like a symptom of programmer error, but not the cause. Is there evidence the bug density is actually less, or the symptoms of bugs just change?

    1. 5

      It’s safe to assume they still have bugs.

      1. 3

        Of course, I’d love some real defect stats though.

        1. -1

          Well, it’s essentially a fluff article, meant to suggest that Elm’s type system has somehow made their code rock solid.

          They’re inviting static typing advocates to congratulate themselves for having the good sense to advocate static typing.

          All in all, it’s safe to ignore the article and move on.

          1. 6

            I don’t think that’s a fair characterization of the talk/summary. While i haven’t had a chance to watch the specific talk summarized here, i’ve seen Richard Feldman talk about Elm before and doesn’t claim that there are no bugs in their software. He simply states that certain classes of errors (runtime) don’t occur. He typically also lists a number of other benefits to using Elm, all of which, while anecdotal, are credible.

            Speaking from personal experience as someone who has built and maintained large & complex javascript apps and who has also built some smaller things with Elm, opting to ignore the claimed benefits and “move on” is a mistake. I see bugs almost daily that would have been prevented or mitigated by the saftey guarantees Elm offers. It’s also mistake to dismiss Elm’s only advantages as static typing. A lot of work has been put into the tooling an error messages for the language, which shows, and makes a marked improvement in the development experience vs javascript.

            On a less Elm-specific note, even if static typing only converted runtime exceptions into an equal number of logic bugs (and I don’t think that’s the case), I’d still gladly take that trade-off. In my personal experience, runtime exceptions often take more time and mental effort to diagnose than a bug in business logic. The latter’s symptops usually indicate something about the root cause, and they’re often easier to reproduce.

            1. 0

              Personally, I’ve recently become convinced that the benefits of type systems are way overstated.

              They’re somewhere between difficult and impossible to quantify too.

              1. 1

                I don’t know what your type system experience has been, but I had a similar view of strong/static typing when I had only encountered it via Java and similar languages. I still find those type systems to be of minimal benefit, so I won’t make the claim that statically typed languages are inherently better.

                What I will say is that an algebraic type system with pattern matching, no null values, immutable data, and managed side effects has been a game changer for me. Particularly when paired with the exceptionally good compiler errors in Elm, which I found to be very helpful in getting comfortable with the type system.

                If you have worked with similar type systems and found them lacking, I’d be interested to know more about your experience. It’s certainly possible there are issues I just haven’t encountered.

                I agree that any type system benefits seem very difficult to quantify. I suspect the amount of benefit varies from project to project as well.

                1. 1

                  The biggest benefit to a type system I’ve seen is that IntelliJ IDEA can refactor Java code extremely reliably. I can change something in a class and trust that the corresponding changes will be made all over the codebase.

                  That’s certainly valuable, and something I’ve missed when working with Clojure.

                  To be fair, I don’t actually have any experience with Haskell or OCaml and so on. But even so, I’ve come to the conclusion that the benefits are overstated. Obviously, I could be wrong, but I’ll take my chances :)

                  People keep bringing up the supposed benefits of “safety” and “correctness” but what do they even mean in concrete terms?

                  How does a language with a strong type system[1] help make your code more “safe” and more “correct” than one without?

                  [1] Whatever a “strong (or algebraic!) type system” might mean in practice.

                  I think it was yesterday when I saw someone tell someone else that he just hadn’t experienced the “true” power of a strong type system. But if you actually asked him what that means, I bet he wouldn’t be able to answer. He’d probably talk about “safety” and “correctness” though.

                  with pattern matching, no null values, immutable data, and managed side effects

                  I like the idea of pattern matching. It looks neat and useful. I’m not concerned about null values.

                  In fact, Clojure kind of uses null values to your benefit. There’s a lot of stuff that will just do nothing when given null, i.e. nothing. It’s elegant and intuitive.

                  I already have immutable data for free with Clojure, and I don’t know what you mean with “managed side effects”.

                  1. 1

                    To elaborate a bit on Clojure and nulls, suppose you have a variable that generally contains a list of items, but in some situation it’s null.

                    A null value signifies “nothing”, right? So what do you get when you take the first list item of nothing? -You get nothing.

                    What do you get when you look up the key “:foo” from nothing? -You get nothing, because nothing doesn’t have any keys and values. It’s just nothing.

                    And then when you have nothing, you take out nothing from it, and pass it along to something else, that thing also gets or does nothing, and nothing blows up because everything only works with something.

                    So I don’t see a problem with null values being “possible” and around. They’re often useful for working with nothing :)

                    1. 1

                      Let me start by saying that I’m a big fan of Clojure. I own a couple of back-end services at work that are written in Clojure, and there’s no language I’d rather work with on the JVM. it’s a better language than Java, hands down, IMO. There are also specific problem spaces where it really shines and I’m not sure I’d prefer a ML style language, despite my affinty for them. That said, let me try to describe some of the specific things I like about ML style languages, in comparison to Clojure, since we share that as a common refrence point. Perhaps you’ve heard some of them before, but hopefully they’ll provide some insight on where I’m coming from.

                      You are right that idiomatic Clojure, and it’s core functions do a lot right in terms of dealing with null values. Behaviors like the ones you’ve described, and tools like the some-> threading macros go far toward removing the pain of unexpected NPEs.

                      From my perspective, Clojure’s treatment of null values is a pretty decent approximation of the Maybe type of Elm/Haskell, which is how those languages represent “nothing.’ Without getting into the weeds on type system details, I’ll say that Maybe allows for the behaviors you’ve described, with the additional protection that the compiler will prevent you from making a mistake in your functions that handle null values, such that you can be confident you get the nice behaviors of Clojure’s core functions in all of your functions as well, even if they were written by the intern who’s just getting a handle on this stuff.

                      Confident refactoring is part of the value I get out of a statically typed language. In Clojure or Javascript, if I decide to change the structure of some data, I have to remember all of the places where that data is used and update them accordingly. This can be easy or quite difficult depending on the complexity of the application, and I often have a little bit of a lingering feeling of uncertainty after doing so in a larger project. When I’m working in Elm, the compiler will lead me around the project pointing out all of the bits that need to be updated. This provides improved reliability if the code is big or unfamiliar enough that I’d forget, but it also provides a lot of value in that I’m no longer worried about what I might have missed. That’s pretty valuable to me in terms of development experience.

                      When I say managed side effects, I’m talking about a system where any side-effects (database writes, http requests, dom updates) are explicit in the type system. Part of the value there is that it’s easy to see which functions do stuff because it’s part of their type signature. That makes it really hard to accidentally do (or not do) things, which is a class of bug I see often. Aditionally, when every side effect is represented as a value returned by a function, it means you can test your side effects without having to use mocks. That’s a really nice experience. I’d much rather give a function some input and verify something about it’s output than spend a lot of time mocking out function calls.

                      A nice thing about algebraic types is that you can model some of the invariants of your logic in the type system. Probably the most straightforward example of that is a list that should never be empty. There are often contexts where some data is well represented as a list, but the list being empty is a degenerate state that has to be avoided. With ML style type systems, it’s pretty straightforward (and often readily available) to create a List-like type that can’t be constructed without providing at least one value, which passes off maintaining the guarantee of non-emptiness from unit tests and developer discipline to a robotic helper (the compiler)

                      To sum up, Clojure is really good, no arguments there. ML style type systems provide a nice robot helper in the form of the compiler that can reduce the occurence of some types of errors. This can also make for a nicer developer experience, particularly in Elm, where a lot of thought has been put into compiler messages. I’d never recommend dropping Clojure for Haskell or another ML language at least not without some understanding of the problem space, but I would suggest not dismissing the value of ML style type sytems out of hand. Maybe if you need to write a UI sometime, consider giving both Clojurescript and Elm a try, if you have the time. Elm will probably seem weird at first, but it’s a lot better introduction to ML than something like Haskell.

                      Oh, and if you want some pattern matching in your Clojure, I’ve found core.match to be pretty useful.

                      1. 1

                        Thanks for the thorough response!

                        with the additional protection that the compiler will prevent you from making a mistake in your functions that handle null values, such that you can be confident you get the nice behaviors of Clojure’s core functions in all of your functions as well, even if they were written by the intern who’s just getting a handle on this stuff.

                        A lot of that can also be achieved with conventions. For example, I tend to wrap things in a (when value <..do stuff..>)

                        When I’m working in Elm, the compiler will lead me around the project pointing out all of the bits that need to be updated. This provides improved reliability if the code is big or unfamiliar enough that I’d forget, but it also provides a lot of value in that I’m no longer worried about what I might have missed.

                        This is a mild benefit :) Mild because at least in principle, you’ll achieve the same result with good tests.

                        In larger projects, you’ll basically have to have the kind of tests that would give you that benefit anyway.

                        The compiler will just catch that kind of small bugs a bit sooner than the tests would, because the code needs to be re-compiled before it’s tested :)

                        I’m talking about a system where any side-effects (database writes, http requests, dom updates) are explicit in the type system. Part of the value there is that it’s easy to see which functions do stuff because it’s part of their type signature. That makes it really hard to accidentally do (or not do) things, which is a class of bug I see often. Aditionally, when every side effect is represented as a value returned by a function, it means you can test your side effects without having to use mocks

                        Functions generally “do stuff” anyway? And you’ll be making your code “functional” in nature anyway, right? But I just don’t understand what that stuff means.

                        I’m not planning on writing any mocks anyway, because I think they’re (much?) more trouble than they’re worth. What’s the benefit of testing that your code works against not-real-app-code? :)

                        I’d much rather give a function some input and verify something about it’s output than spend a lot of time mocking out function calls.

                        Yeah, we’re in agreement there :) But that doesn’t require any magic type sauce, does it?

                        A nice thing about algebraic types is that you can model some of the invariants of your logic in the type system. Probably the most straightforward example of that is a list that should never be empty. There are often contexts where some data is well represented as a list, but the list being empty is a degenerate state that has to be avoided.

                        That sounds highly unusual. Most apps are basically just CRUD anyway, right?

                        I’d never recommend dropping Clojure for Haskell or another ML language at least not without some understanding of the problem space, but I would suggest not dismissing the value of ML style type sytems out of hand.

                        Well, you’ve brought up some good points.

                        But on the other hand, I bet you’d agree that all that type magic doesn’t come without a cost.

                        How would you describe the costs of a Haskell/Elm kind of type system? Obviously there’s more code to write, and presumably you’ll need to spend more time thinking about how stuff fits together.

                        You’ll need to do thinking with dynamic typing too, but you’ll be free to explore and iterate faster, right?

                        But what else?

                        1. 1

                          Functions generally “do stuff” anyway?

                          Some functions are ‘pure’ (you could run them multiple times, or cache their results for performance, etc without changing behavior) and others have ‘effects’ (eg disk writes, network calls, modification of global variables).

                          In most languages when editing code the following can happen:

                          • You no longer need something you were getting from a function call.
                          • So you remove it.
                          • But it was populating a cache / updating a counter / otherwise doing something as a side-effect.
                          • You have to be careful to avoid a bug.

                          When your type system tracks ‘effects’, you can statically see whether it’s safe to remove the code or not.

                          1. 1

                            Right. I wasn’t thinking of “doing stuff” involving any side-effects.

                          2. 1

                            A lot of that can also be achieved with conventions. For example, I tend to wrap things in a (when value <..do stuff..>) This is a mild benefit :) Mild because at least in principle, you’ll achieve the same result with good tests.

                            Certainly for most of the advantages provided by a nice type system, you can always say “tests/conventions will cover that”. The issue I’ve found with that is that tests and conventions rely on the discipline of the development team to maintain at that level. As projects grow in complexity and live longer, and the team gets larger, it becomes more and more likely that that discipline will slip somewhere, even on the best teams. Nobody’s perfect, and even the best engineers have bad days. I like to think I’m pretty good at that stuff in general, but when the baby is up all night because baby reasons, work still has to get done, but I’m not going to be at my best. Some of our junior developers are really sharp and have a solid future ahead of them, but they don’t have the experience yet to catch everything. Code review helps, but reviewers have off days too.

                            It’s not so much that any individual benefit provided by a good type system and helpful compiler is an incredible revelation. It’s the aggregate effect of having a tireless, unrelenting robot helper that always has your back that makes the difference. It means getting back a lot of the cognitive load the team is expending to maintain testing and convention discipline. Again, for an individaul developer, maybe that doesn’t seem like a lot, but in aggregage, it can be a big win.

                            How would you describe the costs of a Haskell/Elm kind of type system?

                            It’s a bit difficult to describe trade-offs at that high of a level, in much the same way that it’s difficult to answer the question “How would you describe the trade-offs of a Clojure/Javascript type system” Clojure and Javascript both have their own trade-offs, but only a few of them map to the type systems directly. I’ll give it a shot, but I might need to dig into specific languages for it to be meaningful.

                            I think you hit the primary trade-offs. If you just need to get something prototyped in a hurry, or you know the project is small and will stay small, you can probably deliver faster with a dynamic type system. I’d make the case that that speed advantage disappears (and possibly reverses) as a project grows in scope and complexity. At some point there’s too much for a person to keep in their head (or it’s been too long) and having the compiler’s help gains some time back.

                            I wouldn’t say that you necessarily have to think more to get the same result in an ML style type system, but you certainly have to think differently. Solving problems becomes more about figuring out how to best represent your problem in the type system. That approach takes some learning, but in my experience it wasn’t any more difficult than getting up to speed on Clojure (specifically in Elm, more on that later).

                            Another factor to consider is that the compiler for an ML style type system will force you to handle cases that you can skip in other languages. As an example, if you’re pulling from the head of a list, you’ll have to handle the case where the list was empty, even if you know it shouldn’t be at that point in the code. This can be annoying, but ultimately leads to better code if you lean into it and figure out what really needs to happen in that case. It typically means either making sure you’re providing nice error messages in the case where things go wrong, or representing your data in a way that that problem is impossible. Richard Feldman gave a good talk on this topic.

                            Certainly my Elm code is not as concise as Clojure, but Clojure is a very conscise language. In some places this is a win for Clojure, in others the explicitness of the Elm code makes things clearer, I think.

                            One thing I’m not sure an ML language would be better for is if you have code that does a lot of data transformation without needing to be particularly smart about the meaning of the data. That’s something that Clojure excels at, and is a big part of why I chose Clojure for the serivices I use it in.

                            A few more comments contrasting Elm and Haskell and conciseness vs learning curve, and I think I’ll have said most of what I have to say on the subject.

                            Haskell focuses heavily on very high-level abstractions, which are powerful, but can be difficult to tie back to specific use cases. Those abstractions help keep code compact, but the focus on them comes at a learning curve cost. When I first started trying to learn Haskell, I found myself struggling to figure out how to apply concepts like “Monad” and “Applicative Functor” to my specific use cases. That was the wrong approach, but the way that Haskell tends to be presented often leads people into that trap. Evan Czaplicki’s Lets be Mainstream talk contains a good critique of this.

                            On the other hand, Elm doesn’t have a facility for that sort of abstraction, instead relying on convention. This comes at a cost in terms of less concise code, but it makes the langague much easier to get a handle on, IMO. If you ever make the decision to experiment with an ML style language and have the option, I’d recommend it over Haskell as a starting point.

                            Hope that’s not too much of a wall of text to be useful. Ultimately what I wan’t to suggest is just to be careful about dismissing the benefits of different languages until you’ve at least tried them out a bit. At the very least, working with a ML family language might provide some insights and ideas you can bring back to tools you prefer better. Same with Erlang, or anything else. Well… maybe not PHP.

                            1. 2

                              Hey thank you for another exhaustive and well thought-out post :)

                              It’s not so much that any individual benefit provided by a good type system and helpful compiler is an incredible revelation. It’s the aggregate effect of having a tireless, unrelenting robot helper that always has your back that makes the difference.

                              Sure, but on the other hand, the same robot is also a tireless hinderer when you’re just trying to get something done :P It never stops either!

                              The way people generally speak about “a good type system” - I’m never quite sure what to call it - sounds more like it’s something you’ll benefit clearly and greatly from.

                              That’s what I meant with the benefits being “way overstated”.

                              You’ve made a good case for there being benefits to a good type system, but even your assessment of them is very measured :).

                              I guess that’s what happens when you’re being objective about it!

                              I don’t really have a response to everything you’ve said there. But I found this interesting:

                              I found myself struggling to figure out how to apply concepts like “Monad” and “Applicative Functor” to my specific use cases. That was the wrong approach, but the way that Haskell tends to be presented often leads people into that trap.

                              Is there a way to briefly summarize “the right approach to Haskell”? :)

                              1. 3

                                Is there a way to briefly summarize “the right approach to Haskell”? :)

                                More or less: writing your program is your top priority, learn to use type system features incrementally and at your own pace.

                                My first haskell programs were basically python programs: filled with IORefs, and what most haskellers would call un-elegant. It’s super easy to get started that way!

                                The biggest issue people have with learning Haskell (and for that matter anything new), is trying to run before they can walk. This leads to frustration, countless monad blog posts, and a large number of programmers who try to rationalize their avoidance of Haskell: hard to reason about, etc.

                                1. 2

                                  Sure, but on the other hand, the same robot is also a tireless hinderer when you’re just trying to get something done :P It never stops either!

                                  This is a subjective point, I think. I’ll say that for me personally, it rarely feels like a hinderance. Most of the time when I fix a complier error, I find I’m thinking “Ooh, that would have been a bug” or “That could easily become a bug someday.”

                                  How it seems to others of course will depend on the nature of the project at hand and the prorities in play.

                                  You’ve made a good case for there being benefits to a good type system, but even your assessment of them is very measured :).

                                  I try to be measured in my assesment of most things. I’m a “It’s all tools in the toolbox” kind of developer. Different languages and frameworks are good for different things. Different people work differently. Organizations have different cultures and constraints. I’m never going to tell anybody I’ve found the one true path to code enlightenment, or that what works for them is invalid because it’s not what works for me.

                                  That said, I’m exited about Elm as a tool for building UI applications. It’s resolving a lot of issues I’ve been struggling for years with, some of which I’ve only recently begun to recognize as problems. I’m sure my co-workers are sick of hearing me ramble on about it.

                                  Is there a way to briefly summarize “the right approach to Haskell”? :)

                                  I don’t have solid advice to give here, but ec’s comment in this thread rings true. My expeirence with Haskell was that I got comfortable with the basics, started getting a grip on the type system, and then I got lost in the weeds of high-level abstraction and didn’t make much progress beyond that. After working with Elm for a while, I can see better where I went wrong, and I may revisit Haskell or another server-side ML family language in the near future as a result.

                              2. 1

                                This is a mild benefit :) Mild because at least in principle, you’ll achieve the same result with good tests.

                                In larger projects, you’ll basically have to have the kind of tests that would give you that benefit anyway.

                                The compiler will just catch that kind of small bugs a bit sooner than the tests would, because the code needs to be re-compiled before it’s tested :)

                                IMO the best arguments for the power of type systems don’t come from Haskell or Elm but Python and Javascript. That’s because the latter two have gradual typing, which means they’re dynamically-typed languages with a completely optional typechecker. For python, it’s mypy and function annotations. For Javascript, it’s the Typescript language. Since both are relatively recent, there’s lots of code out there that is completely untyped that can be converted to typechecked code without a huge investment of time or money. So we can see what happens when well-designed, well-tested code adds types.

                                Results are still tentative, of course, but there’s at least some signs that it’s pretty effective at finding bugs. Even when you already have good tests.

                                1. 1

                                  If you like “gradual typing”, I’d recommend trying Clojure.

                                  Though with Clojure, type annotations are generally added to make your code run faster, not for correctness.

                                  Still, Clojure is a beautifully designed and practical language. It’s very well suited for getting stuff done, but does have a bit of a learning curve.

                                  Having used Java would help you come to terms with Leiningen though :)