8 ms·
> They [dynamic types] have potential to be more than "no static types", but that hasn't happened. It happened thirty years ago: https://en.wikipedia.org/wiki
by lisper 4y ago
> They [dynamic types] have potential to be more than "no static types", but that hasn't happened.
It happened thirty years ago:
https://en.wikipedia.org/wiki/The_Art_of_the_Metaobject_Protocol https://en.wikipedia.org/wiki/The_Art_of_the_Metaobject_Prot...
But the thing that really drives me nuts about this never-ending debate is that static and dynamic are not a dichotomy. You don't have to choose one or the other. You can have both.
The real choices are:
1. Do you want the language to require the coder to provide type information (e.g. Java), or permit the coder to provide type information (e.g. Common Lisp) or forbid the coder from providing type information (e.g. early versions of Python, Javascript).
2. How much work do you want the compiler to do with regards to finding potential problems at compile-time, and what do you want it to do when it finds one? Options include: do nothing (e.g. Javascript), warn if it finds a problem (e.g. C), or forbid you to run the program if it finds one (Java, Haskell).
Personally these choices have always seemed like no-brainers to me: allow but don't require type information, and warn when the compiler finds a problem but allow the program to run anyway. What possible advantage is there to be gained from any other choice?
- iaabtpbtpnn 4y agoWhy do you want to run programs that are wrong?
- aserafini 4y agoBecause the property of being wrong is not binary. It might be partly wrong but correct enough for your current situation.
- majewsky 4y agoHow do you know when it's crossed into being just a little too wrong?
- ReflectedImage 4y agoYou can have a catch all exception handler that emails any exceptions that occur to you. It's not pretty but it can be done. :P
- aserafini 4y agoI think a catch-all top level exception is totally fine and not ugly at all! That’s why my Python APIs never fatally crash, they just log an exception and move onto the next request.
- 3pt14159 4y agoYou presume that the program is able to determine that it is wrong. One of the most annoying things moving from Ruby / Python into a language with static types that are checked at compile time is dealing with JSON from an external API. I know, I know. The API could send me back a lone unicode snowman in the body of the response even if the Content-Type is set to JSON. I know that is theoretically possible. But practically, the endpoint is going to send me back a moustache bracket blob. The "id" element is going to be an integer. The "first name" element is going to be a string. Sometimes a disable-able warning is all we want.
- zozbot234 4y agoYou should check what the external API returns anyway. If only because the format of any external API might break in subtler way than just returning a single Unicode snowman, and you'll want to ensure that all such failures are correctly detected as soon as practically feasible.
- nkingsy 4y agoTypeScript can type based on object literals, so while you can't type JSON on the fly, you can type a JSON file automatically. With code gen you can usually just print out your JSON directly from the source and import that as a type into your source code. This is why apis are moving towards specifications that trigger multiple generators. JSON typing is possible without generation, but more cumbersome. interface TypedJSON { string?: string; number?: number; array?: TypedJSON[]; boolean?: boolean; object?: {[key: string]: TypedJSON} } {foo: {string: "my string"}, bar: {number: 4}} Probably makes sense to make the "string" key "s" for minimal payload size impact, or this could also be done by converting standard JSON to this typed representation at runtime.
- Mavvie 4y agoI haven't had problems with that in strongly-typed languages I've used (most notably, C#). If the response payload isn't JSON, or if it has a missing required field or something is the wrong type, then JsonSerializer.Deserialize(...) will throw an exception. I don't have to add more code just to make the type check pass or anything. (And if I did, _usually_ it's around nullability and the "!" operator would do the trick, or "?? throw new ImpossibleException(...)") And within the bounds of pragmatism, it's nice that it actually checks this stuff and throws exceptions. In Ruby if the ID field is the wrong type, the exception won't happen at time of JSON.parse(...) but instead much later when you go to use the parsed result. Probably leading to more confusion, and definitely making it harder to find that it was specifically an issue with the JSON and not with your code.
- lisper 4y agoBecause you haven't finished the whole thing but you want to test part of it before you continue.
- iaabtpbtpnn 4y agoThere's always an escape hatch for this. Even in Java (and Haskell), you can just throw an unchecked exception from your unimplemented function.
- josephg 4y agoLikewise in Rust you can todo!() in incomplete functions. But it’s still much harder to run incomplete code than it is in javascript, because you still need to satisfy the type checker and borrow checker even for incomplete programs.
- lliamander 4y agoThere are two reasons you may want to run a program that fails typechecking: 1. The program is in fact correct, but the typechecker can't prove the program is correct and so reports it as a failure (for contrast other typecheckers only report errors if it can prove that there is an error) 2. The type checker is correct in that the program is flawed in some way, but you are the middle of development and it would be more productive if you could observe the behavior of the program before fixing the types. Since your CI pipeline will fail on type errors it's not like you're going to be able to merge your changes before fixing them.
- ReflectedImage 4y agoLet me add a 3rd. 3. The program is incorrect but correct for all expected end user inputs.
- naasking 4y agoIn that case, add a type that constrains the values to the expected user inputs. Now not only are you passing the type checking, you're strongly enforced this invariant at the point where it makes the most sense, you've future-proofed this code against possible future misuse where the expected inputs might change, and you've made the code essentially self-documenting.
- lliamander 4y agoThat is the correct end state, but there can be practical considerations for why you might want to run (and possibly even release) the software in this partially correct state, and fix the edge cases later.
- naasking 4y agoI don't disagree, but most statically typed languages don't enforce a level of correctness where this would be an issue. We're not programming with theorem provers with an elaborate specification. I somewhat agree with your point #2 which is why Haskell provides a deferred type checking mode for prototyping purposes[1]. I think this should be more common among typed languages, but this is not a very typical scenario either. [1] https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/defer_type_errors.html https://downloads.haskell.org/ghc/latest/docs/users_guide/ex...
- hbrn 4y agoWhy would you want to talk to people who are wrong? Sometimes I don't care which political party the barista voted for, I just want to get my coffee. People and programs are never perfect. Type checked program is not guaranteed to be correct, it just has stronger constraints.
- TulliusCicero 4y ago> What possible advantage is there to be gained from any other choice? You're thinking like a single individual. Almost all programming that happens in the real world is in teams, and most of that probably within companies. Teams and especially companies often benefit from strict standards that keep everyone on the same page and make it harder to screw things up.
- dllthomas 4y agoThose standards don't have to live between the individual and every single execution, though.
- zozbot234 4y agoEven a single individual has to team up with his past and future selves.
- lisper 4y ago> Teams and especially companies often benefit from strict standards that keep everyone on the same page and make it harder to screw things up. Sure, but those standards don't have to be enforced by the language design. They can be enforced, for example, by policy, e.g. "Don't push anything that produces warnings to the master branch". That still allows you to, for example, test parts of an uncompleted program, which can vastly improve productivity. Not being able to test-run a partially completed program can increase your development cycle by orders of magnitude.
- subarctic 4y agoWell, sure, they don't have to be. As a team you can come to an agreement on policies, and if you have the time, you can set up linting or configure your tools to make sure that they are enforced in CI. We use Rust and TypeScript at work, and for the latter we've made some effort to make it as strict as possible, and even added some eslint rules to enforce things more. But it sure is nice when the language itself does that by default for you, so you don't have to advocate for lint rules or tsconfig options and spend time setting them up. Plus, if it's built into the language, it effectively means that even people outside your organization are following the same policies as you. All that being said, your point is a valid one. It's a tradeoff and depending on what you're used to, you'll feel the pain from one approach or the other more strongly.
- MiguelX413 4y agoRust has strong static typing but it also has very powerful type inference, I think it fails to fit in your dichotomy.
- galangalalgol 4y agoAgreed, and Julia is in an odd spot as well. The type inference combined with JIT and dispatch act make for strong dynamic optional typing vs rusts strong static inferred typing.
- josephg 4y agoRust’s type inference still requires you to manually specify the input and output types of every function. I think it’s clearly in the camp of “all types must be specified by the programmer / you get compiler errors”. The GP asked what’s the point of languages like this. The two massive advantages in my mind are that compiled programs are usually correct, and compiled programs run really fast. I often have the experience in rust of writing hundreds of lines of new code, and my program works perfectly the first time I run it. That’s a delight.
- masklinn 4y agoThe decision to require explicit global type annotation in Rust has nothing to do with "compiled programs are usually correct, and compiled programs run really fast", you can do that with global type inference. It has to do with documentation, intent, and clarity. Also clarity of error messages, compilation errors with generic functions under GTI are as bad as C++ template errors.
- CoastalCoder 4y ago> It has to do with documentation, intent, and clarity. I wish it were common [0] for source code to show a combination of programmer-specified and compiler-deduced details. Seems like we could get the best of both worlds. [0] I know some IDEs show e.g. type information as mouse-over text. I'm talking about something that integrates more cleanly into the actual source-code text. So, e.g., it would show up when diff'ing the source code.
- duped 4y ago> and warn when the compiler finds a problem but allow the program to run anyway Your compiler giving you a type check error is saying that the program is meaningless. What is the advantage of running a program that is known to be wrong? The design of the type system will define what is meaningful/meaningless per type check rule (eg a missing branch which can be a warning vs an ambiguous type inference that means the program cannot be compiled). The question is really whether you want to design a type system that can be checked statically, which to me the no-brainer answer is "yes." I don't want to have to run the program to know if it type checks. I don't want to have to run it to know what types are in the program, or the signature of functions. I want my tool to tell me when my code is provably wrong, and to prevent provably wrong code from being merged into a codebase without executing it.
- masklinn 4y ago> Your compiler giving you a type check error is saying that the program is meaningless. What is the advantage of running a program that is known to be wrong? Well one bit is you might not be looking at the meaningless part. Sometimes when doing large refactorings or updates never getting to run, try, or look at anything is discouraging. It's a great feeling when it compiles and everything works (or near enough), but spending two days cycling between compiling and fixing compilation errors is not as enjoyable as e.g. seeing the unit test report's green bar tick up. Maybe compilers should provide more enjoyable feedback, not just better error messages (though I fully support that movement).
- _se 4y agoLarge refactoring and updates are exactly where strong type systems are the most useful. Make change, fail build, fix build, be happy. Unit tests are still there to turn green when you're done. I can't think of many worse experiences than my past self trying to refactor Python 2 code. Compared to _any_ type system it's just not even close.
- masklinn 4y agoHave you considered reading what I actually wrote rather than just hook onto a pair of words and ignore everything else?
- skrebbel 4y agoThis is the best opinion about this topic I’ve ever read and I thought that after 20 years of programming in as many languages I knew them all.
- lisper 4y agoThanks!
- remexre 4y agoHaskell can actually do the "warn if it finds a problem" option: https://downloads.haskell.org/~ghc/7.8.2/docs/html/users_guide/defer-type-errors.html https://downloads.haskell.org/~ghc/7.8.2/docs/html/users_gui... I've been meaning to write up something comparing dynamic typing + SLIME to -fdefer-type-errors and ghcid...
- JoelMcCracken 4y agoThe existence of this option is what actually finally convinced me to give haskell a real try, but I have to say, I haven't actually ever used it. I alwyas just use undefined to defer any type error, actually seeing each spot there is an error and comprehending how to delay the problem for later basically seems to help me comprehend the problem I need to solve.
- chowells 4y agoIn Haskell, type information is used to generate code. And not just in fancy metaprogramming libraries like Servant - any use of chained type class resolution is generating code for you based on static type information. How does that power fit into your framework?
- jjav 4y ago> What possible advantage is there to be gained from any other choice? If you're the only one writing the entire stack of code that gets bundled into the delivered program, you're right, no real advantage from other choices. But as that's rarely the case, there is a lot of advantage from having a forced type system. Otherwise your code might be great but you're pulling in libraries and code from many other teams who chose to omit type info and/or ignore all warnings so you still have the same problem.
- lliamander 4y ago> Otherwise your code might be great but you're pulling in libraries and code from many other teams who chose to omit type info and/or ignore all warnings so you still have the same problem. This is actually a really great argument. Having well-typed dependencies is a real benefit. But I would say you don't need to force typing on people to achieve that. In my experience with gradually typed languages (TypeScript and Erlang) you tend to see decent adoption of typing in prominent libraries. There are natural incentives (i.e. crashing software) that drive people away from depending upon libraries that don't have strong type guarantees.
- lolinder 4y ago> What possible advantage is there to be gained from any other choice? Substantially better performance, which still matters in some domains. If you occupy any space on the spectrum besides fully static typing, you must represent and check your types at runtime. This means that every object (even Int and Bool!) needs to have a header that tells the runtime what type it is, which can quite literally double your RAM requirements. If a language is already including object headers anyway (like Java does), you're correct that there's not an enormous barrier to embracing gradual typing [0]. But there are a lot of places where we really do still care about RAM usage, and erasing types at runtime is an obvious place to free up a lot of RAM very quickly. [0] Indeed, you can sort of do this in Java already by just typing everything as Object and coming back later to correct it with actual types, though please don't actually do this.
- Someone 4y ago> If you occupy any space on the spectrum besides fully static typing, you must represent and check your types at runtime. If the language requires you to give at so much type information that the compiler can infer all the types, I would say that language, from the compiler’s view, would have fully static typing, so it wouldn’t have to do any checks at runtime, but from the programmer’s viewpoint, it could look as if it didn’t have fully static typing. The modern auto to deduce a type in C++ makes a small step towards that, but I think it could be taken further. The language could deduce types both in the direction “if you pass foo to a function taking an integer, it must be an integer” and “if you only pass integers to bar, bar must take integers”. I expect it will be hard to specify a powerful algorithm for filling in the blanks that feels natural to programmers, though. If the compiler frequently complains about missing type information where the programmer doesn’t understand why it complains, the language won’t be popular.
- lolinder 4y agoWhat you're describing is already common in functional languages such as OCaml and Haskell. They tend to use the Hindley-Milner type system, which allows for exactly the kind of non-local inference you're talking about (where `auto` is only for local inference): https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy... However, better type inference does not mean that a language is no longer statically typed, it just moves a lot of the burden of assigning types from the programmer to the compiler. You still don't have as much flexibility at runtime, and you still can't run your code until the compiler can prove that it it correctly typed. The only thing you get is less syntax, not dynamic semantics.
- BerislavLopac 4y agoIt's not just about type information; in fact, having type information doesn't mean anything in its own right. The point is in static analysis, which can be mandatory (most statically typed languages), or optional (e.g. Python). I am not aware of any static languages that check the types at runtime (if such languages exist, please mention them in the comments); however. they require a static analysis to happen at compile time, and refuse to compile if the analysis breaks at any point. Python, on the other hand, allows to be compiled and executed without ensuring the type safety first; however it can still be statically analyzed, even without explicit type annotations, using an optional tool such as mypy (which can infer types quite correctly in many cases).
- twhitmore 4y agoEclipse IDE (Java) can run your project, even if it doesn't all compile. I expect this must be fairly common among IDEs with incremental compilers. You can run valid sections & gradually progress the complete correctness. No need for a dynamic language. With static typed language, reliability and refactoring are easy. I'm scripting some small scripts in Python this week but every time I come back to it, I am reminded how not actually very good it is.
- xmcqdpt2 4y agoMost languages do some checks at runtime. This is the strong / weak typing dichotomy (which is different from the static/ dynamic one). For example, Java will throw an exception at runtime if an object is cast (using .asInstanceOf) to an incompatible class. Python throws if you add an integer to a string. Both of those languages are strongly typed. On the other hand, C has no runtime checks whatsoever. If you add a number to an array the program will happily move the array pointer and maybe overflow, but even that won't cause a runtime failure every time.
- BerislavLopac 4y agoThis is precisely my point. In the examples above, the checks are implemented in specific cases, and not universally; so the whole point of static typing is the analysis, and not runtime safety.
- AnimalMuppet 4y ago> What possible advantage is there to be gained from any other choice? I was with you until your last sentence. The world of programming is a lot bigger than your tiny corner of it. I could be working in a safety-critical situation, where preventing errors is more important than developer velocity. Or, I could just have that one annoying co-worker who checks in stuff that "works for him", but always crashes when others try to use it. Or...
- lliamander 4y agoYou can still enforce all of those constraints in your commit hooks or build pipeline, without enforcing them on an individual developer's machine.
- tabtab 4y agoPer #1, why can't it be per module or per routine? Framework-oriented API's we often want "tighter" while domain-centric glue-code can be looser. Per #2, why not a language that facilitates "lint" style inspectors to give a list of warnings, as in "do you really wanna do that?" This reduces the burden of the compiler. Meta-markers can switch certain warnings off so we don't clutter the results for intentional spot looseness.
- lliamander 4y agoI think that if you permit, but don't require, type information, then there is little practical difference between failing typechecking at compile time and merely issuing warnings.
- munificent 4y ago> 1. Do you want the language to require the coder to provide type information (e.g. Java), or permit the coder to provide type information (e.g. Common Lisp) or forbid the coder from providing type information (e.g. early versions of Python, Javascript). Liberties are rarely created and destroyed. Instead, they tend to be trasferred from one place to another. So an equally valid way to phrase this question is from the compiler's perspective: 1. Do you want the compiler to be able to rely on the presence of type information (e.g. Java), or sometimes have access to type information (e.g. Common Lisp) or be forbidden from using type information (e.g. early versions of Python, Javascript). In that equivalent formulation, the value proposition of static types is clearer: When the language specifies that everything has a computable static type, then the compiler can rely on types for useful language features (extension methods, overloading, static dispatch, etc.) and to generate smaller, faster code. Dynamic types give the programmer some liberties (they can not worry about types) but takes some others away (they can't reach certain performance goals or use type-dependent features). Static types do the opposite. Pick your poison.
- habitue 4y agoAdding to this, there's a set of language features which use the type system to generate code. So, things like traits in Rust or Typeclasses in Haskell use the type system to infer which instance is being used and insert a dictionary of trait method implementations as the first argument to your polymorphic function. You can't really do this kind of thing if the compiler can't do `Type -> code` and is only verifying the types are consistent. A pseudocode example is something like: trait ToString for T { fn toString(self: T) -> String; } impl ToString for number { fn toString(self: number) -> String { // implementation here } } impl ToString for Point { fn toString(self: Point) -> String { return f"({self.x.toString()}, {self.y.toString()})"; } } fn printString<T: ToString>(t: T) { print(t.toString()); } Then when it generates code, it uses the inferred type information to generate different code: let p = Point(1.0, 2.0); printString(p); // translated into printString(implToStringForPoint, p); let n = 2.0; printString(n); // translated into printString(implToStringForNumber, n); And then type inference becomes implementation inference, which is really powerful