Hacker News new | ask | show | jobs
by toast0 1159 days ago
Rust doesn't have a lot of good runtime introspection tools (or they're very not obvious). If you're running a system with a lot of concurrency, it's nice to be able to attach a debugger and find out exactly what's going on with each of your tasks.

I haven't seen hot loading for Rust (but a quick search shows there's some out there), and I'm not sure how amenable Rust is to dlopen and friends to force the issue.

Erlang (and Elixir) have a constrained language that allows for BEAM to be effectively premptive in a way that a Rust concurrent runtime can't be. At every function call, BEAM checks if the process should be preempted, and because the only way to loop is recursion, a process must call a function in a finite amount of time. A Rust runtime cannot preempt, if you need preemption, you've got to use OS threads, which limits capacity, or you need to accept cooperative task switching.

Also, some of us are as anti-typing as you are pro-typing. :)

3 comments

>Also, some of us are as anti-typing as you are pro-typing.

Assuming ample experience with both, how does one reach this conclusion?

I have yet to see a project of any size that needs to be worked on by multiple teams and is written in an untyped language not descend into dumpster fire.

I work on a lot of 'glue' issues, often with languages like Perl, PHP, and Erlang (and a bit of Javascript here and there). Specifying types all over the place in languages like C, C++, Java, and Rust feels like it gets in the way and limits more than it helps. (feelings more than data here, of course)

Sure, at boundaries between teams, you need to specify the data in some way. That could be a type, but for me, often the other team is using a different language than me, so it needs to be a language agnostic type, and it can't include unsigned numbers because Java can't cope, and it can't include large integers because Javascript can't cope, etc. Protobufs are popular, json is too.

I have a lot of unpopular opinions though, and that's fine. It's just tiresome that everyone wants to come in and add types to things that don't need them. Also, I agree with dllthomas, most developers and teams are capable of creating dumpster fires in all sorts of environments, with all sorts of tooling. :)

You can absolutely create a dumpster fire in any language.

Putting the fire out in an untyped language is a Herculean effort.

In my experience it is even harder in a typed one because now you have to deal with the type system nightmare they built. So the compiler fight your refactoring.
You never actually get away from types, they are a core requirement of using any data beyond raw bytes. The guarantees that a strong type system provide mean you can be certain about certain things before your program even runs. If that's a problem for you, you're likely just leaving bugs on the table to be discovered at runtime.
I’d modify your statement to say "strong, static type system". Strong and weak typing are orthogonal to dynamic and static typing. JavaScript has weak dynamic typing; TypeScript has strong-ish static typing sitting on weak dynamic typing. Ruby and Erlang/Elixir have strong dynamic typing. Rust and Go strong static typing (Go’s is weakened by interface{}, IMO, but it’s a valid choice).

With the way that Erlang and Elixir pattern matching can be used in function heads, I can have much the same feeling of certainty that people express from Haskell and Rust. (Erlang typespecs help here, but are not checked by the compiler itself, only by additional tools like dialyzer or gradualizer.)

Nobody code without a type system. The distinction is build time vs runtime type checking. At build time you catch bugs that would appear later at runtime. Which is more costly to fix later.
> Which is more costly to fix later.

This assumption is changed, IMHO, by Erlang. Hot loading makes the cost to make small changes very low. So the question becomes, do you pay the definite cost of build time type checking (usually includes coding time type annotation), or do you accept the possible future cost to making small fixes.

Of course, if you work in an organization where even a small fix requires months to release, then do all the things you can to prevent making small mistakes.

> Nobody code without a type system.

A small number do. Assembly languages are generally untyped. The Forth language is also untyped.

> At build time you catch bugs that would appear later at runtime. Which is more costly to fix later.

Generally agree. Programmers proficient in Haskell or Ada tend to consider types to be integral to their development process. The real question is whether this is a good tradeoff against development velocity, for your given project. Neither language markets itself for rapid application development, instead they tend to emphasise that the language aids with correctness and the ability to reason about code's behaviour.

Well no since unit tests run faster than build time.
Conway's Law means you have to fix the organization before you can fix the software. That's the real Herculean effort.
Test coverage is, in my experience, much more important factor than typing. A codebase with great testing is much easier to aggressively refactor/change whether typed or not.

That said, a dumpster fire usually has no or little tests, so maybe we're arguing non-existent hypotheticals :|

I’ve gone back and forth over the years on whether tests are a good enough replacement for types. A few thoughts:

- Types and tests find different bugs. I’ve found new bugs by converting a project from javascript to typescript. The project in question had a 2:1 test:code ratio but as soon as the typescript compiler could read it, it spotted a couple obvious errors.

- Large test suites often make refactoring harder, not easier. If you have a clear, fixed API boundary and your tests test that boundary, then testing helps. But most refactoring also involves changing up those APIs as well - since bad APIs are often the reason you want to refactor in the first place. When you do that, you have to also rewrite all your tests. Good type systems help refactoring. Writing rust in Intellij, I can globally rename functions and types in my project, promote tuples to structs, reorder function arguments, and all sorts of other handy refactorings. My tests get updated too. And the compiler tells me immediately if I missed anything, without needing to rerun my tests.

- Reading the types is my favourite way to get up to speed on a project, or get back up to speed on something I wrote myself that I’ve forgotten. "Show me your flowchart (code) and conceal your tables (type definitions), and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." -- Fred Brooks, The Mythical Man Month (1975)

- I find I need far fewer tests to write reliable software when I’m using a language with a good type system. Most rust code I write works correctly once it compiles. Javascript is easier to write than typescript, but it’s harder to test and debug.

So with all that, I’m personally in camp type these days for most software. I think it’s usually the right choice.

> If you have a clear, fixed API boundary and your tests test that boundary, then testing helps.

A clear, fixed API boundary is exactly what Phoenix tries to encourage with contexts. Unfortunately, a lot of developers find them hard to understand. They're simple if you read up on DDD but again, a whole host of developers won't, or don't, do that either. LiveView in particular has a really a really great testing library [0] where you can write what are essentially end-to-ends that never touch even a headless browser. Since I'm always writing LiveViews, I pretty much only write LiveView tests and contexts tests which gives me large coverage (also some unit tests for the odd utility function). Otherwise, it's really important when writing non-typed functions to make it really obvious what is coming in and out, which is arguably a nice forcing factor.

The number one thing people bring up when shilling types is large codebases (it's been brought up in these comments). My opinion there I have found is quite unpopular and that is that pair programming should be far more prevalent than it is. I think the whole notion of "just stick a junior on that" is broken and I don't understand how types make that situation _that_ much better.

All said and done, I'm not actually anti-type. I mostly just find them to be incredibly noisy compared to a well-written function. I really like Ocaml where it's statically typed without needing to actually specify them.

have you tried doing it in elixir? It's not that bad.
I think that glue-issues are especially well captured by languages with good static type-system. Moreso than in languages without static types, because when it comes to glueing, there are many things, especially errors, to consider that can easily be forgotten without the help of a compiler.

However, a language with an insufficient type-system indeed makes things harder than they are without it. I would count all the languages you listed into this category.

As another poster mentioned, typescript is fairly expressive. There are other (production) languages too, such as Scala or maybe D. And there are lots of academic/very-niche languages.

> It's just tiresome that everyone wants to come in and add types to things that don't need them

Well, types are there, if you like them or not. There's a reason that you have e.g. typeof in javascript, gettype in PHP. The question is rather if you explicitly annotate them or not. But yeah, sometimes it's not helpful to annotate types, especially if the language is incapable of expressing the correct type anyways, which is true for most programming languages.

IMO, TypeScript strikes a great balance here. I loved the way I could cast something to `any` when hacking something out, then add proper type annotations once it's ready to be productized. It also a did a good job with type inference.

Disclaimer: I work at Microsoft, but not in the Developer Division

I always wanted to ask someone whose “native tongue” is untyped languages — when you reason about code, what do you think of an object, is it of specific type? Nominal, or more like structural typing that you know that it has to have this and that method?

I have started programming in untyped languages, but simply can’t remember back at all, and now I can’t really imagine dealing with objects in my mental medal as not having some type.

Note: this is not a rebuttal for/against dynamic typing, I do think that types are really important at boundaries, but they may not be the silver bullet - contracts may be better at some things, for example. This may be an open question.

I generally think about "types" in terms of capabilities more than shapes. When I write Ruby, I don’t generally think "this parameter must be an array". Instead, I think "this parameter must be an Enumerable". Or I think "incoming objects must behave like strings" (that is, they implement #to_str or are Strings)…although most of the time, I would really think "incoming objects must have useful string representations".

In Elixir, I do think about shapes more than capabilities (because Elixir is not OO), but with pattern matching, I can either specify "this must be a MyApp.Account struct" (which is just a fancy map) or I can specify "we will handle any map that has the keys X and Y, and Y must be a map itself".

I replicate this more formally when writing TypeScript, usually by building up type definitions and specifying those.

I’ll first say that my favorite languages are StandardML (very strongly typed), Common Lisp (not very typed), and JS (even less typed).

Look at Erlang. It has bigints, floats, Booleans, but-strings (sequence of bits —added because it is so common in telecom), string (not technically a primitive data type), functions, atoms, list, tuple, and map.

None of these look the same or act the same. People dream about seeing `123 == myMap`, but it simply isn’t a common thing because it doesn’t make sense.

The common rebuttal becomes: but how do I know if property X is a string or number when I’m using it?

If that’s your question, you are already messing up. What you really want to know is what X actually represents. Otherwise, you’re just shooting in the dark which is at least as dangerous as getting the type wrong and probably more so because a wrong type will become obvious quickly while mangling that number or string may not be caught until a much later time after serious damage has propagated throughout the data.

Let’s say you have something called `login`. Is it a number or string? If it’s a string, is it an ISO date, UTC date, or something else? Is it when they logged in or when their login expires? If it’s a number, it could be a Unix string. It could also be a calculated value for how long the user has been logged in and could be days, hours, minutes, seconds, milliseconds, or something less common.

How do you know which thing is correct?

In a good codebase, you read the docstring comment on the data constructor that describes what it does. If it says “milliseconds since last login” vs “token expiration using ISO datetime format” do you have any question at all about whether it’s a number or string?

If there isn’t a docstring, you’ll be digging through that code or playing around with the responses and will see the data type anyway.

The result is that you’re forced to better understand what you’re doing which isn’t a bad thing in my opinion. There may still be mistakes, but that leads to the next point.

Dynamic languages generally make it easy to dynamically check the types of incoming data and tend to be more flexible with mistyping (especially JS). I can’t count the number of major errors from common typed languages because they make introspection hard, so programmers don’t do it and crash on malformed data or when an API suddenly changes.

It’s also worth talking about null exceptions. Many dynamic languages expect type weirdness and handle it well. This usually includes null. Most statically typed code out there has LOADS of null exceptions lurking about which only trigger in obscure cases during runtime. In this regard, you could argue that the worse type issues also happen in typed languages, but are more dangerous in those languages too.

Untyped languages also tend to use safe numbers everywhere. Infinity is mostly useless, but not usually dangerous. Bigints everywhere are slightly slower in some cases, but completely eliminate overflow errors. Most typed languages use risky numeric types, so they must also force users to think about those things.

Finally, no common typed language offers good runtime introspection like smalltalk, Common Lisp, Erlang, or even JS. I feel static types are just a crutch to make up for this deficiency.

That brings us to good static languages. Typescript offers all the benefits of normal, unsound static typing combined with all the robustness of JS’s dynamic environment. The same can be said for Coalton and Common Lisp.

StandardML offers a language that feels like a dynamic language, but still offers static checks that are actually sound and completely eliminates null exceptions. Rust (an ML in spirit if not in syntax) does the same things in environments where garbage collection and other such amenities aren’t possible.

I like both kinds of languages and good examples from each word around the problems of each approach to make them (in my experience) about equal in productivity for equally skilled and experienced developers.

I tried Haskell for a while and switched to Common Lisp (although I still follow Haskell from a distance). My experience just doesn't match up with the claim that a project of any size written in an untyped inevitably descends into a dumpster fire. I've worked on largish systems in several dynamically-typed languages and several statically-typed and I personally haven't noticed any major difference in overall productivity suggesting that static types are better: they just have different friction points and different ways of working work better in each paradigm.
This is exactly my experience as well. Code architecture, metaphors, tests, etc have had larger impacts on both initial development as well as long term maintenance and malleability.

Some times, some type systems actually make people jump through hoops to accommodate their design and then it can actually have a negative effect. Other times, the typing helps.

It’s kind of like really good grammar and punctuation. They can make a story you write better and clearer. But they far from guarantee it. You can write a very good story with subpar grammar/punctuation. And you can write a really lame story that is grammar perfect.

One thing that I haven’t seen much in the discussion, is any discussion about Elixir’s matching abilities. Does Rust have that as well? I love what Elixir matching does for my code.

(Edited spelling)

The issue isn't productivity. As far as just slamming out code untyped languages are undeniably faster.

The issue is working on projects once they've reached a certain size where you have no idea what the intent of the original author was and you maybe need to refactor, add-in major pieces, or change anything with the expectation that it continues to work.

I’m including maintenance costs in “productivity”. I’ve worked on large dynamically typed codebases and never experienced what you’re talking about.

I think it’s a question of understanding how to work and think without explicit types rather than something that makes statically typed codebases easier to maintain.

Untyped code bases with microservices are the best code bases out there by far.

They are exceptionally easy to refactor, add-in new parts, etc.

The keyword is microservices, you need to know how to do proper microservices if you are using untyped code.

I dunno. Something like spec, dialyzer, or "assertive" typing (in the case of Elixir) on the boundary works just fine for me.
Microservices just push the problem out of sight — now you need interoperable types between them, there are race conditions, IDEs don’t see inside the black box of other services so refactoring is harder, etc.

They can be the correct solution sometimes, but blindly applying them everywhere is just dumb.

While I'm pretty solidly in the "pro-typing" camp, it seems worth acknowledging that such projects often turn into dumpster fires in typed languages as well, even expressively typed languages.
Maybe, maybe not - but in my experience, a dumpster fire with static types is easier to read and understand and also easier to refactor.
That matches my experience as well. I just think we need to be resistant to reading too much into "I've seen bad code that X", because that can easily be true of almost any X.
You think that statically typed languages don’t often turn into dumpster fires? Not my experience! Even though there are real advantages.
Sure, but they don't turn into dumpster fires because of the types, as untyped projects tend to do
I don't know that that's true. When the types in a program are designed in a way that's sufficiently out of sync with what a program needs to do, you can get a lot of mess working around them.

Can we say that's misuse of the tools? Sure. Is it less likely than things becoming a mess without types? Probably? Even more so as the tools improve and as the people involved know better how to use them.

I have yet to see a project of any size that needs to be worked on by multiple teams not descend into dumpster fire. Typing can definitely help, but it's just a small tool. Java is a pretty typed language and I'm seen some real doozy code bases in Java.
Though that might just as well mean that typed language projects, like those in Java have actually survived long enough to have turned into that.

It is easy to be maintainable at iteration 1 when the requirements haven’t changed 20 times yet.

>I have yet to see a project of any size that needs to be worked on by multiple teams and is written in an untyped language not descend into dumpster fire.

Github? Dropbox? I mean they both eventually went to type hints in their respective languages or migrated to a typed language but for a long time I'm certain it wasn't.

It's worth noting of course that ~Github~ Dropbox was the driving force behind mypy
yes, that is exactly what I meant by "they both eventually went to type hints", but doesn't github use ruby? I think you mean sorbet? Dropbox was the driving force behind mypy (IIRC).
bleh, I meant to say Dropbox.
And I can pretty much guarantee you that some form of dumpster, fire or other was the driving factor behind those moves.
Untyped languages work fine if you use them with microservices.

The only thing you can't do is have both untyped and monolithic at the same time.

for some reason people are able to use huge undocumented common lisp monolithic projects from the 90s without much effort and without setting their computer on fire. why do you think that is? i mean, given your world view, why would people even think about doing this for a codebase that doesnt have static types?
Maybe because the language is not untyped? It has both dynamic typing and optional static typing.
sure but not in a sense that rust is. my point is that it is entirely possible to build good software with substantial code bases in dynamically typed languges and i used common lisp as an example. in fact i dont know of one common lisp code base that turned to a dumpster fire because of typing problems. instead i find the opposite true: old forgotten code can often be resurrected because the language promotes clear coding and interactive introspection
This interactive introspection is usually facilitated by dynamic types and actual types in the software. For example large parts of the Lisp software in the 80s were already written in an object-oriented way, where the code was structured around explicit classes. Means: one sees the classes/methods both in the source code and in the running code. The source is far from 'untyped'.

For example the Filesystem Browser (FSEdit) from 1980 was written in Flavors. It actually uses an explicit OOP system with hierarchical classes.

https://tumbleweed.nu/r/attic/sys78/file?name=lmfs/fsedit.li...

Lisp is not Python or JS.

People use Python and JS, not Lisp.

Dynamic typing in Python and JS + medium-sized project = dumpster fire.

I'm just trying to point you in the right direction.

Dynamically typed microservices is where it is at.

thanks, but maybe not
it's certainly possible to write a nif that cooperates with the VM so that its async yield points match up with the VM's expectation of yield points. Definitely tricky to do correctly in C (given that C doesn't have a yield statement, lol, you have to structure it as an awkward tail call where you pickle/unpickle whatever state you want to keep around or unmarshal it from a passed struct). I'm not sure if that's so easy to do in rust.
Sure, you can do that for the code you write, but you can't for the code you call. Any Erlang you write or call will have this property, and most of the ERTS provided C code will as well; either it's trivially finite, it is designed to yield during the work, or it's neither but it hasn't triggered anyone to fix it, with occasional deviations of things that become known issues (like -- was for some time; although it got fixed twice and is now fairly ok).
While Rust doesn't offer this kind of tooling, the JVM and CLR ecosystem certainly do.