6 ms·
When people refer to "typing", it is almost certainly reasonable to assume they are referring to static typing, as implemented by most languages. I don't think
by james-mcelwain 6y ago
When people refer to "typing", it is almost certainly reasonable to assume they are referring to static typing, as implemented by most languages. I don't think this should be confusing.
Specs/contracts are cool but ultimately don't afford the same kind of descriptive and expressive power that a static type system does.
> static types in most languages[2] don't reduce this burden much: there isn't an alternative to thorough testing.
However, there definitely is a burden about how much testing you have to write. I generally don't want to have to test every branch of my program to make sure a string doesn't slip through where an int should be or that variables are initialized and not null, etc.
- alexbanks 6y agoI see that language parroted all the time - "With thorough enough testing a dynamic language shouldn't be a problem", and I have never understood it. Arguing to build what is essentially a build-time type checker in the form of automated tests seems twice as cumbersome for half the benefit. Instead of building tests that check each branch of a program's types, why not use a language that forbids dynamic typing? You should still have tests, but IMO tests that are just checking that a string is a string are A.) Time consuming, and B.) largely useless beyond type validation.
- kerkeslager 6y ago> You should still have tests, but IMO tests that are just checking that a string is a string are The correct completion to this sentence is "irrelevant.", because that's not what anyone is proposing. The fact is, behavioral tests catch a lot of type errors even without intending to, and more to the point, if you test all the behavior you care about, then you don't care if there are type errors, because they only occur in situations where you don't care.
- _t0du 6y agoMy personal real life experience with Ruby developers disagrees with you, but I accept that I could just have experienced a set of developers that weren't very good at testing.
- peferron 6y agoTrust your experience. It's impossible to defeat the claim that "with good tests you don't need static type checking", since every counter-example can be dismissed by arguing that better developers would have covered that test case. I can claim just the same that "with correct code you don't need tests", and dismiss every counter-example by arguing that better developers wouldn't have made that mistake. Obviously we know that developers sometimes make mistakes, and sometimes these mistakes are in the tests themselves. So trust your experience regarding how this all works out.
- alexbanks 6y agoYeah. You formulated my opinion much more elegantly than I could've - in my experience it's always the "Well if the tests didn't catch this, we just aren't testing enough." Which in my experience is a losing strategy, you'll never test "enough" in languages like Ruby. In my experience this idea always brings a strawman, "Well in statically typed languages you still have to test", which is obviously true. But the type of tests and the content of the tests is very different.
- kerkeslager 6y ago> Yeah. You formulated my opinion much more elegantly than I could've - in my experience it's always the "Well if the tests didn't catch this, we just aren't testing enough." Which in my experience is a losing strategy, you'll never test "enough" in languages like Ruby. Maybe someone is saying that, but I didn't say that. My question isn't whether you can test enough to catch all bugs. My question is whether time spent wrangling types gets you more value than time spent writing tests. > But the type of tests and the content of the tests is very different. Are they? How so? Again, unit tests of the form `assert isinstance(foo, type)` are an antipattern--that's not what I'm proposing.
- alexbanks 6y ago> My question isn't whether you can test enough to catch all bugs. My question is whether time spent wrangling types gets you more value than time spent writing tests. Yes, by a tremendous amount, in my experience. > Are they? How so? Tests are only as good as the person writing them. I could see a model working where the person that wrote the code isn't the person that writes the test, but that's definitely not how most development orgs work. If a dev is good enough/capable of writing comprehensive enough tests to accurately test the correctness of their code, that's great, but almost none are (I say almost because I actually mean "actually none" but am leaving room for my own error). If you're an average dev, you'll write average tests (neither of these are insults), but that means you still won't catch everything (by a lot). I think another comment of yours on my posts actually summarizes the core disconnect between your thinking and mine. > But in the vast majority of modern software, it mostly just matters that you catch and fix bugs quickly--whether you catch those bugs at compile time or runtime is usually not as critical. I couldn't possibly disagree more with this statement.
- jfim 6y agoHaving static type checking avoids errors caused by typos, missing match branches, and other brain fart-style mistakes. For example, in Elixir: f = fn {:ok, message} -> "It worked #{message}" {:eror, message} -> "There was an error #{message}" end Running this iex(2)> f.({:ok, "yay"}) "It worked yay" iex(3)> f.({:error, "oh no"}) ** (FunctionClauseError) no function clause matching in :erl_eval."-inside-an-interpreted-fun-"/1 This kind of error isn't caught if all of the testing doesn't cover the error paths. Contrast with Scala, where using Either (which can be Left or Right): def f(x: Either[String, String]) = x match { case Right(x) => s"It worked $x" case Left(x) => s"There was an error $x" } If for example one forgets a branch: scala> def f(x: Either[String, String]) = x match { | case Right(x) => s"It worked $x" | } ^ warning: match may not be exhaustive. It would fail on the following input: Left(_) Or to follow the Elixir tuple pattern more closely: scala> sealed trait Status | case object Ok extends Status | case object Error extends Status trait Status object Ok object Error scala> def f2(x: (Status, String)) = x match { | case (Ok, msg: String) => s"It worked $msg" | case (Error, msg: String) => s"There was an error $msg" | } def f2(x: (Status, String)): String scala> def f2(x: (Status, String)) = x match { | case (Ok, msg: String) => s"It worked $msg" | } ^ warning: match may not be exhaustive. It would fail on the following input: (Error, _) def f2(x: (Status, String)): String The typo also gives an obvious type error: scala> def f2(x: (Status, String)) = x match { | case (Ok, msg: String) => s"It worked $msg" | case (Eror, msg: String) => s"There was an error $msg" | } case (Eror, msg: String) => s"There was an error $msg" ^ On line 3: error: not found: value Eror Caveat: still learning Elixir and my Scala is rusty, so there might be better ways of doing the above. :)
- pavel_lishin 6y ago> if you test all the behavior you care about, then you don't care if there are type errors, because they only occur in situations where you don't care. If I test all the situations I care about, the one situation I thought I didn't care about is going to fuck me in production.
- kerkeslager 6y agoIf you test all the situations you care about and statically type check, the one situation you thought you didn't care about and that wasn't caught by the type checker is going to fuck you in production. It's not useful to talk about a binary "bugs versus no bugs", because "no bugs" isn't plausible in most codebases. It's also not useful to talk about "more bugs versus fewer bugs" because that's only part of the picture: the other parts of the picture are how much development effort was necessary to achieve the level of bugs you have, and whether the number of bugs you have, and when you have them, is acceptable. If it's a life or death application where any bugs at runtime are unacceptable, then of course we want static types, but static types aren't enough: I'd also want a theorem prover, fuzzer, a large number of human testers, and a bunch of other things that require way too much effort to be useful in an average software project. The vast majority of software projects, runtime bugs are acceptable as long as they don't lose data, cause downtime, or expose private information. If you catch these bugs during unit testing instead of 30 seconds earlier at compile time, that's fine. Static types might catch a few more bugs, but it is very much not in evidence that the level of effort involved is lower than equivalent unit testing in situations where reliability requirements are typical.
- deleted 6y ago[deleted]
- bryanrasmussen 6y agotype errors are generally the lowest common denominator error, thus tests that catch higher level errors will also catch whatever typing error is related to the actual error you are testing for. Not to say I haven't had benefits from catching type errors but generally not as great as those I have from having an automated GUI test running.
- smoe 6y agoNot GP, but I have done plenty of TDD development in dynamic languages and not once have I written a test that checks whether a string is a string. You get that implicitly because others test will fail if the types don't match. While I personally prefer certain statically typed languages over dynamic ones for new projects, In practice, for small to medium sized projects, runtime type errors is in my opinion much less of an issue as some people make it out to be. Except for null exceptions they rarely make it into production and if, are easy to track down and fix. Interestingly, a lot of the people I have seen constantly running into type problems in say Python, are the ones coming from statically typed languages that keep insisting doing thing the way they are used to instead of embracing the duck.
- pavel_lishin 6y ago> I see that language parroted all the time - "With thorough enough testing a dynamic language shouldn't be a problem", and I have never understood it. "If you drive carefully enough, a car without seatbelts shouldn't be a problem!"
- alexbanks 6y agoExactly
- kerkeslager 6y agoAnalogies are a great way to explain things, but not so much a great way to prove things. If you're writing software that's life and death critical like wearing a seatbelt, you should absolutely be using a strong, statically typed language, because catching errors at runtime is completely unacceptable. But incidentally, none of the proponents of static types on this thread have talked about any languages that I would actually use for this situation. Java or C-family languages certainly aren't strongly typed enough. Type systems aren't a magic bullet. But in the vast majority of modern software, it mostly just matters that you catch and fix bugs quickly--whether you catch those bugs at compile time or runtime is usually not as critical.
- pavel_lishin 6y ago> Analogies are a great way to explain things, but not so much a great way to prove things. What are we proponents of static types being asked to prove? > But in the vast majority of modern software, it mostly just matters that you catch and fix bugs quickly--whether you catch those bugs at compile time or runtime is usually not as critical. I would argue that catching bugs at compile time, before you ship them, is vastly preferable to catching them at run time.
- kerkeslager 6y ago> What are we proponents of static types being asked to prove? Your arguments for why you think static types are better. > I would argue that catching bugs at compile time, before you ship them, is vastly preferable to catching them at run time. I would argue that you're only doing the benefit part of a cost-benefit analysis, which isn't very useful.
- ianamartin 6y ago>> When people refer to "typing", it is almost certainly reasonable to assume they are referring to static typing, as implemented by most languages. Really? You're going to go with "most languages implement static typing"? That seems like a bold statement.
- james-mcelwain 6y agoI'm not making a claim about all languages, I'm talking about languages that support any kind of extensible type validation, whether statically or at runtime.
- kerkeslager 6y agoIf you're willing to include dynamic types, then I really don't know what you're even talking about. Literally no language I know of doesn't do validation at runtime: assembly does type validation at runtime (try dividing by zero).
- mapcars 6y ago>Specs/contracts are cool but ultimately don't afford the same kind of descriptive and expressive power that a static type system does. What exactly dialyzer type checking lacks compared to static type systems?
- JohnBooty 6y agoHowever, there definitely is a burden about how much testing you have to write. I generally don't want to have to test every branch of my program to make sure a string doesn't slip through where an int should be or that variables are initialized and not null, etc. This has not been my experience! I've been writing Ruby full-time for about six years (including one of the largest Rails apps in the world) and I don't find this particular aspect of Ruby to be a problem whatsoever relative to statically-typed language. Ruby is famously easy to write tests for. Creating mocks in a dynamic language is such a breeze. There are plenty of problems with Ruby but an increased test suite burden is NOT one of them. "Confident Ruby" by Avdi Grimm is a great read in this vein; not about testing specifically but writing confident Ruby code in general. Part of it is simple, human-friendly code hygiene. Give your arguments descriptive names and use keyword params when possible. If you have a method: foo(user_name:, height_cm:) ...then you'd really have to be asleep at the wheel to write something like this elsewhere in your code: foo(user_name: 157, height_cm: "Smith") And so you don't need to write your code or your tests with any real extra level of paranoia. If somebody does pass a string to `height_cm:` it will return a runtime exception, just like it should. Of course, I do get type errors all the time in Ruby, but that's when I'm parsing JSON or something and that's generally not something static typing's gonna help you with anyway. Now... there ARE places where I miss strong typing in Ruby. One, I miss having extremely intelligent IDEs like you can have with Java or C#. I absolutely loved Visual Studio and Resharper in the C# world. The amount of reasoning it can do about your code and the assistance it can give you is absolutely bonkers. Two, obviously, there is a price in runtime execution speed and RAM usage to be paid for dynamic typing. I don't find raw execution speed to be much of a bottleneck in a Ruby app because Postgres/Redis/etc are doing all my heavy lifting anyway. RAM usage and app startup times with large applications is more of a real-world issue.
- phtrivier 6y agoMy counterpoints would be: First, if you're writing any kind of big code, than somewhere, in your code base, someone else is writing a code where the user name is spelled `username`. Or `user`. You don't have to be "asleep at the wheel" to not remember, or not know, which is which. So you're going to type the wrong one (not necessarily on purpose), and you'll get a runtime error. Not that bad, sure, but you'll get it. Also, at some point, you'll realize that a string is not the ideal way to represent a user name [1], and some of the functions that deal with users are going to start returning a Struct %User{first_name: ..., middle_name: ....}. Or will it be a Map %{"first_name" => ...} ? And surely you're going to track all the calls of `foo` to fix them. And all the calls of all the calls of `foo`, because, who knows ? And suddenly you're doing the mental job of a static type checker. Surely you're not "asleep at the wheel" anymore, because you're doing the work of the wheel by manipulating the gears by hand. Bottom line: I'll gladly admit that I'm too old and stupid to do that anymore. I had typechecking in the 90s. Give it back. [1]: https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/ https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-...
- kerkeslager 6y ago> When people refer to "typing", it is almost certainly reasonable to assume they are referring to static typing, as implemented by most languages. I don't think that's a reasonable assumption at all. Even if, as you assert, the average person doesn't understand that types exist in dynamically-typed languages, I don't think that means I have to conform to common misconceptions. > Specs/contracts are cool but ultimately don't afford the same kind of descriptive and expressive power that a static type system does. True, but not what I was talking about. Could you explain what descriptive and expressive power is missing here? class Square(Rectangle): def __init__(self, side_length): self.side_length = side_length @property def height(self): return self.side_length @property def width(self): return self.side_length
- james-mcelwain 6y ago> the average person doesn't understand that types exist in dynamically-typed languages Again, this is needlessly uncharitable as to what people mean when they talk about type systems, which is obviously about the capabilities of defining new types for program analysis, not that the runtime has an internal conception of types. > Could you explain what descriptive and expressive power is missing here? Sure! Looking at this, I have no idea what the size of side_length is, whether it's possible for it to be negative, or whether it's possible it to be null. Of course, unless you're writing purely square based software, most domains are more complex than this. But I still think it's pretty helpful to know the properties of the parameters being passed to build your square are!
- kerkeslager 6y ago> > the average person doesn't understand that types exist in dynamically-typed languages > Again, this is needlessly uncharitable as to what people mean when they talk about type systems, which is obviously about the capabilities of defining new types for program analysis, not that the runtime has an internal conception of types. There is nothing "internal" about the example I gave. That's valid Python code that creates a type. My definition (which happens to be the actual definition of the word) charitably assumes that people know dynamic type systems exist, so I don't think you can really accuse me of being uncharitable. If anything I'm being too charitable, as evidenced by this conversation. > Sure! Looking at this, I have no idea what the size of side_length is, whether it's possible for it to be negative, or whether it's possible it to be null. > Of course, unless you're writing purely square based software, most domains are more complex than this. But I still think it's pretty helpful to know the properties of the parameters being passed to build your square are! Do you really not know whether side_length can be negative or null? Or are you just saying that to be argumentative? If we're pretending we don't know obvious things, why not just go all the way and pretend we don't know that side_length is a number? As for not knowing the size: why would you want to have to know the size? The fact that this square will work with any numeric type is a feature, not a bug. Now, consider this (disclaimer: my C++ is rusty, and I didn't syntax check this): class Square: Rectangle { private: double sideLength; public: Square(double sideLength) { this.sideLength = sideLength; } } Let's evaluate this based on your complaints about the Python example: 1. The size of sideLength. Well, yes, this example does tell you what size it is. Which is rather annoying, since now you have to cast if you want to pass in an integer, and you might overflow your bounds. This is an annoyance, not a feature.[1] 2. We know that sideLength can't be negative because we know what squares are. The type doesn't enforce that. You could enforce that by using unsigned int, but then you can't handle decimals. And in either case, you can't use very very large numbers. I haven't worked in a static typed codebase which has an unsigned BigDecimal type, have you?[1] 3. We know that sideLength can't be null because we know what squares are. The type system technically also tells us that, but the type system telling us what we already know isn't particularly useful. [1] Haskell's numeric type can actually handle this much more cleanly than C++, as I mentioned upthread. But in typical Haskell, you'd probably just let the types be implicit in a lot of cases.