5 ms·
This confirms my own experience. I discovered that programs written in (or use) dynamic languages like Lisp are surprisingly reliable (Emacs in particular). Ty
by progman 10y ago
This confirms my own experience. I discovered that programs written in (or use) dynamic languages like Lisp are surprisingly reliable (Emacs in particular).
Type safety won't protect us from broken software. Quote: "The most common bugs caught by static typing are also the least critical sort of bug."
Source: http://www.drmaciver.com/2016/10/static-typing-will-not-save-us-from-broken-software/ http://www.drmaciver.com/2016/10/static-typing-will-not-save...
Static typing helps a lot to catch basic type errors but it is surely not the "Messiah" of code safety. In big systems the good old way of testing still seems to be the best practical way.
Haskell's claim "If it runs then it's likely correct" is a deception because no compiler can catch logical errors. This was also Ada's problem in the Ariane disaster, although Ada has probably the strongest type system beside Haskell. You need special verification tools like FramaC, and even those tools don't catch all errors. Haskell's safety is even more questionable in face of the underlying libraries which are written in C. Finally, the still unresolved Cabal hell speaks for itself. Stack works only because it is an isolated repository where the maintainers have to take a lot of attention to make sure that new code doesn't break other code.
- mpweiher 10y ago> Static typing helps a lot to catch basic type errors > but it is surely not the "Messiah" of code safety. I've been wondering why that is, and one reason I can think of is that testing verifies values. This also implicitly verifies the types of those values.
- codygman 10y agoThe type system can verify values as well, but can do so more exhaustively than you can practically test.
- dllthomas 10y ago> Haskell's claim "If it runs then it's likely correct" is a deception This is missing some context. If we generate random functions until we find one that compiles, of course it's not "likely correct". But that's not what people are doing - they are setting out to write correct code. If the types they use exclude functions that are almost correct, then it's likely when they actually hit something that compiles it will also be something that's correct. All of that said, it's true that the claim is sometimes made more strongly than it deserves to be. > because no compiler can catch logical errors. Not without my help. But I can certainly write my code such that the compiler will catch certain logical errors I'm likely to make. I have a pile of examples, but not the time to elaborate - I'll add them later.
- btam 10y ago> I have a pile of examples, but not the time to elaborate - I'll add them later. Please don't forget to share, this sounds very interesting to me as a novice.
- dllthomas 10y agoAdded as a reply to my earlier comment :)
- dllthomas 10y agoExamples. The first few are in C, the last couple in Haskell because they need it. At the most simple, consider a function that I'm calling like `compute_thing(bid, ask)`. Maybe that's in error, and it actually expects `(ask, bid)`. If it was defined to take primitives (`int` or whatnot) then that's a logical error the types don't catch. But if I wrap those primitives in a struct to get nominal typing (`typedef struct ask { int v; } ask_t`), it's caught. This is only a small improvement. In code that's well exercised, this will probably be found by tests, and certainly you should make sure you have coverage. But an obscure corner case may only be run in tests where bid == ask, and then the values won't tell you anything's wrong. A bad value also doesn't tell you where the error is, whereas this use of nominal typing points you at the exact source location. (Named parameters would also catch this particular issue, where supported and used). Slightly more complicated, give the same treatment to indexes. `foos[idx]` versus `lookup_foo(idx)` with a nominally-typed argument. In the former case, there is room for me to have confused which index was talking about which array. For tests, this is possibly worse than the above case, because it's quite common for small tests to involve few elements, which means indexes are more likely to spuriously agree. Now let's get tricky. I had a few threads (statically assigned to various roles, not a pool) managing resources they own. Latency on the order of microseconds was vitally important, so couldn't have threads waiting around on locks. I had to be careful that a function called on one thread didn't touch a resource owned by another thread. If my access functions demanded a nominally typed token I passed between functions, I could detect exactly where I called something from an unintended thread. If this token took the form of an empty struct, all this checking happened at compile time with no runtime overhead at all (the exact same bytecode was emitted if I manually stripped out the tokens), and completely eliminated one kind of concurrency bug (which are often very difficult to detect and localize with tests). This made it very easy to move functionality between threads - I could make a small change and my compiler would point out specific locations incompatible with that change. Moving to Haskell, I had a Markov text generator, where the models were represented as a map from lists of strings to maps from strings to counts (`Map [String] (Map String Int)`). Merging two models was easy (`M.unionWith (M.unionWith (+))`) but if the two models looked back a different number of tokens then the result would be broken. I could hang that value on them as data, and check it manually... or I could carry it in the type and let the compiler check it for me. In a more serious context, I was evaluating SQL I had parsed. I would produce "I have evaluated this bit of it" results that I could thread together. Adding a phantom parameter to their type, I could distinguish between "this is something that operates at the table level" and "this is something that operates at the row level" and I could not merge them without them being of the same type. I then wrote functions to convert between the two. The one to take a "table" value to a "row" value (as one might when evaluating a sub-select in an expression) was a no-op. The one to go other way (evaluating an expression in a selection list or WHERE or ORDER BY or ...) I wrote to demand that I introduce the values present in the current row. So consider: I have lists and sometimes I forget to add them to my dictionary in the right places - a logical error if ever there was one. Now my type checker will catch it.
- rdnetto 10y ago> Static typing helps a lot to catch basic type errors but it is surely not the "Messiah" of code safety. I'd argue that type safety is only one benefit of strong types - the others being better documentation[1] and a design which is clearer and easier to reason about.[2] You can get both in a dynamically typed language, but they're much less common since the compiler doesn't require them. [1] I've lost count of how many times I've looked at some function in Python or Javascript and had no idea what kind of values I was supposed to provide. [2] My go-to example of this is how in Persistent (a Haskell ORM), values which have been inserted into the database (entities) and which have not have different types, since the non-inserted values don't know what their primary key is yet. This makes it much easier to reason about where the values are coming from and what needs to be done with them. > Stack works only because it is an isolated repository where the maintainers have to take a lot of attention to make sure that new code doesn't break other code. In practice, the only breakage checked for is compilation errors. The onus is still on library authors to declare compatibility bounds in their cabal files, which is why [PVP](http://pvp.haskell.org/ http://pvp.haskell.org/) is still a thing. I'm also not sure how accurate it is to call it isolated - it's pretty much at the centre of the ecosystem at this point.
- dllthomas 10y ago> I'm also not sure how accurate it is to call it isolated It's entirely inaccurate to call it isolated - you can trivially pull in other packages from hackage.
- codygman 10y ago> Haskell's claim "If it runs then it's likely correct" is a deception because no compiler can catch logical errors. Yes it can, see dllthomas' updated examples. > Finally, the still unresolved Cabal hell speaks for itself. The major reason for cabal hell was that you couldn't install multiple versions of the same package. This has been resolved with cabal new-build and should become the default after a few releases of testing. For the curious cabal new-build accomplishes this by copying what the nix package manager does. See: http://blog.ezyang.com/2016/05/announcing-cabal-new-build-nix-style-local-builds/ http://blog.ezyang.com/2016/05/announcing-cabal-new-build-ni...
- dllthomas 10y agoOne optics issue is that dependencies occasionally cause pain in every environment. When that happens in Haskell, it gets labeled "cabal hell" even though the worst issues have been long since fixed. https://en.wikipedia.org/wiki/Dependency_hell https://en.wikipedia.org/wiki/Dependency_hell