Hacker Newsnew | past | comments | ask | show | jobs | submit | dandotway's commentslogin

> The most philosophically and mathematically consistent interpretation of Quantum Mechanics is that there are "many worlds".

Why is "many worlds" the most mathematically consistent? (I won't bother asking for the philosophical bit; I'm sure it's too long for an HN post.)


Atheists: "We have faith natural selection at a cosmic level makes life way more likely."

Christians: "We have faith supernatural selection at a cosmic level makes afterlife way more likely."


Question for JIT experts: JS and Python are extremely hard to optimize because they both allow redefining anything at any time, yet V8 crushes Python by an order of magnitude in many benchmarks[1]:

         All times in seconds (lower is better)
  
  benchmark          Node.js     Python 3    Py takes x times longer
  ==================================================================
  regex-redux          5.06         1.34     ** Py3 is faster (PCRE C)
  pidigits             1.14         1.16     0.02
  reverse-complement   2.59         6.62     2.56
  k-nucleotide        15.84        46.31     2.92
  binary-trees         7.13        44.70     6.27
  fasta                1.91        36.90     19.3
  fannkuch-redux      11.31       341.45     30.19 (wut?)
  mandelbrot           4.04       177.35     43.9  (srsly?)
  n-body               8.42       541.34     64.3  (no numpy fortran cheat?)
  spectral-norm        1.67       112.97     67.65 (Python for Science[TM])

(If Python is allowed to call fast C code (PCRE) for regex-redux, I don't see why Python shouldn't be allowed to call fast Fortran BLAS/etc for n-body, but rules are rules, I guess. V8 doesn't cheat at spectral-norm, it's 100% legit JS.)

Both ecosystems have billions invested by corporations worth trillions; bottomless money exists to make Python faster. So why isn't Python faster?

V8's tactics include dynamically watching which loops/calls get run more than (say) 10,000 times and then speculatively generating[2] native machine instructions on the assumption the types don't change ("yep, foo(a,b) is only called with a and b both float64, generate greased x86-64 fastpath"), but gracefully falling back if the types later do change ("our greased x86-64 'foo(float64,float64)' routine will be passed a string! Fall back to slowpath! Fall back!"). Why doesn't Python do this? Is it because Google recruited the only genius unobtainium experts who could write such a thing? Google is a massive Python user, too.

[1] https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

[2] https://ponyfoo.com/articles/an-introduction-to-speculative-...

EDIT: HN commenter Jasper_ perhaps has the answer in another post[3]: "The current CPython maintainers believe that a JIT would be far too much complexity to maintain, and would drive away new contributors. I don't agree with their analysis; I would argue the bigger thing driving new contributors away is them actively doing so. People show up all the time with basic patches for things other language runtimes have been doing for 20+ years and get roasted alive for daring to suggest something as arcane as method dispatch caches. The horror!"

[3] https://news.ycombinator.com/item?id=30047289#30050248


> If Python is allowed to call fast C code (PCRE) for regex-redux, I don't see why Python shouldn't be allowed to call fast Fortran BLAS/etc for n-body…

The regex and arbitrary precision number libraries were allowed for regex-redux and pidigits because some language implementations wrapped those libraries rather than implement their own thing.


Big Endian POWER isn't bug-for-bug compatible with buggy Javascript usage of typed arrays that assumes little endianness, and thus browsers/nodejs/deno on POWER will be exposed to bugs that don't affect little endian x86-64/ARM.

After so many years of endianness bugs in C/C++ code, it's perplexing that the web standards committee voted to put typed arrays in Javascript in such a way that exposes platform byte order to Javascript programmers who can't generally be expected to have low-level C/C++/ASM experience with memory layout issues:

  function endianness () {
    let u32arr = new Uint32Array([0x11223344]);
    let u8arr = new Uint8Array(u32arr.buffer);
    if (u8arr[0] === 0x44)
        return 'Little Endian';
    else if (u8arr[0] === 0x11)
        return 'Big Endian';
    else
        return 'WTF (What a Terrible Failure)';
  }
EDIT: my old Power Mac was big endian, but I just read POWER has an endianness toggle. So in little endian mode it ought run endian-buggy JS with bug-for-bug compatibility.


Spoiler alert: it does (typing this in Firefox 96 on a little-endian POWER9). In TenFourFox, which ran exclusively big, we had code to byteswap typed arrays to make them look little-endian to scripts. This partially worked (enough for many `asm.js` scripts to run).


> [15] Richard Waters (1979) developed a program that automatically analyzes traditional Fortran programs

Anyone have a PDF link to a paper about this Fortran analyzer?



While faster-than-light (FTL) travel is impossible through spacetime as PhD physicists know it (i.e. as "Relativity-confirming experiments reveal it"), there are forms of apparent FTL that have mainstream PhD acceptance, because they don't allow information to travel from point A to point B faster than light and thus do not violate causality.

The expansion of the observable universe itself being apparently FTL is perhaps the most interesting. If we point the Hubble telescope (and soon JWST) towards opposite ends of the observable universe, we observe extreme redshift galaxies receding away from each other in opposite directions greater than twice lightspeed if we measure according to "distance in light years" from earth. But no photons or information from a galaxy at one side of our observable universe can be sent to a galaxy at the opposite end: they are receding away from each other too rapidly, these two galaxies exist Beyond the Cosmological Event Horizon relative to each other and are forever unknowable to each other, and thus causality is not violated.

An advanced alien race in galaxy A might be able to beam a message to earth before galaxy A vanishes Beyond the Cosmological Event Horizon within the next 100 million years earth-time, and likewise galaxy B might beam a message to earth within the next 100 million years earth-time, but earth can't relay the message from A to B or B to A because they have already forever receded from each other and will soon both likewise forever recede away from us.


There is no restriction on relative velocities between objects from a given observer as long as all objects have velocities less than the speed of light. You can point laser pointers in opposite directions and the "front" of the laser pointer will recede at "2x" C from the opposite pointer.

In the case of the expanding universe, our standard "flat" space time represented by minkowski space is warped into de-sitter space. As distances increase apparent acceleration increases up to and beyond the speed of light due to the expansion of space time.


  - never talk to cops

  - never read a patent

  - never read proprietary source code
I need a nice printable version of this to post on my wall.


Clearly you've never been sued by a patent troll.

The patent system only benefits (1.) rich corporations that can afford the millions of dollars in lawyer fees to litigate patent claims, (2.) the lawyers that receive said fees.


There have been significant changes to the U.S. patent system the past ~10 years. One of which is the ease, effectiveness, and costs of invalidating a patent.

First, spend a couple of hours analyzing the Patent Claims. Often you'll discover that you most likely do NOT infringe at all - or least highly unlikely.

Other times (albeit after having developed a bit more knowledge one-time), you'll assess with decent probability of the likeliness of being able to invalidate the patent - then the tables turn - use this as a threat against the troll. They greatly fear having their $costly patent potentially wiped out. They too are playing the odds, and even a 10-20% probability of having their patent invalidated can go a long ways to dropping the suit against you.

At the end of the day, engineers are overly afraid of patents. If they spent a little time understanding them, they could greatly reduce this exaggerated fear. It's not an insurmountable hurdle - engineers&scientists have more innate abilities then they give themselves credit for. It's more of a matter that this exaggerated fear has been drilled into them. Lawyers are incentivized to perpetuate these fears.


Individuals and small business owners don't have time to learn patent law, nor do they have money to have dedicated legal departments like rich corporations. If I was sued I would not listen to some anonymous stranger on HN. I would get a lawyer. And I would be charged $300-$800/hr by said lawyer. In the patent ecosystem the patent-holding apex whales and lawyer-sharks hunt smaller creatures to eat, and devour whole.


So whenever I have to study someone else's 'dynamic' python I encounter this sort of thing:

  def foo(bar, baz):
      bar(baz)
      ...
What the heck is 'bar' and 'baz'? I deduce no more than 'bar' can be called with a single 'baz'. I can't use my editor/IDE to "go to definition" of bar/baz to figure out what is going on because everything is dynamically determined at runtime, and even

  grep -ri '\(foo\|bar\|baz\)' --include \*.py
Won't tell me much about foo/bar/baz, it will only start a hound dog on a long and windy scent trail.


Yup, I find this completely insane behavior to think that you somehow benefit from types not being there.

You just make it way harder for people to understand your code and contribute to it.


To be fair, in recent years I've worked on a number of Typescript projects and it was common for developers to use `any`, `Object`, `() -> Promise<void>`, etc. Not super helpful.

Though in my experience, sane code structure and informative comments trump everything else when it comes to understanding big and unknown codebase. I still shudder when I think about working years ago on various Java codebases (mostly business IT systems). What a convoluted mess of n-levels deep interface hierarchies. Types? Yeah, but good luck unraveling what exactly is happening in the runtime.


The example is entirely ridiculous. Types are names too. Does foo(bar: baz) solve the issue? Languages are there to convey meaning.


At least I can navigate to the definition.


It doesn't just make the code harder to read, it makes it run slower, too. Static typing provides some compile-time guarantees about what's going to go where, so the compiler can make a lot of simplifying assumptions that speed things up.


Insane, no - it's just a tradeoff.

I agree that on balance type signatures are better -- and that's why modern Python has evolved to incorporate them. But they aren't a magic cure-all, and do they impose a significant tax of their own.


Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

Python is easy to grok, and if you have programmers writing code like bar(foo,baz) then the problem is not Python. You can write crap in any language.

Unit tests do much of what typing checks anyway ... and here's the thing ... you NEED unit tests no matter what. No typing system can tell you that you wrote > when you should have written <.


> Python is easy to grok

It’s what you’re used to. I personally find Python horrible to read because I used to a whole different class of programming languages. But I’m sure some of my code might be hard to read by others who aren’t used to that particular programming language too.

> Unit tests do much of what typing checks anyway ... and here's the thing ... you NEED unit tests no matter what.

Some, not all. Strictly typed languages are handy when it comes to refactoring and unit tests can sometimes fail there if the design is being changed enough that the unit tests need rewriting too.

> No typing system can tell you that you wrote > when you should have written <.

Not technically true. Some languages with a richer set of types and operator overloading could have code written to detect that sort of thing. But I do get your point that unit tests are import too.

I’ve been programming for > 30 years and in dozens of different languages. In that time I’ve felt strictly typed languages make larger and more mature code based slightly easier to maintain. While loosely typed languages are easier for smaller and/or younger code based. But largely it boils more down to personal preference than anything.

I will caveat that by saying the fact that Python supports type annotations should be telling that even dynamic languages benefit from a stricter approach to typing.


I'm glad to see someone else that finds Python unreadable. I keep seeing people saying that it's one of the most readable languages out there, and each time I feel like I'm from another planet.


> Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

People who just learn programming probably think the same about whatever language they are learning.

> No typing system can tell you that you wrote > when you should have written <.

The more the compiler can figure out for you, the quicker problems can be identified and fixed. I stopped using python altogether, because it was just infuriating to have the tiniest mistakes blowing up in spectacular and inscrutable ways. Mixing up values of complex types often does not fail at the actual site of the error, but much, much later. Sometimes literally later in time, as in hours, days, or months until you get an obscure "FooType does not Bar" error, and how the thing in question ever became a FooType is inscrutable at that point. If the result even is a runtime error at all! (Bonus points if your production database is now full of junk as well.[1])

The unit test did not catch it because it did not test the offending composition of classes and functions. Meanwhile, a compiler would have caught it immediately: "The thing you're doing here leads to your data structures being nested wrong."

When I started using async/await in python, at first it was just over, since in plain python that introduces another layer of typing without any assistance whatsoever. Then I discovered mypy which actually lets me do some amount of static typing in python, and it was very enjoyable and now python is back on the table for smaller projects.

There is a reason Haskell has the reputation of "if it compiles, it works". There is a reason why system programmers that work on critical systems are jealously eyeing Rust if their shop still does C.

By the way, dependent type systems absolutely can tell you if you wrote > instead of <. But since that usually comes at the expense of not being Turing complete anymore, it's more used for very critical systems, or for theorem provers.

[1] Yes sqlite, I'm looking at you. The decision to make database column dynamically typed, and hence have for example an INTEGER column silently accept data that is very much not an INTEGER at all, caused me some grief on a widely deployed system once.


FWIW, sqlite now has 'strict' tables.


Thanks! I still like sqlite a lot and plan to use it again someday, so I will be happy knowing that in advance.


Typing allows you to specify the expected behavior in terms of input/output structure of algorithms in such a way that they can be statically verified without writing unit tests or manual checking code in the source, allowing your unit tests to check behavior by value rather than by value and structure. The equivalent of type checking is not unit testing, but fuzzing.

Python is not easy to grok at all, when you consider you have to grok implementations to understand what they are supposed to do, and require extensive runtime debugging to figure out if it is behaving as expected before you can even write unit tests.

Compare to decent statically typed languages, which have quicker write/debug cycles since checking type definitions is faster than checking code behavior, and the structural unit testing is covered automatically by the compiler.

It's like getting more than 50% of your programs' test coverage, for free!


> Python is easy to grok,

It's not, though, it just gives you that illusion.

The code might be easier to read but it's harder to understand and to modify safely because of the absence of type annotations.


> Oh yeah the easiest code in the world to read is some contorted type system and function signatures that look like hieroglyphics that you need a PHd in CS to comprehend.

You can also make a book easier to read by ripping out all its pages.

If you eliminate the content you need to read to understand something, what have you actually made easier?

> No typing system can tell you that you wrote > when you should have written <.

There are many that can; e.g. via SMT-decidable refinement types, or even full undecidable dependent types coupled with automated solvers and manual proofs.


Yeah, I always say that python is an amazing language to prototype and terrible language to scale precisely because it lets people write the usual terrible code and then gives you the freedom to make it even worse.


In Clojure, I tend to put pre and post assertions on most of my functions, which is useful for checking errors in the schema of runtime data (very useful when dealing with 3rd party APIs) but it also offers the documentation that you are seeking:

    (defn advisories
    [config]
    {:pre [
         (map? config)
         (:download-advisories-dir config)
         ]
    :post [
            (map? %)
           ]
    }
    (let [
        dir (:download-advisories-dir config)
        ]

    ;; more code here


And now imagine the compiler would actually enforce that practice, and you have static typing, with less boilerplate.


How is this any better than static types?


Pre/post conditions are complementary to a type system. They can ensure logical properties that may not be encodable in your underlying type system (that is, essentially every mainstream statically typed language). Such as the relationship between two values in a collection. Trivial example, if you have a range such as [x,y] where x < y must hold, how would you convey that in any mainstream type system?


The Haskell-y way to do this is to use a smart constructor[0].

[0]: https://wiki.haskell.org/Smart_constructors


The first part of that page demonstrates what amounts to pre/post conditions, but placed in the constructor. The range is checked dynamically, not statically.

The second part is using Peano numbers to enforce the constraint. I guess you could try and force that into some mainstream languages, probably C++. With its template programming you could get something going in this vein, though I'm not sure how well it would work if the number were calculated at runtime rather than compile time. You'd still end up with a dynamic check somewhere.


The way that the value floats through the system is checked statically, and the program can (and should) be designed so that the value with the appropriate type cannot be constructed unsafely.

If you need to statically check the construction of values in Haskell, there are things like refinement types[0].

[0]: http://nikita-volkov.github.io/refined/


> The way that the value floats through the system is checked statically, and the program can (and should) be designed so that the value with the appropriate type cannot be constructed unsafely.

Except that in the first example from the first link you sent me, there is no static guarantee that the inputs to the constructor are valid, thus the error branch (it would be unnecessary if static guarantees could be made regarding the use of the constructor). And that was my point, that you still end up with dynamic checks on the values which is where pre/post conditions step in to cover what static typing cannot (or, again, cannot easily in mainstream languages, which would not be Haskell).


Is there any reason you didn't address the second link that I shared?


Mostly because, per my reading (as an admitted Haskell dabbler and not fluent), it looks like a variation on a theme rather than a totally different thing. It seems to be a more consistent and refined (har har) way of doing the same thing as the first link, and still has a dynamic check (at least in one form, refine vs refineTH) just like the smart constructors.

But also because we got derailed from my initial point and context.

> Pre/post conditions are complementary to a type system.

Do you disagree or agree with this statement? Because you never addressed it either.

> They can ensure logical properties that may not be encodable in your underlying type system

Note the "may", because that's important. I didn't say that there were no languages in which my example could be encoded in the type system. And maybe it wasn't the best example, but the point itself was that there is no type system (that I'm aware of, not even Idris as far as I know) which can prove in its static type system every piece of logic about a program. This means that some properties of the system will end up being checked (if you bother to) at runtime and not at compile time. That's where pre/post conditions are useful, they contain information (and in a more deliberate form in cases like the Clojure example) about the properties of the system that are hard or impossible to encode directly in the type system.

Different type systems (both static and dynamic) let you express more or less with them, which reduces the need/desire to have checks like these in your program. But I seriously doubt that any mainstream language will ever totally remove their utility, as complements to the rest of the type system.


> there is no type system (that I'm aware of, not even Idris as far as I know) which can prove in its static type system every piece of logic about a program.

Many static type systems can prove anything that can be proven. Notionally one might write a program that relies on something unproven like the Collatz conjecture, though I'm not sure that would happen in practice (e.g. it's easy to write a program that relies on the Collantz conjecture for numbers below 2^64, but that's quite provable). Whether it's actually worth writing out the proof is another question though.

> This means that some properties of the system will end up being checked (if you bother to) at runtime and not at compile time. That's where pre/post conditions are useful, they contain information (and in a more deliberate form in cases like the Clojure example) about the properties of the system that are hard or impossible to encode directly in the type system.

This is true but makes surprisingly little difference, because you still want to keep track of which values have or haven't had that runtime check applied. So you almost always want your postconditions expressed as types (even if they're just "marker" types that indicate that a given runtime check was passed). Put another way, any metadata you would want to be able to track about a function's argument values or return value, you almost always want to be able to carry that metadata around with that value before it's passed in or after it's returned - but at that point that metadata is a type, and it's easiest to represent it as one.


Sorry, I should have been clearer. I was referring to the Template Haskell part of that refinement types library, where the construction of values can indeed be statically checked at compile time.

> > Pre/post conditions are complementary to a type system.

> Do you disagree or agree with this statement? Because you never addressed it either.

I agree with it. I write web applications in Haskell for a living, and the very nature of web applications is that most values coming into the system are not known until runtime. It is not a reasonable design goal to want every possible value in the system to be verified at compile time. However, it is valuable to be able to statically verify the relationships between functions and values as those values — once they have been parsed at runtime into a more principled type — move through the system.



This is one of my favourite blog posts on the Internet and I implore every programmer to read it.

> The claim is simple: in a static type system, you must declare the shape of data ahead of time, but in a dynamic type system, the type can be, well, dynamic! It sounds self-evident, so much so that Rich Hickey has practically built a speaking career upon its emotional appeal. The only problem is it isn’t true.


Static languages unfortunately don't save you from that. You find automatically inferred types, or types that refer to some abstract interface or template-class-mess but you have no idea where the actual implementation lives until you compile with RTTI and run it under a debugger... and as tfa posits, people working with the limitations of static languages often end up reinventing a dynamic structure.

Is this somehow supposed to relevant to the posted article or did you just want to start a tangentially related dynamic-vs-static flame war here in the comments section?


> Static languages unfortunately don't save you from that.

While you still can make a static language that is confusing, it's a lot harder... I challenge you to write a function signature in Rust that is both:

1) Useful

2) As opaque as the python signature above.

> You find automatically inferred types

A minority of static languages do type inference in function signatures. I think it's a bad idea for exactly the same reason the python code is bad. On the other hand, every dynamic language allows you to omit any information about a type signature.


They usually save you from that particular pitfall, but not always of course.

Static vs dynamic makes for such difference in the detailed workflow, both in terms of changing existing code & in terms of writing new/(more) from scratch code, yet they can both be quite fruitful, and can both be abused in absurdum.

It seems like people naturally fall into one of the two camps (either by personality or by training), and the other side just seems kind of insane: "how can you even work that way!?". Then culture and idioms emerge over time and strengthen the tribalism.

I've gone back and forth between the two over the course of my career, and it's quite a mind-shift when switching, with a fair bit of pain involved ("but it would be so easy to do this in [old language]", or "what the hell is this garbage anyway!?") and then eventually it settles in and it's not all painful, all the time ;)

(Going back and forth between Scala and Python right now, so this hit a bit of a nerve)


> Static languages unfortunately don't save you from that. You find automatically inferred types,

Oh yes, they do. Even inferred, the types are there and pretty easy to locate, even if you're not using an IDE.


The types are there.. but you don't know which one it is that your program is dealing with. You could have dozens of implementations for any given abstract interface. One gets picked up at run time.


You don't need to know which one, because the abstract interface tells you how to use it...


That's the theory. Works great when there are no bugs and everything's been designed just right. In that world you could wipe implementations from memory because you won't ever need to dive in..

Very often I'm looking at code and "how to use the interface" is not a question I'm looking to find answers for.


Some information is a lot better than none. In some cases you might want to know what implements the interface: that information is also statically available. In Rust, you can look at a trait and see what types implement that trait.

If you need to know exactly which implementation is being used in a particular context then maybe you shouldn't be using an interface, but should be using the concrete type?


> If you need to know exactly which implementation is being used in a particular context then maybe you shouldn't be using an interface, but should be using the concrete type?

Look again at the original comment by dandotway. It's not a question of "what type should I be using here", nor is it "how should I use this interface", but "what do I need to do to understand this code?". Even if an abstract interface is used correctly and is the right thing to do, you still need to understand the code before you can (debug|rewrite|extend|whatever) it.

And it's a pipe dream to say you can look just at the interface. Something blows up, is it the interface used wrong? Maybe, maybe not? Is it the interface implemented wrong? Maybe, first you need to know what implementation you're looking at. Subtle interactions between abstract and concrete send you spelunking through the layers when you're debugging or trying to extend the interface to account for something the original author didn't anticipate, and often the devil (in the details) comes from concrete implementations.


> to some abstract interface or template-class-mess

And traits! "Oh look, this functionality is implemented in a trait implemented by a trait implemented by a trait implemented by what you're looking at. Maybe"


The real power of dynamic languages is being able to do:

    const foo = JSON.parse(arbitraryJsonString);
and not having to worry about the structure up front.


When that JSON payload changes (intentionally or not), you will run into a mysterious problem in some unrelated area of your code. It will be significantly more expensive to fix than failing fast at the point of parsing.


But depending on the situation, that problem may never happen. I'm not a big fan of introducing complexity to guard against code screwups in the same codebase.


The hardest bugs are the rare bugs. Common and frequent problems are easy bugs to fix. These subtle or rare changes are those which should be feared. If you model only that which you support, finding the source of the problem becomes much easier.


But the thing is, don't you have to worry about structure? You have to unpack the elements from the JSON, so you will need to encode its structure explicitly, which includes type information. The only reason this would be useful is if you're just shuttling data to another API that expects a dict structure (that will then validate everything) and not JSON and you aren't really doing any real work yourself.


> The real power of dynamic languages is being able to do: const foo = JSON.parse(arbitraryJsonString); and not having to worry about the structure up front.

That's not power, that's a shotgun aimed at your crotch whose trigger is connected to a cosmic ray detector.


+NaN For comments that make me laugh out loud for duration T>2.0 seconds, I wish HN provided a way to transmute/sacrifice one's past karma points into additional +1 mod points.


Every static language that I know of also supports this -- you can parse into a sum type of `JsonAny` (or whatever), where `JsonAny` is one of `Null | Number | String | List[Any] | Dict[String, JsonAny]`.

The API then becomes a runtime fallible one, which is perfectly sound.


In C# I can just parse it to a Dictionary<string, object>, and I can do that without destroying the usability of the rest of the language.


What if the JSON represents a list, or an int?

Also, how do you then access nested objects, like data['key'][0]['attr'] in Python?


> What if the JSON represents a list, or an int?

Then you write one short operator (and I agree that some static languages make this more cumbersome than it should be) to say so, and either handle the case where it isn't, or explicitly declare yourself partial and not handling it.

> Also, how do you then access nested objects, like data['key'][0]['attr'] in Python?

With lenses, something like:

    data ^? (key "key") >>> (nth 0) >>> (key "attr")
If you do several unsafe operations in a row then this is cumbersome by design - you want to be clear which parts of your program are safe and which are unsafe, so that readers can understand and know where to review. But a good language should let you compose together several unsafe operations in a lightweight way and then execute them as a single unsafe operation, for cases like this where you want to work in the unsafe part of the language for a bit.


Sure there are solutions.

But my main point is that HideousKojima's "statically-typed" solution would result in a runtime type error if it was given unexpected input, just like a dynamically typed solution.


> But my main point is that HideousKojima's "statically-typed" solution would result in a runtime type error if it was given unexpected input, just like a dynamically typed solution.

I don't think HideousKojima ever called it a "statically-typed solution". Their point was that statically-typed languages still let you write unchecked code when you want to - and yes, of course such unchecked code can fail at runtime - but give you the option of having checking in the cases where you want it.


You can parse it to the dynamic type too. Everyone is happy...right?


Well in Python everything is an object, so that type definition holds true for Python as well :P


Rust:

    let foo: serde_json::Value = serde_json::from_str(arbitraryJsonString)?;
There, just as powerful [1]. But you know what's even more powerful? After you've done your dynamic checks, you can do this on the entire JSON tree, or on a subtree:

    let bar: MyStaticType = serde_json::from_value(foo)?;
and you get a fully parsed instance of a static type, with all the guarantees and performance benefits that entails.

[1] Value represents a JSON tree: https://docs.serde.rs/serde_json/enum.Value.html


There's something to this. I love featurefull type systems, but I've seen engineers try to parse JSON the "right" way in Scala, get frustrated, and blame the entire concept of statically typed languages. Elm manages to make this user friendly so perhaps it's "only" a matter of compiler messages and API design?


Structure is a virtue, not a vice. By doing this you're subverting your own interests.


Structure is a tool. Like any tool, it can be misused or overused.

For anything even remotely production-y I'll always prefer explicitly parsing JSON into a known structure, but there's a lot of value in in being able to do some exploratory scripting without those constraints.


Yes! Exploratory scripting is a categorically different thing than programming, though, I think.


not necessarily. there is a bottom up school of thought that encourages people to noodle around and construct primitives by playing in the domain, and then interactively composing those primitives into larger and larger systems.


Yeah, that's true. It's a judgment call for sure, but I've always found that angle on things to be self-subversive. The best programmers I know are all bottom-up learners, not top-down.


You can do that in static languages too by just parsing into a map


What's the type definition of the map out of interest?


  const Json = union(enum) {
      null,
      number: f64,
      bool: bool,
      string: []const u8,
      array: []const Json,
      object: HashMap([]const u8, Json),
  };

Usually it's a tagged union of the base JSON types which can easily be consumed by most statically typed languages or a variant of it.

EDIT: added "tagged"



In TS JSON is usually Record<string, unknown>.


Well unknown is not a type (by definition), so you have just stepped outside of a type system, which is very common in TS if I understand.


Unknown is a top type in the TS type system. It serves the very important role of "here you need to apply some pattern matching and validation" and then you can make sure that you can continue working in a type safe environment. TS has a lot of facilities that help you with this (from the usual narrowing things, typeof and instanceof guards, control flow analysis, and at the end of the list there are the big guns like this thing called type predicates which basically allows you to wrap these checks and "casts" in nice reusable functions).

There are also recursive types that help you model JSON, but knowing that it's an arbitrary deep nested map/list of maps/lists and number and bool and string mixed like a Bloody Mary cocktail doesn't really help :)

With NestJS it's very easy to add decorators/annotations to fields of a class, and the framework handles validation (throws HTTP 422 with nice descriptions of what failed) and then in your controller you can again work in a type safe environment.

https://www.typescriptlang.org/docs/handbook/release-notes/t...

https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...


Json = Map<string, Json>


In Nim you can even convert JSON to static types! https://nim-lang.org/docs/json.html#to%2CJsonNode%2Ctypedesc...

Now you get type checking on JSON at compile time :)


How powerful is this, really?

As soon as you try to do anything useful to foo it's not arbitrary anymore. You have to make some kind of an assumption on the underlying type, check for keys, nulls, maybe it's a number (the right number?), maybe it's a list. So now you have to scatter some boilerplate checks everywhere you touch a part of foo.

If you could parse it into a typed structure up front, you'd only have to deal with this in one spot, and have guarantees for everything else that follows.

Bonus: if your typed language has good support for records, you can even do this in a way that only provides structure to the parts you care about, and is robust to changes to any other parts of the json.


You can trivially do exactly the same thing in Haskell, so I think you’re suggesting that dynamic languages have no “real power”.


I thought the real power of dynamic languages was being able to do things like:

   eval('alert("hello, ' + userInput.name + '!")')


What's 'foo' and what you can do with it?


An arbitrary object? What else would arbitrary JSON parse into? Then you can access its properties, like with any JS object.


That’s not a capability unique to dynamic languages


What the heck is 'bar' and 'baz'?

So there's no docstring? And the actual variables are that random and indecipherable?

Sounds like the problem is that you're tasked with looking at code written by someone who is either inexperienced or fundamentally careless. When dealing with reasonably maintained codebases, this kind of situation would seem pretty rare. In modern python we now have type hints of course, which have helped quite a lot.


In other words, in Python you have to rely on your colleagues manually writing documentation, and if they don't you're out of luck and they're 'bad developers' and potentially the whole product is affected.

In static languages this simply isn't a problem. Types are checked for consistency at compile time and you don't have to rely on people toiling on this busy work.

Not to say documentation isn't necessary, or good, but isn't something you need to create working programs because otherwise no one knows wtf any variable is without running the program.


No, actually I advocate static typing approaches for precisely the reasons you give. As does Python, since years and years ago.

I'm just saying that core problem it solves -- "bad developers" -- is going bite you no matter what (if not addressed at the source). And that supposed magical efficacy of measures designed to protect against it is somewhat overstated.


What I've found is even worse: on medium-size personal projects I inevitably pass the wrong type to a function (or refactor and forget to change a call somewhere). Generally it does not fail until somewhere else, which relies on it being a certain type. So even though I know what my functions should take, I still spend a bunch of time tracking down the problems, which involves a printing out a lot of stuff because the error happened outside the current call stack. This is something a static type system would just prevent for me, and I've basically decided that anything beyond really simple stuff is actually faster for me to write in a statically typed language. (Examples in the past are a parser for a static site generator with a mostly Turing-complete language, and 3D model generators)


> In modern python we now have type hints of course, which have helped quite a lot.

I had to laugh, hard.

If your Python program uses any library whatsoever, chances are that library won't have types, so you can't really use them.

Even super widely used libraries like numpy don't have good support for types, much less any library that consumes numpy for obvious reasons.


I had to laugh, hard.

It's fine if you want to insulate yourself. But I don't see that you're making much of a point here.


They're probably laughing because a) you're suggesting manually doing the work static typing does in a dynamic language because its untenable not to for large projects, and b) you can't easily add type hints to other people's libraries.


No - (a) is not what I'm suggesting. And (b) while disappointing, just doesn't slow one's work down very frequently in daily practice.

Look, I just don't buy the suggestion that static typing magically solves a huge set of problems (or that it does so without imposing negative tradeoffs of its own -- the very topic of the original article). Or that dynamic languages are plainly crippled, and that one has to be a kind of a simpleton not to see this obvious fact.


> just doesn't slow one's work down very frequently in daily practice.

Well, maybe you don't feel it slows you down, but it is manual work you must do to get a reliable product only because of dynamic typing. Not only that, but you have to then refer to these docs to check you're not creating a type calamity at some nebulous point down the run time road. Static languages just won't let you mess that up, and often have utilities to generate this documentation for you at no effort.

> I just don't buy the suggestion that static typing magically solves a huge set of problems

Static typing really does "magically" solve a whole class of problems without any negative tradeoffs, assuming the language has decent type inference.

Not all problems, but a specific class of them that you should do extra work to guard against in dynamic languages. Whether that is extra documentation that has to be reliably updated and checked, or run time code to check the types are what you expect at the point of use.

Take for example JavaScript, where typing is not only dynamic, but weak. Numbers in particular can be quite spicy when mixed with strings as I'm sure you know. Strong, static typing forces you to be explicit in these cases and so removes this problem entirely.

By the way, no one's saying anyone is a simpleton. The reality is our field is wide and varied, and different experiences are valid.

Dynamic languages can do some things that static languages can't. For example, you can return completely different types from different execution paths in the same function.

This has been something that has confused me when reading Python, but it does make it easier for stuff like tree parsing. In a static language you need to specify some variant mechanism that knows all data possibilities ahead of time to allow this. From my perspective the dynamic typing trade off isn't worth these bits of 'free' run time flexibility, but YMMV! It really depends what arena you're working in and what you're doing.


I think I've said about 3 times in this thread that I'm firmly in the "pro" camp as regards the net positives of static typing, and for precisely the reasons you've detailed. I just don't see its absence (or instances where it less than algebraically perfect) as the crippling dealbreakers that others seem to regard it as.

What I was referring to as "not slowing one's work down very frequently" was the corner case situation that someone brought up 4 comments above yours, which (in their view) renders the general type checking capabilities of Python >= 3.5 moot. I don't buy that logic, but that was their argument, not yours.

But to shift gears: if there are languages besides JS that you feel get their type system "just right", I'd be curious as to what they are, for the benefit of that future moment when I have the luxury of time to think about these things more.


> not slowing one's work

Put back into context, your reply makes sense as these popular libraries are pretty battle tested. Having said that, it is a valid point that type hints being voluntary means they can only be relied upon with discipled developers and for code you control. Of course, the same point could be made for any code you can't control, especially if the library is written in a weakly typed language like C (or JS).

> I just don't see its absence as the crippling dealbreaker

My genuine question would be: what does dynamic typing offer over static typing? Verbosity would be my expectation, but that only really seems to apply without type inference. The other advantage often mentioned is that it's faster to iterate. Both of these don't seem particularly compelling (or even true) to me, but I'm probably biased as I've spent all of my career working with static typing, aside from a few projects with Python and JS.

> if there are languages besides JS that you feel get their type system "just right", I'd be curious as to what they are

This is use case dependent, of course. Personally I get on well with Nim's (https://nim-lang.org/) type system: https://nim-lang.org/docs/manual.html#types. It's certainly not perfect, but it lets me write code that evokes a similar 'pseudocode' feel as Python and gets out of my way, whilst being compile time bound and very strict (the C-like run time performance doesn't hurt, too). It can be written much as you'd write type hinted Python, but it's strictness is sensible.

For example, you can write `var a = 1.5; a += 1` because `1` can be implicitly converted to a float here, but `var a = 1; a += 1.5` won't compile because int and float aren't directly compatible - you'd need to type cast with something like `a += int(1.5)`, which makes it obvious something weird is happening.

Similarly `let a = 1; let b: uint = a` will not compile because `int` and `uint` aren't compatible (you'd need to use `uint(a)`). You can however write `let b: uint = 1` as the type can be implicitly converted. You can see/play with this online here: https://play.nim-lang.org/#ix=3MRD

This kind of strict typing can save a lot of head scratching issues if you're doing low level work, but it also just validates what you're doing is sensible without the cognitive overhead or syntactic noise that comes from something like Rust (Nim uses implicit lifetimes for performance and threading, rather than as a constraint).

Compared to Python, Nim won't let you silently overwrite things by redefining them, and raises a compile time error if two functions with the same name ambiguously use the same types. However, it has function overloading based on types, which helps in writing statically checked APIs that are type driven rather than name driven.

One of my favourite features is distinct types, which allow you to model different things that are all the same underlying type:

    type
      DataId = distinct int
      KG = distinct int
      
      Data = object
        age: Natural  # Natural is a positive only integer.
        weight: KG

    var data: seq[Data]

    proc newData: DataId =
      data.setLen data.len + 1
      DataId(data.high) # Return the new index as our distinct type.

    proc update(id: DataId, age: Natural, weight: KG) =
      data[id.int] = Data(age: age, weight: weight)

    let id = newData()
    id.update(50, 50.KG)  # Works.
    50.update(50, 50.KG)  # Type mismatch got int but expected DataId.
    id.update(50, 50)     # Type mismatch got int but expected KG.
    id += 1               # Type mismatch += isn't defined for DataId.
As you can imagine, this can save a lot of easy to make accidents from happening but also enriches simple integers to serve other purposes. In the case of modelling currencies (e.g., https://nim-lang.org/docs/manual.html#types-distinct-type) it can prevent costly mistakes, but you can `distinct` any type. Beyond that there's structural generics, typeclasses, metaprogramming, and all that good stuff. All this to say, personally I value strict static typing, but don't like boilerplate. IMHO, typing should give you more modelling options whilst checking your work for you, without getting in your way.


So why can't Nim infer from

   let b: uint = a
that you're really just saying

   let b: uint = uint(a)
And BTW don't you get tired of typing (and reading) `uint` twice in the latter setting? That's what I mean about "side effects" after all.


Oh, just to add that

    let b: uint = uint(a)

    # can be written as:
    let b = uint(a)
The type is inferred from the right hand side during assignment. The only reason I wrote this

    let b: uint = a
is because in my example `a` was an `int`, so

    let b = a
Would infer an `int` type for `b`, which compiles fine, and doesn't show the type mismatch I wanted to present.


> So why can't Nim infer from `let b: uint = a`

It "can", but it's a design decision not to by default because mixing `uint` and `int` is usually a bad idea.

This is telling the compiler you want to add an `int` that represents (say) 63 bits of data with a +/- sign bit to a `uint` that doesn't have a sign bit. If `a = -1` then `b = uint(a)` leaves `b == 18446744073709551615`. Is that expected? Is it a bad idea? Yes. So, the explicit casting is "getting in your way" deliberately so you don't make these mistakes. If `a` is a `uint`, it can't be set to `-1`, and adding them is freely allowed.

Incidentally `uint` shouldn't be used for other reasons too, for instance unsigned integers wrap around on overflow, whereas integers raise overflow errors. The freedom of mixing types like this are why languages like C have so many footguns.

In short, explicit is better than implicit when data semantics are different. When the semantics are the same, like with two `int` values, there's no need to do this extra step.

You could create a converter to automatically convert between these types, but you should know what you're doing; the compiler is trying to save you from surprises. For `int`/`float`, there is the lenientops module: https://nim-lang.org/docs/lenientops.html. This has to be deliberately imported so you're making a conscious choice to allow mixing these types.

> don't you get tired of typing (and reading) `uint` twice in the latter setting?

Well, no because I wouldn't be writing this code. This example is purely to show how the typing system lets you write pythonesque code with inferred typing for sensible things, and ensures you're explicit for less sensible things.

For just `int`, there's no need to coerce types:

    var
      a = 1
      b = a + 2
      intro = "My name is "
      name = "Foo"
      greeting = ""

    b *= 10

    # Error: type mismatch: can't concatenate a string with the `b` int.
    # greeting = intro & name & " and I am " & b & " years old"

    # The `$` operator converts the `b` int to a string.
    greeting = intro & name & " and I am " & $b & " years old"

    # If we wanted, we could allow this with a proc:
    proc `&`(s: string, b: int): string = s & $b

    # Now this works.
    greeting = intro & name & " and I am " & b & " years old"

    echo greeting # "My name is Foo and I am 30 years old"

    # Normally, however, we'd probably be using the built in strformat.
    # Incidentally, this is similar to the printf macro mentioned in the article.

    import strformat
    echo &"My name is {name} and I am {b} years old"


Okay, int/uint was a bad example; but what about

  let a: int = 1
  let b: float = a
Why wouldn't we want our dream language to infer a coercion here?

That said, Python's behavior (though correct to spec) is arguably worse:

   a: int = 1
   b: float = a 
   print(b, type(b))
   >>> 1 <class 'int'>
With no complaints from mypy.


We don't want to automatically convert between `int` and `float` because there's a loss of information, since floats aren't able to represent integers precisely.

However, we don't need to specify types until the point of conversion:

    let a = 1
    let b = a.float
> Python's behavior (though correct to spec) is arguably worse

Yeah that is not ideal. Looking at the code it seems logical at first glance to expect that `b` would be a `float`. In this case, the type hints are deceptive. Still, it's not as bad as JavaScript which doesn't even have an integer type! Just in case you haven't seen this classic: https://www.destroyallsoftware.com/talks/wat

Another gotcha I hit in Python is the scoping of for loops, e.g.,https://stackoverflow.com/questions/3611760/scoping-in-pytho...

Python takes a very non-obvious position on this from my perspective.

Ultimately, all these things are about the balance of correctness versus productivity.

I don't want to be writing types everywhere when it's "obvious" to me what's going on, yet I want my idea of obvious confirmed by the language. At the other end of the scale I don't want to have to annotate the lifetime of every bit of memory to formally prove some single use script. The vast majority of the time a GC is fine, but there are times I want to manually manage things without it being a huge burden.

Python makes a few choices that seem to be good for productivity but end up making things more complicated as projects grow. For me, being able to redefine variables in the same scope is an example of ease of use at the cost of clarity. Another is having to be careful of not only what you import, but the order you import, as rather than raise an ambiguity error the language just silently overwrites function definitions.

Having said that, as you mention, good development practices defend against these issues. It's not a bad language. Personally, after many years of experience with Nim I can't really think of any technical reason to use Python when I get the same immediate productivity combined with a static type checking and the same performance as Rust and C++ (also no GIL). Plus the language can output to C, C++, ObjC and JavaScript so not only can I use libraries in those languages directly, and use the same language for frontend and backend, but (excluding JS) I get small, self contained executables that are easily distributable - another unfortunate pain point with Python.

For everything else, I can directly use Python from Nim and visa versa with Nimpy: https://github.com/yglukhov/nimpy. This is particularly useful if you have some slow Python code bottlenecking production, since the similar syntax makes it relatively straightforward to port over and use the resultant compiled executable within the larger Python code base.

Perhaps ironically, as it stands the most compelling reason not use Nim isn't technical: it's that it's not a well known language yet so it can be a hard sell to employers who want a) to hire developers with experience from a large pool, and b) want to know that a language is well supported and tested. Luckily, it's fairly quick to onboard people thanks to the familiar syntax, and the multiple compile targets make it able to utilise the C/C++/Python ecosystems natively. Arguably the smaller community means companies can have more influence and steer language development. Still this is, in my experience, a not insignificant issue, at least for the time being.


> I just don't buy the suggestion that static typing magically solves a huge set of problems

  // compiles and runs, but does bad things
  function foo(x, y) {
    someDangerousEffect();
    return x + y;
  }

  -- does not compile; huge sets of problems magically solved
  foo :: Int -> Int -> Int
  foo x y = someDangerousEffect >> pure $ x + y


A problem but not a huge one in practice.

And you're neglecting the part of my statement you conveniently truncated.


> not a huge one in practice.

Our experiences have been wildly different :)


Yeah, life sometimes gets that way.


You suggested that Python type hints are useful.

I laughed hard at that suggestion.

Can you maybe just show how to type a Python function such that it does the absurdly simple thing of taking a numpy array of integers?

   def fun(nump_array_of_ints: ???): ...
Just to show everyone just how "useful" type hints in Python _actually_ are.


  import numpy as np
  import numpy.typing as npt

  x = np.array([1, 2, 3])
  def foo(x: npt.NDArray[np.int_]) -> bool:
      return True


Coming from any statically typed language to Python or JavaScript codebases this plagues me. Virtually every project I have seen suffers from this.

Function names and doc comments describe behavior, not argument and return types.


Our main codebase is in PHP, but we enforce type hinting for all new code, so it feels more like a static language at this point in practice. However, there are chunks of old code without type hinting or types in PHPDocs. Whenever I have to deal with that code (especially if it's unknown code), my productivity decreases considerably. I have to click through many layers of functions to figure out the data flow to understand what the implicit contract of a function is. In static languages, all you need to care about is a contract, the rest is implementation details. In the dynamic portions of the codebase, there's just too much cognitive load because I have to look at implementation details to get it. PHPDocs and dynamic checks seem to be pretty error-prone, because a dev often forgets to update both the code and the annotations/type checks (and type checks are often ad hoc and random), leading to even more cognitive load. Having static analyzers in the pipeline to have some control of the situation leads to longer build times, so in the end it feels like PHP builds slower than all our Go projects combined.


Not argument and return types.

When conventions are followed they do exactly that, actually. And unless the code is a complete trainwreck, it's pretty easy to tell what the return type is, even without explicit annotation in the docstring.


What conventions are those, besides Hungarian notation (which I don't think I've ever seen in Python)?


PEP 257, and local conventions in certain projects.

What all comes down to is: if you really have people on your project writing code like the foo/bar/baz example way up above - then you have problems way bigger than static type checks can possibly help you with.


Wouldn't it be something if there existed tooling that enforced this level of discipline and checked its validity before executing any code such that you didn't rely on the entire ecosystem to adhere to the same standards and remove that as a source of ambiguity...


Which Python has had since 3.5.


I do not want to argument against Python. This is more like off topic / side note.

What I like about Rust is that it even checks code in your 'docstring'. So it is easier to keep it maintained.


> I can't use my editor/IDE to "go to definition" of bar/baz

I use "Find Usages" on foo to see where it is used. Once you see where it is used, you know what the types can be. It's not great, but it's also something that can be progressively remedied.

In the event that the function is not as trivial as your example suggests, the author should have written a docstring to help you understand what it is trying to do, in addition to type annotations that will make it more readable.

In this example, bar can either be a function, a class, or any object with __call__(), so the type information is less important in this case, than actual docstrings that express intent.


My worst developer experience ever was trying to make changes to a custom build system with functions exactly like that. Injector or decorator pattern, or what ever it is called.

I just gave up and introduced globals and used as flags for some places in the code.

To make things even more fun, big parts of the system was written in 2.7 called from runtime generated bat files from 3.4 as remnants of a rewrite and the consultant that had his funding cut.


That is why I like dynamic languages like Julia better because they use type annotations more frequently.


Well, you could put the types on these days.

But also, there’s nothing stopping the code from being much clearer about its intention than this weirdly contrived example (I have a lot of code and most the function names are pretty unique). And surely you want to search for ‘foo(‘ to find invocations.



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: