Hacker News new | ask | show | jobs
by wofo 957 days ago
Lack of proper types is what killed the joy of programming in Elixir for me (I lasted 6 months, hoping I would somehow adapt, but the experience was so miserable that I decided to move on). I simply can't understand how anyone can be productive in a big codebase without the rigor of static typing, but those people exist, so I guess there must be something about our brains that divides us in the dynamic vs static typing camps.
8 comments

I'm firmly in the camp that it's all perception. The studies I've seen aren't what I would consider conclusive, but they certainly suggest there is very little difference in the output and bugs of dynamic vs static languages.

In my experience, static typing seemed to lend itself to poor testing, maybe some sort of belief that static types were good enough to not need tests that can prevent regressions. So from my point of view the static typing is negative value. It prevents such a low value class of bugs while seemingly incentivizing people to be lax with the most important classes of bugs.

I literally struggle to read and work with dynamic code. My head explodes when trying to hold return types or the shape of semi-complex data structures in my head, versus having it spelled out. I wouldn't call it an issue of "perception", I really think in my case it's "capability", pure and simple. I've programmed for thousands of hours in both styles, so we're well past the possibility of this being an adaptation problem.

I am totally convinced that the advocates of dynamic programming are right, too, just possibly built differently. For example, if I had an order of magnitude more working memory than I have as an individual (I'm assuming that's neurologically plausible), maybe I'd view dynamic programming differently, too.

I’m a proponent of dynamic typing. I’d say the same thing. I don’t have enough working memory in my head to store the compile-time type information about every relevant variable, in addition to the actually important information about what the runtime data could be.

The way that I write static-typed code is by imagining how I would write dynamic code to solve the problem, and then additionally imagining what types and type constraints I need to add.

I honestly think it’s a kind of an instance of Paul Graham’s Blub Paradox. I know JavaScript and I spent years writing JavaScript. So if you ask me to write TypeScript, well, I write the JavaScript that I want to write and then go back and add types to make the typescript compiler happy.

There are a bunch of other things I could talk about. I should write a post.

> I don’t have enough working memory in my head to store the compile-time type information about every relevant variable

Neither do I, but I know the compiler will check that for me so I don't need to hold all of that in working memory. I know my IDE will always be able to tell me the types too, and flag if anything is wrong immediately.

The different points of view on this are really interesting.

In the end both approaches work well enough to keep companies in business. Companies fail not because of static or dynamic typing but because of marketing, internal fights, etc. The technical details are something that impacts very rarely.

Personally I worked in C for the first years of my career. Then the web happened and the two languages for it were Perl and Java. No more mallocs and frees, it was so great that I kind of forgot C. Then I was assigned to higher level tasks than programming and when I came back to it almost 10 years later I refreshed my Java (I kept doing Perl for my own stuff - CLI scripts and CGIs) and discovered Ruby on Rails. I realized that I could do the same web apps I was writing in Java with much less code and without having to lose my time after obvious but nearly useless details such as specifying that a given variable is a string, or number and how big, or an array of that class of objects indexed by strings (I intentionally use generic terms), etc. It's almost always clear what it is, especially if one picks good names for identifiers. There are some hiccups but not every year.

By the way, a great feature of Elixir is pattern matching in function definitions. In a pythonish pseudocode

  def fib(0): 0
  def fib(1): 1
  def fib(n): fib(n-1) + fib(n-2)
Of course n is an integer. It works with floats too and probably fails with anything else. But who would run fib on some complicated not numerical class that happens to implement the - operator?

I accept the argument that providing type information to a compiler lets it generate faster code. However none of my customers from the last 10 years care about that argument and they selected their technological stacks. Like everybody else they care about getting features done as quickly as possible. They all run their services on a single server, make enough money to pay themselves, their employees and a bunch of consultants like me.

> The way that I write static-typed code is by imagining how I would write dynamic code to solve the problem, and then additionally imagining what types and type constraints I need to add.

Is that not how everyone writes static typed code? You have a need for a variable holding some data, you think about the bounds of that data, you pick a type. What other way would you do it?

The problem with dynamic typing isn't in the first-write. It's all in your head then. It's in the 2 year later bugfix, when you're looking at a function and wondering what kind of data is being passed into it. And then you find all the places that call that function and still can't work out what fields that object will have, or whether they're numbers or strings.

> The problem with dynamic typing isn't in the first-write. It's all in your head then. It's in the 2 year later bugfix

The 2 year later bugfix is often easier I agree. But for me by far the biggest difference is speed and effectiveness in writing new code. I'm not just much faster, but also much better with typed code probably because it fits the way my brain works better.

> Is that not how everyone writes static typed code?

For me the process is a bit opposite. I tend to write the interfaces first, usually until the entire routine of whatever I am writing is complete. So e.g. only the types accepted and returned by functions, but not the actual function itself. Then I go back and add the code itself usually at the very end.

My working memory is garbage and I do prefer working with dynamic langs, however. (I also have ADHD)

I do agree though if I have to work with spaghetti code, I'd much rather work with a language that has a proper type system (I mean a proper one, like OCaml, Standard ML, Rust, Gleam, Etc.) I think static typing in these languages doesn't get in the way, they all have varying degrees of type inference, the type system is sound and uncomplicated. Whereas a Language like Typescript's type system (of Facebook's Flow for JS) had my head spinning, It often felt like playing a mean spirited scavenger hunt with Simon from Simon says.

As far as dynamic langs go, some languages are terrible to work in, (Perl). and other languages because of strong conventions (Ruby and Elixir,) I find very very easy to work in, write tests, create libraries that use DSLs, etc.

So I guess I'm in the middle? leaning in the dynamic langs direction though.

For me personally, my new theory is its an ADD thing. I'm older but recently came to fully appreciate how ADD my mind is, and how I've basically built many habits around coping with it. More recently I learned types is one of those things. I realized with types I can off load a ton of my working memory into the type system. I can bounce between ideas the way my brain wants me to without losing any state, because I'm always setting up the interfaces to the various thoughts I had. And the IDE knows how to take advantage of course, linking everything, popping up docs and hints and etc for every little idea. And so the result is it just matches my brain's natural patterns.

I still remember being quite shocked many years ago when I took a job using Java of all things, I was so fearful coding would take forever. It was my first typed language. But within months, I was faster, by a lot. I would write a lot more code before I had to test it, and once I began testing it there were few if any surprises. Now I'm working in Ruby, and without a debugger and constant tests, I feel I can't hardly get anything complex done in a reasonable amount of time. Like you I thought this was a hurdle that would pass, but while I'm far more effective than when I started, I find that its overall just mentally taxing to write code in this language.

Yet at the same time, when I pair with people who do not dislike Ruby, and watch them work, they are constantly asking "What is this" and "how does this work" and navigating parts of the codebase by grepping. They are effective, and I know its been studied a bit. But to my eyes, they would so obviously be far more effective with types, even when they state they dislike them, I just can't accept its a generally better approach in the absolute sense. I won't judge people for preferring a non-typed language. But I'll likely not accept another job where I can't use a typed language.

Nice to see that I'm not the only one - lasted 9 months trying to adapt to Elixir and couldn't. My background is Scala/Rust with heavy use of effect systems and reliance on writing the types out and then solving the puzzle of how to make everything fit. The tooling is pretty poor due to lack of investment compared to some of the other languages, and the debugging story is not great either. But hey, it works for some.
I found the tooling to be quite nice. In my opinion, it’s better or equivalent to most popular languages (python, ruby). It’s definitely not full-on bumper rails/training wheels like a Microsoft IDE, but it’s obvious they’ve invested heavily in tooling early on.
same here, mate. though mine is haskell. I'd rather waiting for gleam to mature enough to mess around with it than to be miserable with the dynamics of the data. I just don't like to guess stuff down the line.
I believe this as well. I like dynamic typing, because it often feels to me that rigour of static typing is slowing the prototyping phase. Plus overusing types creates a lot of boilerplate. In one of the frontends I've seen adding a boolean field to a form required changing over 10 files - thank you Typescript.

On the other hand, sometimes I feel like it has a lot to do with test writing. I feel people enjoy static typing, because you can get a feedback loop catching certain things without writing any tests. If you do write tests, all the type errors get caught pretty immediately anyway, so I just don't see the benefit.

Personally, the biggest advantage of dynamic typing for me is the ability to skip formalities for a time. If I want a function that can modify any struct with field "val" in (by, let's say, setting it to zero), I can - and I don't have to do multiple definitions, I don't have to define an interface. Just a function and I am done. If I want to skip error handling for now and only code the happy path, I can - and my program will work (and crash on error, but in some cases it's ok).

As the projects get more complex and require higher safety guarantees, static typing helps in ensuring nothing got broken - but nothing beats dynamic typing for prototyping.

The example is, interestingly possible in TypeScript, at a very cheap cost

  function modifiesValField(struct: {val: number}) {
    struct.val = 0;
  }
Structural types and inline interfaces (or as I like to call them inyourfaces) are pretty cool.

But I do get your point - although type systems have gotten much less annoying, its not like they're perfect.

I do think types and tests have a lot of overlap. In an ideal system, you would have tests that result in progressively more narrow types as each test case is added, providing the benefit of both.

This is basically the entire reason I am so excited about Zig.

Edit: it's also the reason I have a love/hate relationship with typescript. Typescript almost lets you do this if you try really hard, but at the end of the day it's not designed to truly test your code and requires you to "cheat" in order to make the types properly describe the situation; every time you "cheat" in order to get a more expressive type (whether that's looser or narrower), you would have been better off writing a test instead.

> If you do write tests, all the type errors get caught pretty immediately anyway, so I just don't see the benefit.

That's a pretty big if, though. You will never catch me in my typed backyard, writing tests that say: if typeof(arg) =! string then return error("Not a string!")

An additional problem of course is that solutions that assume extra work from people, tend to be brittle ones (also thinking about "manual backups" here) aka. those tests will end not being written at all.

You will never find me in my dynamic backyard writing such tests neither - I will write normal tests and they still expose type errors.

And, if you do not write tests, there is a bunch of other problems that will surface, which will not be caught by the compiler itself. What I'm saying is static types give you feedback loop:

write code -> make the project compile -> fix bugs

while the dynamic ones give you:

write code -> fix bugs (including type ones).

Obviously, YMMV. For me it works. And to be honest, from my experience nothing is more brittle than a type hierarchy designed early in project lifetime and then fixed repeatedly until it "works", but again - to each their own.

You shouldn’t be writing that in dynamic languages anyway.
I mostly agree with you. Just wanted to add that in some languages, for example in Typescript, there are ways to say that you do not care about the type for prototyping. For example using :unknown or :any.
:unknown is great, but the problems come in the middle area; when you have lots of very tight types and are prototyping something that makes use of those things, :unknown doesn't work, and :any can be too expressive, even for your prototype.
> when you have lots of very tight types

This is true. But also I've noticed many less experienced TS devs end up writing types that are too specific and not utilizing structural typing enough. So e.g. writing some type and demanding a function accept it, when that function only actually needs one or two properties from it. Instead that function can inline or localize the part of the type it needs, and the original type can be unaware its ever being used by some other function.

And the other thing is trying to leverage the type system too much. For my style, if the types aren't making things easier to read, write, and maintain, they should be loosened and / or duplicated. I blame a lot of libraries here, it should never be puzzling to figure out how to type something so a function accepts it, yet that comes up quite often; I end up diving through these nested and overly DRY types trying to find what the heck even is this thing? That's crazy. If you need to cmd+click more than once or twice to understand what a type is, its way too abstracted.

Sorry, but that is moving the goalposts. Quick prototyping was mentioned, but now we seem to be talking about some undefined balance between typing and no typing for prototyping. It seems :any solves the problem.
I’ve swapped back and forth between static and dynamic types a few times in my career. Java and C/C++ to JavaScript (and coffeescript) to typescript and rust. The entire time (30 years at this point) I’ve felt like whatever I was doing at the time was obviously the one true way to program. I feel that now - that what I’ve been doing lately (static typing) is clearly superior. Even for throwaway JavaScript based projects at the moment the first thing I do is install typescript.

I’m spitballing on the reason - I don’t know why it’s like this. But maybe it’s because static typing encourages you to write & plan your types first. When you know the data types, all the functions kind of flow around your types and everything feels like it obviously flows from that. With dynamic typing, it’s the reverse. I find myself writing my functions first (top down or bottom up) and then backfilling what arguments and data everything needs to pass around. I run my code a lot more as I develop it to stop bugs creeping in. And because I can run my code at any time. There’s no type checker stopping me from trying things out.

I've recently found a great middle ground for this, using esbuild to drive code as I'm writing it, while my IDE may meanwhile be complaining that all my types are broken. Getting comfortable having "broken" types while something is still in development is a nice middle ground, helping you come back to the hot spots whenever you're ready, and acting as a forcing function prior to commit or push.
I've been on both camps, so I'm hoping I can give some insight.

Lots of statically typed languages are very strict about their types and have too many of them. You have to admit, when you're trying to build something difficult and focus on getting the business logic part of your program right, the last thing you want to be thinking about is e.g. whether you need a String, ByteString, LazyByteString or any of the other types of strings, and which library decided to accept which one. At some level its definitely useful to distinguish between those, and I'm sure a lot of libraries make sensible choices. But initially in the development of the program its just unnecessary busywork that distracts you from the important bits.

In the past, typed languages also made it a bit harder than necessary to create new types, especially in situations where you have serialization and deserialization. And finally, we had to do all this work, and for what? To be unable to prevent the most basic of errors i.e. NullPointerException? You have to admit, it was a hard sell.

A lot of things have changed, however. TypeScript makes it really easy to define types, anywhere - inline on the function in the argument, or right above it. You can pretend the type is correct at deserialization time, or you can use libraries like `zod` to both define a validator and a type in the same syntax - its up to you. Rust similarly has powerful and easy to use tools like serde - structs and enums are easy to define, and easy to attach metadata to them quickly to (de)serialize them accurately. Huge differences from the old world of getters and setters and NPEs.

When using dynamic languages, there are techniques to make things easier. There is the often mentioned testing, which does help verify a large subset of code paths. Lesser known and more subtle technique is coming up with and following a strict naming convention for properties, for example, and to keep things as consistent as you can in general. This is why you often see so much focus in code reviews about conventions, linting and so on.

Nowadays I guess it mostly depends on how good your dynamic language techniques (at the team level) are, as well as what your treshold for type system annoyances is. There are still some annoyances even in the best type systems, but its gotten way, way better.

> you need a String, ByteString, LazyByteString or any of the other types of strings,

Out of curiosity, which language do you use where this kind of decision has to be made for daily programming problems?

This particular example with the string types was Haskell, but Rust often makes you think about performance decisions at all times as well. I'm fully aware that that's the point of Rust, however (and yes there are ways to stop worrying and just `clone`)
My best programming experience ever has been in OCaml and I love the idea of Elixir, but I can't even bring myself to try it because I know I'm not going to enjoy not having types. Even something as allegedly shitty as Python's type system would do it for me. I'm aware that there are ongoing projects to add typing to Elixir and I'll keep watching out how they evolve.
> I guess there must be something about our brains that divides us in the dynamic vs static typing camps.

I've wondered if that's a thing. It seems to be.

The only way I've seen it work (rarely) long term is with extensive unit tests and that takes incredible discipline.