Definitely! I couldn't get my head around why people like untyped languages, but I keep an open mind on it. I won't close off that they could be better, if the right patterns/practices are used (whatever they are!)
Yes. Ideally something as powerful as Hindley-Milner or similar. But even the weak sauce versions in Go (or recent Java or C++) are a boon to developer ergonomics.
Java's is particularly weaksauce, and has problems chaining inferences within even the same expression, but it's definitely way better than not having it at all.
> I couldn't get my head around why people like untyped languages
What do you mean exactly with "liking" untyped languages?
I personally program mainly in both a ("cognitively demanding") typed and an untyped languages, and they have different use cases.
For relatively small programs, untyped languages are much faster to develop. A program I wrote yesterday in a few hours would have taken 2 to 4 times as much in a typed language.
On large libraries (I consider gaming engines as so), common wisdom suggests that typed languages make the project easier to manage (modify and so on).
There's obviously a wide range of use cases in between.
Is that true? A simple program that isn't going to see long term use means I don't have to worry about anything but the happy path, either typed or untyped. And something where I have to nail down the edge cases is easier in a typed language.
True? I personally have no doubt (of course, programs vary a lot, so there are certainly cases where the effort is comparable).
Besides the unstructured trees, another example, although more high-level, is interfaces. They're a concept required in statically typed languages (some languages they have a simpler design than others), but not in dynamically typed languages (which have duck typing).
If you have a couple of types that you need to abstract, in a S.T. you'll need to define an interface, the method signatures, which include the return types, and maybe (in lower level languages) the generic types and the super interfaces. Even something as (relatively) simple as generalizing a tuple and a matrix types will require some design.
In D.T. languages, zero effort is required. As long as an object responds to a method (name), it's game.
I think it's not realistic to assume that the concepts that need to be taken care of in a S.T. language are comparable to the ones in a D.T. language. And more concepts imply more cognitive load. And cognitive resources are limited :-)
I joked with a friend that as people get older they start to prefer static typing. I personally don't have a preference, it depends on the application. I think it's obvious what are the advantages of static typing so let me rant about what is for me the main disadvantage: as soon as you have an advanced type system people will try to get creative writing code in the most abstract possible way. It's inevitable. It's the typed equivalent of premature optimization. Why restrict ourselves to implement matrix multiplication for floats? Why not rational numbers? Why not over arbitrary fields? This specially true when people write libraries. And yes, you can encode a lot of things in the type system but maybe it's not such a great metalanguage when taken outside of the well known uses. It's similar of how people get crazy with macros or even worse template metaprogramming.
I don't use go so I don't have an opinion if they made the right choice but I sympathize. I suppose you have to be Ken Thompson to not give a crap and say I'm going to create a language without support for generic programming in 2009. The closest thing would be a tenured professor but they wouldn't dare unless under a pseudonym.
"I joked with a friend that as people get older they start to prefer static typing."
I don't think you can tell that right now. Statically-typed languages have gotten much, much better over the past 30 years (progressively), and over the last 20 years we've all aged, you know, 30 years, so right now I think there's a lot of correlation there rather than necessarily causation. I have also switched to static typing as I've "gotten older", but I would still totally rate things in the order "1990 static < 2010 dynamic < 2020 static", even if I had to choose right now. Static was a real mess in the past.
It also isn't even necessarily big jumps that make that true as much as a steady development of innovations, since obviously it's not like all modern static languages are Hindley-Milner or anything. But a slow dribble of both little features and improvements in understanding of how to use everything over the years, like type deduction (getting rid of the usually-redundant specification of type on both sides of "="), getting away from the idea that OO === matching physical models, libraries that have learned to take more interfaces instead of concrete types, languages like Go that privilege composition over inheritance instead of the other way around and the general trend towards composition, getting iterators embedded more deeply into languages like C#, control flow improvements like usable threading methodologies ("share memory by communicating, don't communicate by sharing memory", and even as much as I dislike it vs. the alternatives, having async/await is better than not having any options even though I prefer Go-style threading by a mile), and so on and so on... a steady dribble of little improvements that one step at a time changed the cost/benefit tradeoffs of a static vs. dynamic language from clearly dynamic circa 2000 to fairly clearly (IMHO) static for any non-trivial code base in 2020. And that's before we talk about Rust or anything like that.
as a counterpoint, genericness can actually serve as a form of documentation. you can often infer a lot from just a signature, e.g:
any :: (Functor f, Foldable f) => (a -> Bool) -> f a -> Bool
tells me that `any` has to work across the whole collection `f a` (list/tree/whatever), because that's
how folding works, and that it will get the answer by calling the function on the collection's elements (the collection is a Functor, and the only thing you can do with one is `map` a function over it).
[of course this is assuming that `any` isn't implemented as `any _ _ = True`]
This advantage can be overstated and give a false sense of security, though. It’s true that for entirely generic functions, you can sometimes infer useful properties just from the type signature, but once you start getting any more specific types in there, all bets may be off. You said
[of course this is assuming that `any` isn't implemented as `any _ _ = True`]
but actually there are many more possibilities. For example, this function might return True if the provided data structure has exactly 5 elements, never using the provided (a -> Bool) function or mapping over anything.
you're right! but i think under normal circumstances it's fair to assume that the function actually uses all of its arguments. and i mean "uses" in a general handwavy sense, e.g. that it doesn't do `any f xs = length (fmap f xs)`, where `f` is technically used, but the results of applying `f` are instantly dropped.
I agree and disagree. I love how you can do abstraction in Haskell based on the minimal interface. If it's "ring-like" or whatever you can define matrix multiplication. That can often come up useful, extending the power of existing functions.
But it means your web scraping code, crud app, {some other boring everyday app} or whatever is now adorned with all this deep mathematical complexity that if you just used nodejs or something it would be easier to understand.
Premature optimization is probably a good term for it, because the good think about haskell is it is easy to make refactoring. Make things specific now and more general later, and it should be backwards compatible for the most part.
But making things into "arrows" is part of the sport I think.
I think this is a fault of language ergonomics rather than static typing.
If you are gluing components together, then surely the components have a common interface that uses compatible types. And surely you need to tell the runtime what those types are in some way, even if that is simply by writing them to return appropriate values.
In theory, a statically typed language with ideal ergonomics would make that as easy to specify as a dynamic language, but would have the benefit of warning you of mistakes as early as possible.
Of course, many statically-typed languages trade-off some amount of ergonomics for other features.
> If you are gluing components together, then surely the components have a common interface that uses compatible types
This is not guaranteed; for example, the representation of a JSON object is trivial in dynamically typed languages, because there are no type bounds.
Ease of metaprogramming is another. Possibly also debugging - I don't get powerful REPLs in the statically typed languages I've used, as much as those in the the dynamically typed language I use.
Concepts like these don't represent best practices for sure at large scale, but they do make things easier at least at small scale, and they don't need to be necessarily supported.
Taken to the extreme, Perl variable autoinitialization and context sensitivity are very convenient for 3-statement programs, but they'd be horror above that. It doesn't make sense for any language to try to support that.
In my experience when gluing things one needs at the very least dependent types to properly express relations. Without that the best hope is a good integration test, but with that in place static types for things that can be expressed in mainstream languages do not bring much extra value while contributing to the cost.