12 ms·
I love static typing/type hints if for only 1 thing - code maintenance. Even code I wrote six months ago. Not having to dig through 6 functions deep to try to
by david422 5y ago
I love static typing/type hints if for only 1 thing - code maintenance.
Even code I wrote six months ago.
Not having to dig through 6 functions deep to try to figure out whether "person" is a string, or an object, and if it's an object what attributes it has on it etc. is huge. And not to mention that some clever people decide - hey, if you pass a string I'll look up the person object - so you can pass an object or a string - which makes all sorts of convoluted code paths when someone else was looking at "person" and only saw one type so now their function doesn't work on both types etc.
I hate having to waste time figuring out the type of every variable and hold it in my head every single time I read a piece of code.
- sseagull 5y agoThis is absolutely how I feel. I've mentioned previously taking over a project, and just not knowing the type of anything took me months to overcome. Also, type hints really help your IDE, even catching errors before you even run tests. There's also a visual cue that you are doing something wrong: If a function returns 4 levels of Union[Tuple[List[int]], Optional[str]........ Then you are doing something too complex and the function should be broken up.
- jfabre 5y agoI never understood this argument. In what kind of shop are you working that passing a string named person to a method expecting an object is tolerated. Or even passing different types that don't share a common interface. This would never fly in a code review in any of the companies I've worked for.
- kennywinker 5y agoI've seen essentially this code in so many organically grown codebases (when they grew up without types). It's usually close the the UI, because someone had to quickly add an alternate path to support some new user interaction function find_user(person) { if user is string { query_by_name(person) } else { query_by_name(person.name) } } and yeah, we all know it's kinda messy, but also that logic has to live somewhere and we need this feature asap so it passes code review. I wrote a test for it, ship it.
- freemint 5y agoSounds like a brilliant case for multiple-dispatch.
- kennywinker 5y agoRight so we have: function find_user(person: string) and also: function find_user(person: Object) how long before someone writes this: find_user(person: { name: "dave" }) meanwhile, someone else, not suspecting that they'll be handed a weird half-formed `User` object adds `person.id` somewhere in the body of the Object version of `find_user` and now we have a weird edge-case where very rarely `find_user` panics because the user object we're handed doesn't have an id??? Great, I just lost an hour trying to dig that out of the logs, and the users are starting to think of the product as flakey because the bug has been in prod for over a month before we finally believed them enough to look into it. Just. Use. Types. Multiple dispatch won't save you on its own. You NEED compile-time types.
- david422 5y agoSomebody downvoted you, I'm guessing because they think this is a silly example and have never actually seen something like this. I have, in a production code base.
- freemint 5y agoMultiple dispatch and compile time times are not exclusive at all.
- kennywinker 5y agoI'm saying the problem isn't solved by multiple dispatch alone, but it is solved by compile time types alone. You can use both together, of course.
- freemint 5y agoThe issue whether the language is interpreted or compildd (which would distinguish compile time types from strong types) is in my opinion completely orthogonal to the issue of how dispatch works. Strong types and multiple dispatch fix the issues I see even in an interpreted language.
- elzbardico 5y agoThis was probably just a silly example for a quick explanation. But all it takes is a method that expects an integer Id to receive a string representation of said id because of some obscure path in code that notwithstanding your 100% line coverage the team is so proud of, was never exercised on tests because nobody can have 100% branch coverage
- cyral 5y agoIf only there was a way to enforce these parameter types automatically
- dec0dedab0de 5y agoI personally love it, and wish every library worked this way. My argument is why go out of my way to make it not work, when it would be easy to make it work. This is because I think of modules/packages as user facing programs that are easy to tie together, instead of simple building blocks. What I really wish existed was a built in way to cast and validate, or normalize and validate. I never care if something is a string. I care that if I wrap it in str(), or use it in a fstring, the result matches a regex. Or if I run a handful of functions one of them returns what I need. The only benefit I can see of type hints on their own is it makes it easy to change a callable's signature, but I think that's best avoided to begin with.
- nybble41 5y ago> why go out of my way to make it not work, when it would be easy to make it work The problem the DWIM approach to APIs is that when you go out of your way to "do something reasonable" with absolutely any kind of argument type, leaving the caller's intent implicit, you will sometimes run into combinations that "work" in unexpected—and often unwanted—ways. For example, say you have a function which returns either a Person object or, in very rare cases, an error string. Moreover, you fail to check for the error string, and pass the result into another function which expects a Person object but will also take a name and look up the corresponding Person object in a table. Now if the first function fails you're left trying to look up an error string as a name, with no obvious signs (such as a type mismatch error) to show that anything is amiss. It's important to make the intent explicit, and not just let the function guess. One option compatible with both statically- and dynamically-typed languages is to provide two functions, one requiring a Person object and another taking a name string. This is still perfectly ergonomic for the user and mitigates most of the potential for confusion.
- dec0dedab0de 5y agoFor example, say you have a function which returns either a Person object or, in very rare cases, an error string. Moreover, you fail to check for the error string, and pass the result into another function which expects a Person object but will also take a name and look up the corresponding Person object in a table. Now if the first function fails you're left trying to look up an error string as a name, with no obvious signs (such as a type mismatch error) to show that anything is amiss. Well I only ever return one type from a function, I'm not a total madman. Sometimes I'll do one type or a None, if I'm trying to replicate the functionality of dict.get(). Any error string would be within an Exception, so that wouldn't be an issue, but even in your example it would show a stack trace to the function looking up the user, and would be much more valuable to troubleshoot than a type mismatch. One option compatible with both statically- and dynamically-typed languages is to provide two functions, one requiring a Person object and another taking a name string. This is still perfectly ergonomic for the user and mitigates most of the potential for confusion. In practice that is usually what I end up doing, but with a 3rd function that takes either and returns a Person object. In this particular case I would probably make the function be a method on the Person object, and have a class method to look up the Person. Here is the scenario that annoyed me enough to turn me off static typing. I had a class that stored the IP address of a network device as an ipaddr.IPAddress object (now ipaddress in the standard library) and there were various subclasses for specific device types. One of the device types needed an SDK, and the init for the SDK class looked something like this def __init__(self, host, port=1234, scheme='https'): if not isinstance(ip, str): raise TypeError('invalid host') self.url = f"{scheme}://{host}:{port}" If they didn't check the type it would have worked fine. Just like every other library we were using to connect to devices. So after a bit of frustration we changed our base class def original__init__(self, ip_address): self.ip_address = ipaddr.IPAddress(ip_address) def new__init__(self, ip_address): ipaddr.IPAddress(ip_address) # just to validate self.ip_address = ip_address and all was well with the world, but there was a dumb mistake waiting for us. A year or two later, after upgrading to 2.7 we started passing around unicode objects instead of strings to get ready for 3.x, as was the style at the time. Again that SDK broke, and only that SDK, because it insisted on checking the type. Sure it was our mistake this time for not having the original fix to be just casting it to str right before passing it to the SDK, but it was annoying and should have been unnecessary. I understand that type hints are much better in this regard because it would only show an error in your tooling. But that brings me to another point. I write my packages/classes/modules to mostly be used in a web app, or as scripts that run on a schedule. However, I also need to be able to write one-offs very quickly. When that happens my code that was previously a library for different applications, now becomes an application itself. Using the REPL, a jupyter notebook, or bpython, I will need to quickly get something done. In these scenarios I don't want to waste time remembering how to normalize the data being given to me. Especially If the code that provides such niceties is tucked away at a higher level for end users of the web app. Like I said, I tend to just make a lookup function, and then have everything else be methods on the object. But that doesn't really help when it's parameters to a function. I really don't know what would make it better. Perhaps some kind of mix between function overloading and interfaces from other languages, and the magic *_validate() methods that Django uses. Maybe instead of type hints for return values we need value hints, that give an idea of what actual objects might look like. Then tooling could take into account if it would still work after validation and normalization. Of course it could be that there is no elegant and reliable way to do what I really want, but I can dream.
- dboreham 5y agoYou'd think. But I've seen many many many examples of this pattern in production JS code.
- tialaramex 5y agoIn C++ you're only ever one missing "explicit" from introducing such problems. Suppose I call fire(bob). Programmers from other languages might reason that since fire is a function which takes a Person, bob must be a Person. Not in C++. In C++ the compiler is allowed to go, oh, bob is a string and I can see that there's a constructor for Person which takes a string as its only argument, therefore, I can just make a Person from this string bob and use that Person then throw it away. To "fix" the inevitable cascade of misery caused by this "feature" C++ then introduces more syntax, an "explicit" keyword which means "Only use this when I actually ask you to" rather than as a sane person might, requiring an implicit keyword to flag any places you actually want this behaviour to just silently happen. This way, hapless, lazy or short-sighted programmers cause the maximum amount of harm, very on-brand for C++. See also const.
- madeofpalk 5y agoWe have tests, and static types, because developers are people and people make mistakes. You can't say "we simply don't allow bugs!" because it's a lie. Why rely on a another person manually checking for silly mistakes when the computer can do it for you?
- wvenable 5y agoThe main argument for dynamic typing is speed in prototyping but I find that's opposite for me. I'm much more comfortable rapid prototyping and ripping stuff apart when I have a strongly static typed environment telling me what I just broke. Doing radical refactoring often involves just making those changes and then fixing all the IDE or compiler errors until it runs again.
- tabbott 5y agoI would argue that the really big benefit of dynamic typing is that it enables a really nice interactive interpreter shell experience. I think it's also important from a prototyping standpoint that Python's static typing model does a lot of inference -- you don't have to add an explicit type annotation on every single variable.
- nyberg 5y agoThis is possible with statically typed languages with Haskell being one of the examples where it's encouraged to use the REPL to work towards a solution and/or qucikly test ideas without having to write unit tests or full program tests. Ocaml and friends fall within this category too and none require extensive type annotation due to type inference. I feel that too much focus is on static languages like C/C++ where types become a chore and judging it on that rather than looking at the plenty of languages with type inference brought by ML-style languages.
- BeFlatXIII 5y ago> languages like C/C++ where types become a chore It's been a long while since I used those languages, but I remember the chore part wasn't so much of typing Int or String and more so having to care if it's an Int, Short, or Long or if the float is single or double precision. I believe that those micro-optimizations are no longer popular, but manually thinking low-level is not something I enjoy.
- loup-vaillant 5y ago> I would argue that the really big benefit of dynamic typing is that it enables a really nice interactive interpreter shell experience. I use the Python REPL quite often, and have non trivial experience with Lua’s. But the best experience I’ve ever got was with OCaml: I type the expression, or function definition, without giving type annotations, and I get the type of the result in the response. You wouldn’t believe the number of bugs I caught just by looking at the type. Before I even start testing the function. And that’s when I don’t have an outright type error, which a dynamic language wouldn’t have caught — not before I start testing anyway.
- handrous 5y ago> I hate having to waste time figuring out the type of every variable and hold it in my head every single time I read a piece of code. If a codebase doesn't have static types, it damn well better be set up to be highly grep-able. Including dependencies and frameworks. This is why Rails pisses me off so much. No static types to help you out, and you can't grep (can barely google, even!) methods and properties that aren't defined anywhere until runtime. Is this from core? Is it from some 3rd party gem? Well fuck me, this file doesn't even tell me which gems it's relying on, so it could be literally anything in the entire goddamn dependency tree.
- rightbyte 5y ago> ... grepable ... This is so important. It is also the reason why I like global variables. They are accused of making a spaghetti mess but ... in my experience the opposite is true. Fancy patterns are way worse to reverse engineer than simple flat long functions accessing globals. Easy to debug too!
- mewpmewp2 5y agoI agree with that. I despise all the DI things where I can't "goto" to the definition of the actual dependency that was injected, but only to the interface. So frustrating. It makes understanding what is going on so difficult for me.
- blacktriangle 5y agoThat's some Rails stupidity there, not a dynamic language problem. Autoloading symbols by name is straight up dumb. As for greppable though...then you may as well be using a static language. The point of a dynamic language is to be dynamic, ie you can do those things at runtime.
- nitrogen 5y agoThe point of a dynamic language is to be dynamic, ie you can do those things at runtime. With Rails you have the option of pry-rails, and you can get a list of descendants of important parent classes like ActiveRecord with this: https://apidock.com/rails/Class/descendants https://apidock.com/rails/Class/descendants With the combination of vim, rspec, pry, fzf, and ripgrep, it's possible to become quite comfortable refactoring pure Ruby and Ruby+Rails code. But it does take some time to learn how to navigate the Rails runtime code generation magic. The more magic the code, the more you might have to use a debugger to break on method definition, but Ruby's dynamicism lets you do that. On the topic of frameworks with a lot of magic, having used both Rails and Spring Boot (with Java and Kotlin), I'll take Rails any day. It was way easier to introspect Rails codegen magic with Pry, than Spring's codegen magic with IntelliJ. With Spring Boot, even with Kotlin, we had the burden of semi-manual typing, but lost a lot of the benefits because a lot of DB interaction and API payload handling was still only runtime checked.
- layer8 5y ago> I hate having to waste time figuring out the type of every variable and hold it in my head every single time I read a piece of code. For the same reason, I’m not a fan of type-inferring variable declarations.
- dmart 5y agoYeah, when writing type inference is obviously nice, but it can be annoying to try to go back and read. I think the best experience is having a language server annotate the inferred types (like how rust-analyzer does it.) But even then, it can become hard to read code on GitHub or somewhere where tools are not available. Granted that's becoming less and less of a problem, and even GitHub allows using some VS Code extensions now.
- layer8 5y agoWith good IDE support, writing types isn’t that much of a burden. Either write a function call first and use "assign return value to new variable", or use autocompletion where you only type the initials of a multi-word type name. Plus IDE refactoring actions when a parameter or return type needs to be changed.
- lamontcg 5y agoI'm okay with "var = new FooBarBazThingyWithALongName()" because I don't need to see the type name twice there. In an IDE you can get the type annotation from the IDE over every inferred var type, but I don't like requiring an IDE to see that information and like it showing up in 'less' as well.
- layer8 5y agoI agree it's redundant if the type name occurs twice in the same statement. However, further evolution of the code often causes the instantiation to be moved elsewhere, and I wouldn't have confidence that the one doing that change then also changes `var` back to the type name. Instead, it would be nice to have syntax avoiding the duplication in the fashion of `FooBarBazThingyWithALongName thingy = new(...constructor parameters...);`.
- tus89 5y agoYay -- maintaining type definitions for arbitrarily complex nested dicts/arrays that are often built up when processing data and in all manner of different ways. You know why "Any" appears in Typescript so much? I doubt anyone here is an actual programmer, if you were you would understand that having to define type definitions for "objects" that are often just parsed from JSON API responses or constructed through the processing of data is the biggest pain in all languages, and why typeless languages are convenient and popular for this kind of work. Oh and then the API response changes slighty, breaking your type definition, whereas typeless Python keep chugging along because it is very tolerant of small changes to underlying data, especially if written in a defensive manner. If you want types, why are you using Python at all? Go write bloated Java/.NET that takes a team of 10 six months to put together a simple app that would be written in week in Python.
- angelzen 5y agoThis is doubly true as experienced programmers argue that designing the data structures is the hardest part of coding. Code follows semi-automatically.
- nitrogen 5y agoI'd add data flows as another level above data structures. It helps to think about how data flows into, through, and out of a system, then it's more clear how the data needs to be packaged, and from there, the code follows semi-automatically. Tangentially related: I think it'd be cool if there was a development environment that combined a node-based dataflow editor with normal text editing, so pure plumbing could be implemented visually, but embedded within (and translated to) textual code.
- karmakaze 5y agoI learned the same thing on a project that was using Java 1 non-generics. Not exactly untyped to typed but an analogous experience. Everyone I asked said that it was too big to do. I started anyway by enabling the warnings for nongeneric use. I turned down the reporting limit to 1000 (I think) so as not to be discouraged. After months and months of incremental work alongside my main work, I got under the 1000 warnings. It got a bit trickier after that. In the end, there was exactly 1 bug, where an object.toString was being added to a dropdown box and we'd see it from time to time as Class@hexhash. What I learned then is that it isn't strictly about the bugs, it's the confident way you can navigate the codebase and understand and add in consistent ways. Now I add types to all my Ruby and it's seems normal again.
- BeFlatXIII 5y ago> some clever people decide - hey, if you pass a string I'll look up the person object - so you can pass an object or a string - which makes all sorts of convoluted code paths Do you have hints on how to avoid being one of those 10x clever programmers while programming a prototype? I find that I am most likely to write functions like that when there's some variables that I don't want to pass 5 layers down the call stack and then, in your example, would accept either a string (in which case those variables use their default values) or the Person object, where the variables are pulled from the Person's attributes.
- david422 5y agoI don't really, but I guess I could say that I have developed in statically typed languages and dynamically typed languages (professionally) for over a decade and I've always found that using the "power" of dynamic languages always ends up causing (me) more frustration in the long run- basically classes of bugs or time wasted that simply doesn't occur with statically typed languages. So for me, I tend to spend a little more time up front to try not to waste (my) time in the future. > I find that I am most likely to write functions like that when there's some variables that I don't want to pass 5 layers down the call stack I agree for a prototype, there are some tradeoffs to be made. However, very often prototypes can end up becoming production. Temporary decisions often become permanent ones. Just something to keep in mind.
- didibus 5y agoI've been working in Clojure for the last few years, and what I learned is that the trick is to reverse the data dependencies, so that instead of your function asking: "What is a "person" and what attributes does it have if an object?". You have your function declaring: "I take a person as a map of keys :name and :age". And it is the caller who needs to ask itself: "What am I supposed to provide to this function?" This is a very different mindset, but once you adopt this style, the lack of static types isn't as big an issue. The reason you can do this in a dynamic language is that you can very easily adapt one structure to another, so its okay if not all your functions work directly on the same shared structures. It also has the advantage that this style really favors making modular independent granular components that can be reused easily, because they aren't coupled to an application's shared domain structures, but to their own set of structures, creating a natural sub-domain. There are other aspects to make this style work well, like keeping call-stacks shallow, and having a well defined domain model at the edge of your app with good querying capabilities for it. Concretely it means say you need to add some feature X to the code, you might think, ok this existing function is one place where I could add the behavior, but for my new feature I need to have :age of "person", but I don't know if the "person" argument of this existing function would contain :age or not. Dammit, I wish I had static types to tell me. Well, in this scenario, instead, what you do is that you don't add the behavior to that function. Instead, in my style you would have: A -> B A -> C instead of: A -> B -> C That means if after B is the right place for your logic, you don't do: A -> B -> B' -> C And hope that the "person" passed to B had the :age key which is needed by B'. Instead you would do: A -> B A -> B' A -> C And when you implement B', you don't even care about "person", you can just say you need person-age, or that you need a Person object with key :age (which you don't care if it is the Person object shared in other places or not). Finally, you modify A, where A was the function that creates the Person object in the first place, it has direct access to your actual database/payload and so finding whatever data you need is trivial in it.
- blacktriangle 5y agoI would argue that any function that branches on argument type is straight up doing dynamic typing wrong. Well branching may not be the right word. Something resembling pattern maching is fine, but like you say having a function that takes a string for lookup OR the object is just a disaster, particularly when you start stacking function calls. Dynamic types should closer resemble things that all share an interface, not totally different representations of that data based on the shape of your code. Javascript is by far the worst offender here with its ignoring extra arguments. Javascript functions that totally change effective type signatures based on number of args are the devil's work. I'd argue that if the types that a function accepts are not easily defineable than you're doing dynamic typing wrong.