Hacker News new | ask | show | jobs
by hesselink 3212 days ago
I feel like this is a false dichotomy: you can have a fast iteration cycle, and have statically checked guarantees. I've worked in Haskell for 7 years and had exactly this. I could load my entire app in GHCi, make changes, reload and test. Now I'm working in Java and in IntelliJ I can have something similar with hot-swap. And in the browser with typescript I have a strong type-system and can reload my app in seconds.

I agree that it's easy to build a slow, batch based build system and just tell people that's how it is. Fast iteration is important and requires effort to keep working. It might even be more important than a static type system, at least for some apps. But you can have both.

6 comments

This has not been my experience with strongly and statically typed systems. The biggest problem is the coding is not fast; especially if you make a change that requires "shaking the tree". Turnaround to me is as much about the code you write as it is the results you get from that code.

For example, I start my program thinking that I need a duck for all of my various waterfowl systems. However, four days into the coding process, I realize I need to allow for an ugly duckling to be passed around as well. Now I have four days worth of code to comb through and re-type. I could just use a refactoring tool to change all of the existing 'duck' types, but some of that code actually does need a duck, and won't work with an ugly duckling. So I have to come up with a more abstract type which can encompass both an ugly duckling and a duck, and refactor that into my program.

Eventually my code will be correct again, at least until I realize that a platypus needs to be included into my now-renamed aquatic_ecosystem as well.

The nice thing about strong and dynamically typed systems - I just start treating the incoming object as what I need it to be. No error chasing, no wading through four days worth of code. Yes, I'm more likely to have incorrect code, and it's probably going to show up while the code is running, and not before. Many times, that's a tradeoff I'm willing to accept.

My ideal system? I don't think at all about types. I just write code, and the (still fast) compiler will tell me when I'm passing something that doesn't quack to a function which expects quacks.

A lot of people who stick with expressive static type systems find the process of fixing type errors quite enjoyable. I really like the experience -- I can go really fast confidently.

GHC does let you defer type errors until runtime but I never want to...

>A lot of people who stick with expressive static type systems find the process of fixing type errors quite enjoyable. I really like the experience

So maybe liking developing in them depends on having that personality trait? (or typeclass if you prefer, pun intended).

Maybe, I don't know. The psychology of programming language preference seems like a pretty interesting topic. Enjoying programming at all seems like it might depend on some quirks; I don't know if there's a significant difference to make you inherently prefer fixing type errors over fixing unit tests or whatever.
> find the process of fixing type errors quite enjoyable

I, myself, would rather be creating new functionality than fixing a litany of type errors. In fact, I'd rather go to a meeting than change several hundred instances of 'duck' to 'waterfowl', 'avian', and 'ugly_duck' (knowing that I'll probably have to go back and change it again later).

Different strokes for different folks, I guess.

Well... but usually those are actual errors that you would need to fix anyway.
Why would generalizing a type definition make an error?
In the places you use a specific concrete type you presumably did so for a reason, and now need to think about how the type change affects it. The parts of your code that are generic should have been written to be generic, and the parts where the type can be inferred from the lower-level functions involved should have left the type to be inferred from the lower-level functions involved.
What you are saying is like the people that would "rather write new functionality than tests"... Like, yeah, that would be nice, if you were able to write 100% bug free code, but you aren't so your tests are actually important in the goal towards a working product.
Well, I'd actually rather write unit tests than shake out type trees too, so no, the two statements aren't equivalent.

Why? Unit tests also check a lot more than the types being passed around, so they are a lot more useful in the long run. There are some type systems where this is perhaps not the case, but they certainly aren't the majority. The majority is "so do I go with a float or a double" or "I have to cast this int to an uint64 for this one function".

So use a good type system rather than a bad one. I mean if your point is "some popular type systems are so bad that they're worse than no type system at all" then I agree with you, but don't tar all type systems with that brush.
A language that is exceedingly popular and has a great type system and has great tooling for tests is Rust. When using Rust, it’s quite evident why you have to use both and not one or the other.

And when it comes down to it, in theory, proofs are better than tests, and Id say no one would disagree, and in practice, types are proof. Unfortunately, you can’t use types for proving everything, so that is where tests comes in.

Imagine a platform where you could indeed prove everything. I believe, but am not sure, that there is some languages that do this, like Idris.

Nowadays I feel that experience with some type system at least as powerful as Haskell's is required to criticize static typing... Well, of course, you can criticize it without such experience, but that only serves to look foolish when you complain about stuff that is solved for a decade.

Are you really complaining about lack of generics?

Come back to me when the most used programming languages have a type system like Haskell's. Then we can talk about the benefits of static typing over dynamic typing. Until then, static typing is mostly "expected a HashMap<i32, u64>, got a HashMap<i64, u64>" or other pedantic stuff like that which is only really meaningful to the compiler.
> Come back to me when the most used programming languages have a type system like Haskell's.

We never get there if the conversation about types is dominated by people who have only used Java-like type systems. Even ignoring that, demanding that powerful type systems be ubiquitous before discussing their benefits is a complete non sequitur. There's no excuse for ignorance, here.

You are confusing "static typing" with "algebraic typing", which is related but different.
> Yes, I'm more likely to have incorrect code, and it's probably going to show up while the code is running, and not before. Many times, that's a tradeoff I'm willing to accept.

I find the runtime errors I get from dynamically typed languages typically much clearer (and easier to debug) than many compile-time errors from C++ or Haskell.

I do like C's static type systems because it's needed for efficiency at runtime. (Re-) Compiling C can be close to dynamic execution for not-too-large applications. Often also C++ is needed for easy to use containers, but as some else said here it's really a tradeoff because compiles are much slower (I don't know why that is, but part may be because containers are re-compiled for every compilation unit that uses them).

And the overwhelming majority of bugs really appear on the first run of the dynamically typed code. The bugs that remain would have very often been also bugs with statically typed languages, since these are so ridiculously bad at expressing the simple invariants... They get unusable much faster than they get good at helping with bug-discovery.

It's not a dichotomy at all. Type systems etc are obviously there to make it easier, faster and safer to build and maintain your code. I'm much more productive in Swift than I ever was in objc.

The one case where it might make sense to through CS out of the window is if you're building something very small that you are sure you will never reuse or even look at again. And even then I'm not sure it is faster to be sloppy.

*throw (not through)
One very common complaint I've heard about Haskell is slow compile times, see this discussion for example with a bunch of GHC developers:

https://www.reddit.com/r/haskell/comments/45q90s/is_anything...

That's a real problem, but it's manageable for day-to-day development because reloading code in the REPL is really fast.

I'm using Haskell at work at the moment and while rebuilding everything and rerunning all the tests takes a frustratingly long time, reloading just the module I'm working on and playing with my changes is so fast I don't notice any delay. In practice, this means that 95% of any given task feels great but the final 5% before I'm done can be a real pain because I need to rebuild everything to faithfully reproduce our production environment and that does have a slow iteration time.

GHC is very slow. That said, cabal defaults to incremental buillds, so most compilations will take few seconds.

It does not change the fact that GCH is slow, since you must do full compilations once in a while. But it does let you keep your flow while developing. Besides, GHCi is much faster.

It's funny how when someone describes their own experience, you tell them they're wrong. Are you claiming the GP didn't actually experience fast reloads?

Initial compiles of some Haskell libraries that essentially do exponential inlining (cough vector-algorithms cough) can take a long time. An incremental non-optimized compile of a small change to a project with a good module structure takes a couple seconds.

The GHC devs are correct that it has been slowing down and are putting a lot of effort into getting that speed back. But it's not at the level of "rebuilding my project takes hours" that you frequently get with some build systems.

Well, here's another data point. I made a single file prototype to implement a Tetris game just for fun. I think it was with the Haskell SDL bindings or so, and I went with the most straightforward way about the implementation, and do have a reasonable level of experience with Haskell.

I gave up when the code reached about 400 lines. The compilation times were at 10-15 seconds already, and the error messsages were really ugly.

In short, compilation times depend on how you use complex type system extensions, or even only how much the libraries that you use make use of the type system. (And if you don't use the type system much - it becomes such a bad developping experience in most application domains, or you code performs very badly, etc.).

It was so much simpler to do it in C. <1 sec compiles, incredibly performant with straightforward non-optimized code.

> It's funny how when someone describes their own experience, you tell them they're wrong. Are you claiming the GP didn't actually experience fast reloads?

Nobody is saying anyone is wrong. One person can perceive short compile times that another thinks are long. But, we shouldn't make generalizations based on one datapoint. Maybe you _can_ have fast builds with Haskell, but maybe that isn't the norm.

That discussion is over a year and a half old.
True, but in my (very limited) Haskell experience, it's got worse since then, not better.
Well, yea, hopefully in the future we have languages that allow both. I do feel like research in language design has ignored the interactive and fast feedback loop aspect though, at least from the reasearch I know of.

My biggest gripe with static checks, is that in reality, most of the software most programmers are asked to write don't need to have 100% correctness. As long as 95% of the most likely to occur and the most user impacting bugs are fixed, the rest doesn't matter.

So I'm really interested to see the optional type system research mature more. When I start programming, I rarely know what the functionality should be, I have a vague idea, but I need to experiment. I don't need each experiment to be correct, at that phase it could have tons of bugs, as long as it can give me a sense for the functionality, and allow me to demo it to the business so they get a similar sense. Static checks slow this process down a lot, even though I do have fun making each experiment correct, its really just a waste of time for the product.

But as the desired functionality gets clearer and clearer, then I'd want to start working towards that 95% correctness, and types are quicker to write then tests. I'd rather have types to assert type errors, borrow checks to assert no memory errors, and tests to assert functional errors. Then have to write tests to assert all three, because tests are the slowest to write. But writing 100% type annotations or memory annotations is too much, just like I wouldn't write 100% test coverage in practice. That's because most software needs 95% correctness. Not 100%. So this is the struggle I feel.

Um, we have those languages NOW. We've had them for decades. You just need to use them:

https://www.haskell.org/

I've used Haskell, have I overlooked parts of it? How does it solve my problem?

Also lazyness I'm not a super fan of. And I/O is kind of a pain, not sure its worth the overhead just to achieve purity.

P.S.: I encourage people to use Haskell though. Its a great language, a step forward in a lot of ways, I'd be happy using it for work, just not as happy as I'd want to be, because of the problem I explained above.

If Haskell was a solved problem, Haskell 98 and Haskell Prime wouldn't exist.
Correct me if I'm wrong, but hot-swap requires a very particular set up and would be tricky to implement after a legacy java based stack has been established no? Mind elaborating on that a bit? I worked in frontend on such a system and looked far and wide for something that would let me maintain my sanity such as a turnaround time of less than 10 minutes.
> It might even be more important than a static type system, at least for some apps. But you can have both.

The important thing is not at which stage in the compilation pipeline the bug became obvious, it's at how many seconds elapsed it became obvious.

I generally found that working in untyped js with a workflow prioritising fast iteration was much better for finding bugs quickly than working in Scala with its advanced type system and miserably slow iterations.

But as you say, it's possible to have both.

Scala has incremental compilation now through SBT, I believe. How long ago was this?
A few years.