8 ms·
i have to be totally honest that I understand about 5% of this post nonetheless, I will offer my obviously uninformed opinion on the topic (hey, it's hacker ne
by recursivedoubts 3y ago
i have to be totally honest that I understand about 5% of this post
nonetheless, I will offer my obviously uninformed opinion on the topic (hey, it's hacker news):
a big problem w/ static languages is that the developers of these languages are not willing to compromise type-safety for simplicity: there are very few 80/20 type systems out there that just say "Well, that's too hard to deal w/ you can bail out to unsafe code" when the implementation gets too crazy.
My experience here is with going through the java generics cluster and helping write a statically typed scripting language that integrates w/ it effectively.
As a positive example of what I'm talking about with respect to an 80/20 type system, consider a type system that supported covariant generics + function types that were contravariant wrt their argument types and covariant wrt their return types.
This, I believe, would capture most of the correctness value of types and certainly provide good infrastructure for code completion and tooling, and it would be very simple to implement. It would not, however, be sound, and, thus, would be laughed out of any serious statically typed language discussions.
- FrustratedMonky 3y agoLike auto type conversions. Instead of barfing when assigning a Float to an Int, just auto round? Or but then if assigning a string to a float, then barf. Does this get 80%? I think anything beyond the simple cases, static enforcement is better, or you could be just getting garbage.
- recursivedoubts 3y agono, i don't think that's where the type system implementation complexity usually explodes, in fact that's a more complicated type system than requiring explicit type conversions (which I am usually in favor of, except in cases of toString)
- junon 3y agoAuto round is rarely the operation you want.
- FrustratedMonky 3y agoGuess that is what I am asking. The original post was highlighting it is easier using non-strict types. Like Python. So I was asking, what would that look like, you still loose data, have problems. At least with static checking, the compiler will catch it and then you can add explicate casting.
- JohnFen 3y ago> Instead of barfing when assigning a Float to an Int, just auto round? Rounding involves a loss of data. If an operation would result in data loss, that operation should not be automatic. It should only be possible by the dev intentionally choosing it.
- throwawaymaths 3y agoTechnically float + float can result in data loss.
- JohnFen 3y agoTrue, but that's a case where you're doing it on purpose by choosing to do floating point math, where that's standard behavior. That's a bit different than data loss resulting from an assignment operation, where data loss is not standard behavior.
- Someone 3y ago> Instead of barfing when assigning a Float to an Int, just auto round? Because you can’t really round many floats to Int. A float can be larger than 10^38. If you ‘round’ that to a 64-bit signed integer, you have to return something that’s smaller than 10^19. For doubles, it’s even worse. They can be larger than 10^308. I think that indicates that “just auto round” isn’t an option. The option “if a reasonable conversion exists, make it, otherwise bail out” IMO is a lot worse than having a way to try that conversion and somehow indicate to the programmer when it failed.
- FrustratedMonky 3y agoGuess that is what I am asking. The original post was highlighting it is easier using non-strict types. Like Python. So I was asking, what would that look like, you still loose data, have problems. At least with static checking, the compiler will catch it and then you can add explicate casting.
- bedobi 3y agoNot sure what you're getting at? Even in Haskell you can easily pass data around as untyped maps of maps if you want. But usually you don't. But maybe you're talking more about when generics and obsession with not duplicating any code ever makes that part of the type system needlessly complex
- recursivedoubts 3y agoi'm talking about the type system itself: keeping the type system implementation itself simple rather than just offering an unsafe work-around this means foregoing system-enforceable type safety and features in some cases as the language level, in order to keep the type system fast and understandable to normies.
- ses1984 3y agoThe type system can be understandable to normies while the implementation is not. I would rather have a type system that is correct and consistent, with the complexity hidden away, than to have to know where the boundary is in the 80/20 system, which seems like the perfect place for bugs to enter your code.
- ragnese 3y agoAgreed. Consistent is WAY better for me than inconsistent. One of the reasons I hate TypeScript so much is that it provides type safety except when it doesn't, and it takes a lot of experience to actually track down the MANY (non-obvious) places where it doesn't.
- CodeSgt 3y agoCould you provide some examples of where it doesn’t (assuming strict = true)?
- ragnese 3y agoI could provide a novel, but I'll try to keep it to just a few. Class methods have incorrect variance, even with the 'strictFunctionTypes' setting: https://www.typescriptlang.org/tsconfig#strictFunctionTypes https://www.typescriptlang.org/tsconfig#strictFunctionTypes. What's even more insane is that interface/type methods will have different variance depending on the syntax used to write them: type SafeFoo = { // This will have correct type variance foo: (x: string | number) => void } type UnsafeFoo = { // This will have incorrect type variance foo(x: string | number): void } The `readonly` feature doesn't fully work for object types: type Foo = { foo: number } type ReadonlyFoo = { readonly foo: number } function useFoo(f: ReadonlyFoo) { // f.foo = 4 // compile error const f1: Foo = f f1.foo = 4 // trololol, I just mutated the input even though I promised it was readonly } Classes are kind-of also an interface, which leads to inconsistent/surprising behavior: class Foo {} function useFoo(f: Foo) { if (f instanceof Foo) { console.log('Cool, a Foo.') } else { console.log('Uh, wut?') } } useFoo(new Foo()) // prints: 'Cool, a Foo.' useFoo({}) // prints: 'Uh, wut?' "TypeScript is structurally typed"... except when it's not. In my above example with the class pseudo-interface, you can "fix" it by including a private field in the class: class Foo { #nominal: undefined } function useFoo(f: Foo) { if (f instanceof Foo) { console.log('Cool, a Foo.') } else { console.log('Uh, wut?') } } useFoo(new Foo()) // prints: 'Cool, a Foo.' useFoo({}) // This is now a compile error! Record types are incorrectly typed/handled. A Record<string, string> implies that for ANY string key, there will be a string value. What you almost always actually mean is: Record<string, string | undefined>. Yes, there is a compiler setting (https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAc...) to make all array and record accesses treated as possibly undefined, but that should only be necessary for arrays. The Array type has only one type parameter: the codomain. It has no way of communicating what the domain is (what indexes have values). A Record, on the other hand, DOES allow us to specify both the domain and codomain, so when I write Record<string, string> I'm telling the type system that every string key will be mapped to a string value. Therefore, it should not allow me to assign something that clearly cannot fulfill that contract to it, but it does: const x: Record<string, string> = {} In a type system sense, Record<K, V> is more-or-less equivalent to a function type: (K) => V. Imagine if TypeScript handled actual functions similarly: const x: (s: string) => string = (s) => {} There's more, but like I said, I don't want to spend all day typing...
- dinosaurdynasty 3y agoTypeScript is like this (can just give up and `as any` anything).
- BenoitP 3y agoAn actual positive in my book. If you consider back propagation of constraining types wrt the derivative of developer effort you get smooth gradients. Program works and is correct, but developer gets yelled at by its IDE (IntelliJ does) until there's a proof of it to some degree. I wish that types and proofs were more progressive. For example in Java, why can't we have the compiler tell us what's missing for proving a variable does not escape and we have to wait runtime to see if we had the optimization? Sometimes we'd like to guarantee it, and not just get it opportunistically.
- the_gipsy 3y agoI find that 99% of my `as any` are due to some type definition limitation. The remaining 1% is due to something not expressable in typescript (or not worth it).
- consilient 3y ago> (can just give up and `as any` anything) You should never do this: `any` is infectious. Assert the particular type judgement you want instead: `as number`, `as ({dispatch: () => void})`, etc.
- ramesh31 3y ago>there are very few 80/20 type systems out there that just say "Well, that's too hard to deal w/ you can bail out to unsafe code" when the implementation gets too crazy. This is what I love so much about Objective-C (and superset languages in general, including Typescript). You can play around all you want in ARC world, but drop straight down into C whenever it gets too bloated.
- dkarl 3y ago> there are very few 80/20 type systems out there that just say "Well, that's too hard to deal w/ you can bail out to unsafe code" when the implementation gets too crazy Scala is built on an OO/imperative functional foundation, with powerful enough types to build pure FP constructs on top. The problem with tradeoffs is not that the language won't let you, but in the culture: it's hard to get different Scala programmers to agree on tradeoffs when weighing type safety and FP purity against code complexity.
- ragnese 3y ago> a big problem w/ static languages is that the developers of these languages are not willing to compromise type-safety for simplicity: there are very few 80/20 type systems out there that just say "Well, that's too hard to deal w/ you can bail out to unsafe code" when the implementation gets too crazy. Well, this is the internet, so there will always be someone to disagree with you. In this case, I get to be one of those people for you! Yay, you. :) I find that most statically typed languages do bail out on type safety for simplicity for very many cases. Take arithmetic. Just about every statically typed language allows you to add two `int32`s together to get another `int32` with no acknowledgement in the type system that this operation my fail (overflow/underflow). Some languages will wrap the value around, and some will actually abort, but those are "runtime" decisions and not in the static type systems. Similarly, most languages allow us to compare IEEE float values for equality with no complaints from the type checker. Most languages allow us to index an array and assume that we will always receive a value. It's funny that you mention Java's generics, because they are kind of implemented in the exact 80/20 way you describe. Java's generics are type-erased at compile time, so all of these generic values and functions actually end up working in terms of `Object` (the base/root reference type) and doing runtime casts where needed. The only reason that's possible is because of the JVM's type system allowing for all of that loosey-goosey stuff.
- alcover 3y ago> Some languages will wrap the value around, and some will actually abort What if a language had clamped int and modular mod types ? Overflow on mod would work as advertised and, if you disabled abort, overflow on int would be closer to its virtual value than if wrapped. mod32 m = UINT_MAX+1 //-> 0 int32 i = INT_MAX+1 //-> INT_MAX
- Jtsummers 3y agoAda offers modular types (ranging from 0 to modulus-1 as expected) with the overflow and underflow behavior you want, but not a clamped type as you describe it. So you can get the first but not the second behavior.
- munificent 3y ago> As a positive example of what I'm talking about with respect to an 80/20 type system, consider a type system that supported covariant generics + function types that were contravariant wrt their argument types and covariant wrt their return types. Java and C# are that with respect to array types. Arrays in both are covariant , which is unsound since arrays are mutable, and erroneous uses are checked at compile time. Dart is exactly the language you describe: All generic type parameters are treated covariantly and we use runtime checks to preserve soundness. It's... OK. It works out mostly OK in practice because the generic types that users happen to use variantly are conveniently used in a safely covariant way: Reading stuff out of Iterables and Maps. But there is a significant performance cost to the runtime checks needed to preserve soundness. And when you using a type in a covariant way incorrectly, it is deeply confusing to users. I think the right 80/20 approach is to do what C# 2.0 and just make all generics invariant.
- Someone 3y ago> a big problem w/ static languages is that the developers of these languages are not willing to compromise type-safety for simplicity: there are very few 80/20 type systems out there that just say "Well, that's too hard to deal w/ you can bail out to unsafe code" when the implementation gets too crazy. Very few? Many languages allow you to cast references to other types at will. C# does one better with its ‘dynamic’ keyword, which makes it much clearer where you are using runtime method lookup. https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/interop/using-type-dynamic https://learn.microsoft.com/en-us/dotnet/csharp/advanced-top...: “The dynamic type is a static type, but an object of type dynamic bypasses static type checking. In most cases, it functions like it has type object. The compiler assumes a dynamic element supports any operation” I think Swift copied that. https://docs.swift.org/swift-book/documentation/the-swift-programming-language/declarations/ https://docs.swift.org/swift-book/documentation/the-swift-pr...: “When you mark a member declaration with the dynamic modifier, access to that member is always dynamically dispatched using the Objective-C runtime.“
- brabel 3y agoAdd Dart to your list.
- KaiserPro 3y agoI do like c#'s dynamic and unsafe bits. it clearly telegraphs that someone is doing something either clever, stupid, desperate or all three.
- Akronymus 3y agoF# goes even further than c# and allows you to embed IL. Which allows you to do really fun things.
- akkad33 3y agoI'm very curious about F#. Can you give some code examples of how this allows dynamic typing?
- 3y ago
- seanmcdirmid 3y agoTypeScript is obviously one of those 80/20 languages, and I like programming in it accordingly. But fighting the type system is one of the easier tasks a programmer could spend their time on if reliability was significantly improved via type safety. For example, null pointer static checking is a bit of a pain but has a significant pay off that probably makes it worth it.
- xwowsersx 3y agoInteresting. I think that many statically typed languages bail out too early! That's obviously a subjective view, but I suspect there's a large enough group of people who feel this way which is why much stricter and more unforgiving static type systems are developed and popularized.
- deleted 3y ago[deleted]