9 ms·
I like this. Very much falls into the "make bad state unrepresentable". The issues I see with this approach is when developers stop at this first level of type
by pclowes 1y ago
I like this. Very much falls into the "make bad state unrepresentable".
The issues I see with this approach is when developers stop at this first level of type implementation. Everything is a type and nothing works well together, tons of types seem to be subtle permutations of each other, things get hard to reason about etc.
In systems like that I would actually rather be writing a weakly typed dynamic language like JS or a strongly typed dynamic language like Elixir. However, if the developers continue pushing logic into type controlled flows, eg:move conditional logic into union types with pattern matching, leverage delegation etc. the experience becomes pleasant again. Just as an example (probably not the actual best solution) the "DewPoint" function could just take either type and just work.
- arrowsmith 1y agoFYI: Ruby is strongly typed, not loosely. > 1 + "1" (irb):1:in 'Integer#+': String can't be coerced into Integer (TypeError) from (irb):1:in '<main>' from <internal:kernel>:168:in 'Kernel#loop' from /Users/george/.rvm/rubies/ruby-3.4.2/lib/ruby/gems/3.4.0/gems/irb-1.14.3/exe/irb:9:in '<top (required)>' from /Users/george/.rvm/rubies/ruby-3.4.2/bin/irb:25:in 'Kernel#load' from /Users/george/.rvm/rubies/ruby-3.4.2/bin/irb:25:in '<main>'
- kitten_mittens_ 1y agoThere seem to be two competing nomenclatures around strong/weak typing where people mean static/dynamic instead.
- jnpnj 1y agoyes, untyped names != untyped objects
- josephg 1y agoSome people mistakenly call dynamic typing "weak typing" because they don't know what those words mean. PSA: Static typing / dynamic typing refers to whether types are checked at compile time or runtime. "Static" = compile time (eg C, C++, Rust). "Dynamic" = runtime (eg Javascript, Ruby, Excel) Strong / weak typing refers to how "wibbly wobbly" the type system is. x86 assembly language is "weakly typed" because registers don't have types. You can do (more or less) any operation with the value in any register. Like, you can treat a register value as a float in one instruction and then as a pointer during the next instruction. Ruby is strongly typed because all values in the system have types. Types affects what you can do. If you treat a number like its an array in ruby, you get an error. (But the error happens at runtime because ruby is dynamically typed - thus typechecking only happens at runtime!).
- 0x457 1y agoIt's strongly typed, but it's also duck typed. Also, in ruby everything is an object, even the class itself, so type checking there is weird. Sure it stops you from running into "'1' + 2" issues, but won't stop you from yeeting VeryRawUnvalidatedResponseThatMightNotBeAuthorized to a function that takes TotalValidatedRequestCanUseDownstream. You won't even notice an issue until: - you manually validate - you call a method that is unavailable on the wrong object.
- arrowsmith 1y agoYou just described why I fell out of love with Ruby.
- 0x457 1y agoBecause you wanted to do 1 + '1'?
- deleted 1y ago[deleted]
- ameliaquining 1y agoI recall a type theorist once defined the terms as follows (can't find the source): "A strongly typed language is one whose type system the speaker likes. A weakly typed language is one whose type system the speaker dislikes." Related Stack Overflow post: https://stackoverflow.com/questions/2690544/what-is-the-difference-between-a-strongly-typed-language-and-a-statically-typed https://stackoverflow.com/questions/2690544/what-is-the-diff... So yeah I think we should just give up these terms as a bad job. If people mean "static" or "dynamic" then they can say that, those terms have basically agreed-upon meanings, and if they mean things like "the type system prohibits [specific runtime behavior]" or "the type system allows [specific kind of coercion]" then it's best to say those things explicitly with the details filled in.
- dpryden 1y agoI think you might be thinking of https://cdsmith.wordpress.com/2011/01/09/an-old-article-i-wrote/ https://cdsmith.wordpress.com/2011/01/09/an-old-article-i-wr... It says: > I give the following general definitions for strong and weak typing, at least when used as absolutes: > Strong typing: A type system that I like and feel comfortable with > Weak typing: A type system that worries me, or makes me feel uncomfortable
- johnfn 1y agoirb(main):001:0> a = 1 => 1 irb(main):002:0> a = '1' => "1" It doesn't seem that strong to me.
- dgb23 1y agoIn the dynamic world being able to redefine variables is a feature not a bug (unfortunately JS has broken this), even if they are strongly typed. The point of strong typing is that the language doesn't do implicit conversions and other shenanigans.
- Spivak 1y agoWell yeah, because variables in what you consider to be a strongly typed language are allocating the storage for those variables. When you say int x you're asking the compiler to give you an int shaped box. When you say x = 1 in Ruby all you're doing is saying is that in this scope the name x now refers to the box holding a 1. You can't actually store a string in the int box, you can only say that from now on the name x refers to the string box.
- deleted 1y ago[deleted]
- 9rx 1y agoThe types are strong. The variables are weak.
- folkrav 1y agoIt would be weak if that was actually mutating the first “a”. That second declaration creates a new variable using the existing name “a”. Rust lets you do the same[1]. [1] https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing https://doc.rust-lang.org/book/ch03-01-variables-and-mutabil...
- johnfn 1y agoRust lets you do the same because the static typing keeps you safe. In Rust, treating the second 'a' like a number would be an error. In ruby, it would crash.
- pclowes 1y agoOops, I meant weakly typed as in JS or strongly typed as in Ruby. But decided to switch the Ruby example to Elixir and messed up the sentence
- js2 1y agoGood luck with this fight. I've had it on HN most recently 7 months ago, but about Python: https://news.ycombinator.com/item?id=42367644 https://news.ycombinator.com/item?id=42367644 A month before that: https://news.ycombinator.com/item?id=41630705 https://news.ycombinator.com/item?id=41630705 I've given up since then.
- arrowsmith 1y agoI already wrote about wrt Elixir: https://arrowsmithlabs.com/blog/elixir-is-dynamically-and-strongly-typed https://arrowsmithlabs.com/blog/elixir-is-dynamically-and-st...
- 9rx 1y agoWe've been reading comments like that since the internet was created (and no doubt in books before that). Why give up now?
- josephg 1y agoYep. For this reason, I wish more languages supported bound integers. Eg, rather than saying x: u32, I want to be able to use the type system to constrain x to the range of [0, 10). This would allow for some nice properties. It would also enable a bunch of small optimisations in our languages that we can't have today. Eg, I could make an integer that must fall within my array bounds. Then I don't need to do bounds checking when I index into my array. It would also allow a lot more peephole optimisations to be made with Option. Weirdly, rust already kinda supports this within a function thanks to LLVM magic. But it doesn't support it for variables passed between functions.
- steveklabnik 1y agoIn my understanding Rust may gain this feature via “pattern types.”
- ijustlovemath 1y agoWhere can I sign?
- steveklabnik 1y agoGonna be honest, at this point, I don't know how this all works in the Rust Project. https://github.com/rust-lang/types-team/issues/126 https://github.com/rust-lang/types-team/issues/126
- mcculley 1y agoAda has this ability to define ranges for subtypes. I wish language designers would look at Ada more often.
- tylerhou 1y agoAcademic language designers do! But it takes a while for academic features to trickle down to practical languages—especially because expressive-enough refinement typing on even the integers leads to an undecidable theory.
- MoreQARespect 1y agoI've recently been following red-green-refactor but instead of with a failing test, I tighten the screws on the type system to make a production-reported bug cause the type checker to fail before making it green by fixing the bug. I still follow TDD-with-a-test for all new features, all edge cases and all bugs that I can't trigger failure by changing the type system for. However, red-green-refactor-with-the-type-system is usually quick and can be used to provide hard guarantees against entire classes of bug.
- pclowes 1y agoI like this approach, there are often calls for increased testing on big systems and what they really mean is increased rigor. Don't waste time testing what you can move into the compiler. It is always great when something is so elegantly typed that I struggle to think of how to write a failing test. What drives me nuts is when there are testing left around basically testing the compiler that never were “red” then “greened” makes me wonder if there is some subtle edge case I am missing.
- eyelidlessness 1y agoAs you move more testing responsibilities to the compiler, it can be valuable to test the compiler’s responsibilities for those invariants though. Otherwise it can be very hard to notice when something previously guaranteed statically ceases to be.
- eyelidlessness 1y agoI found myself following a similar trajectory, without realizing that’s what I was doing. For a while it felt like I was bypassing the discipline of TDD that I’d previously found really valuable, until I realized that I was getting a lot of the test-first benefits before writing or running any code at all. Now I just think of types as the test suite’s first line of defense. Other commenters who mention the power of types for documentation and refactoring aren’t wrong, but I think that’s because types are tests… and good tests, at almost any level, enable those same powers.
- kazinator 1y agoAlso known as "make bad state unexperimentable".
- balamatom 1y agoHahaha. Nice coinage! Funny they set out to do the former then we have to deal with how they've achieved the later
- jevndev 1y agoThe “Stop at first level of type implementation” is where I see codebases fail at this. The example of “I’ll wrap this int as a struct and call it a UUID” is a really good start and pretty much always start there, but inevitably someone will circumvent the safety. They’ll see a function that takes a UUID and they have an int; so they blindly wrap their int in UUID and move on. There’s nothing stopping that UUID from not being actually universally unique so suddenly code which relies on that assumption breaks. This is where the concept of “Correct by construction” comes in. If any of your code has a precondition that a UUID is actually unique then it should be as hard as possible to make one that isn’t. Be it by constructors throwing exceptions, inits returning Err or whatever the idiom is in your language of choice, the only way someone should be able to get a UUID without that invariant being proven is if they really *really* know what they’re doing. (Sub UUID and the uniqueness invariant for whatever type/invariants you want, it still holds)
- munificent 1y ago> This is where the concept of “Correct by construction” comes in. This is one of the basic features of object-oriented programming that a lot of people tend to overlook these days in their repetitive rants about how horrible OOP is. One of the key things OO gives you is constructors. You can't get an instance of a class without having gone through a constructor that the class itself defines. That gives you a way to bundle up some data and wrap it in a layer of validation that can't be circumvented. If you have an instance of Foo, you have a firm guarantee that the author of Foo was able to ensure the Foo you have is a meaningful one. Of course, writing good constructors is hard because data validation is hard. And there are plenty of classes out there with shitty constructors that let you get your hands on broken objects. But the language itself gives you direct mechanism to do a good job here if you care to take advantage of it. Functional languages can do this too, of course, using some combination of abstract types, the module system, and factory functions as convention. But it's a pattern in those languages where it's a language feature in OO languages. (And as any functional programmer will happily tell you, a design pattern is just a sign of a missing language feature.)
- lock1 1y ago
- deleted 1y ago[deleted]
- reactordev 1y agoUnion types!! If everything’s a type and nothing works together, start wrapping them in interfaces and define an über type that unions everything everywhere all at once. Welcome to typescript. Where generics are at the heart of our generic generics that throw generics of some generic generic geriatric generic that Bob wrote 8 years ago. Because they can’t reason with the architecture they built, they throw it at the type system to keep them in line. It works most of the time. Rust’s is beautiful at barking at you that you’re wrong. Ultimately it’s us failing to design flexibility amongst ever increasing complexity. Remember when “Components” where “Controls” and you only had like a dozen of them? Remember when a NN was only a few hundred thousand parameters? As complexity increases with computing power, so must our understanding of it in our mental model. However you need to keep that mental model in check, use it. If it’s typing, do it. If it’s rigorous testing, write your tests. If it’s simulation, run it my friend. Ultimately, we all want better quality software that doesn’t break in unexpected ways.
- valenterry 1y agounion types are great. But alone they are not sufficient for many cases. For example, try to define a datastructure that captures a classical evaluation-tree. You might go with: type Expression = Value | Plus | Minus | Multiply | Divide; interface Value { type: "value"; value: number; } interface Plus { type: "plus"; left: Expression; right: Expression; } interface Minus { type: "minus"; left: Expression; right: Expression; } interface Multiply { type: "multiply"; left: Expression; right: Expression; } interface Divide { type: "divide"; left: Expression; right: Expression; } And so on. That looks nice, but when you try to pattern match on it and have your pattern matching return the types that are associated with the specific operation, it won't work. The reason is that Typescript does not natively support GADTs. Libs like ts-pattern use some tricks to get closish at least. And while this might not be very important for most application developers, it is very important for library authors, especially to make libraries interoperable with each other and extend them safely and typesafe.
- tossandthrow 1y agoThis can usually be alleviated by structural types instead of nominal types. You can always enforce nominal types if you really need it.
- atoav 1y agoAs you mentioned correctly: if you go for strongly typed types in a library you should go all the way. And that means your strong type should provide clear functions for its conversion to certain other types. Some of which you nearly always need like conversion to a string or representation as a float/int. The danger of that is of course that you provide a ladder over the wall you just built and instead of temperature_in_f = temperature_in_c.to_fahrenheit() They now go the shortcut route via numeric representation and may forget the conversion factor. In that case I'd argue it is best to always represent temperature as one unit (Kelvin or Celsius, depending on the math you need to do with it) and then just add a .display(Unit:: Fahrenheit) method that returns a string. If you really want to convert to TemperatureF for a calculation you would have to use a dedicated method that converts from one type to another. The unit thing is of course an example, for this finished libraries like pythons pint (https://pint.readthedocs.io/en/stable/ https://pint.readthedocs.io/en/stable/) exist. One thing to consider as well is that you can mix up absolute values ("it is 28°C outside") and temperature deltas ("this is 2°C warmer than the last measurement"). If you're controlling high energy heaters mixing those up can ruin your day, which is why you could use different types for absolutes and deltas (or a flag within one type). Datetime libraries often do that as well (in python for example you have datetime for absolute and timedelta for relative time)