Hacker News new | ask | show | jobs
by _t0du 2223 days 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. 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.
4 comments

Not 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.

> 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!"

Exactly
Analogies 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.

> 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.

> 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.

> 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.

Having 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. :)
> 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.

If 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.

My 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.
Trust 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.

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.

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.

> 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.

> 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.

type 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.