Now, I know, it's not true. It's entirely possible to build weird things in python that are provably impossible to typecheck statically. But modern language servers and their type inference capabilities in rust, terraform, or even straight up python are very impressive.
Static typing type checks are compile time, dynamic typing doesn't. I don't see how these two could be indistinguishable, in one you can't run the program with type errors, in the other you can.
That's not to say that there aren't quite reasonable questions about how effective the different approaches are or how easy it is to fix the error, how complete the checks are, how deep the checks go, etc. A lot of how you feel about that is going to be subjective based on the languages you use and the quality of the code you work with — Rust has an advanced type system and great developer ergonomics providing unusually helpful error messages, Python has weaker typing but also a culture about simplicity which discourages some classes of bugs, Java has a lot of mushy-typed code where people got tired of language / compiler drawbacks and came up with ways to improve ergonomics at the expense of defeating the type checker, etc.
var instance = new SomeClass();
In function definitions, where you are definitely going to need to provide parameter types it's extremely common to document those types in a docblock in a dynamically typed language. At least I always did. So making the types part of the definition is not a significant difference while writing the code. def compose(start, *args):
def helper(x):
for func in reversed(args):
x = func(x)
return start(x)
return helper
There is no mainstream typed language which can write a fully general type for the vararg compose function. TypeScript is probably the one that comes closest, but last I checked it still was unable to write a sufficiently powerful array type. You can write a type for a version of compose with a fixed number of arguments, but not for one working over an arbitrary number of arguments.Your toy example, even generalised, has no practical use. If I can write this:
compose(f, f1, f2, f3)
Then I can write that instead (Haskell): f . f3 . f2 . f1
Or this (F#): f1 |- f2 |- f3 |- f
And now we’ve reduced the problem to a simple function composition, which is very easy to define (Ocaml): let (|-) f g = fun x -> g (f x)
(|-): (’a -> ’b) -> (’b -> ’c) -> (’a -> ’c)
This generalises to any fold where the programmer would provide the list statically (as they always would for a vararg function): instead of trying to type the whole thing, just define & type the underlying binary operation.let f be a overload set matching the signatures {a -> b, i -> j} let g be a overload set matching the signatures {b -> c, j -> k}
compose(g, f) could be given a to return c or i to return k
This is only because people are using statically typed language that place arbitrary restrictions on such functions and make them harder to use. In dynamically typed languages, vararg functions are widely used and enable patterns that are pretty nice.
https://godbolt.org/z/h7n8Y7qf1
Like sure, you can't write out a type for the entire overload set. Overload sets don't have types, but functions do. However, I don't think you'd ever actually want to write out the type of the compose function. Instead, I think it would be more reasonable to request that every intermediate function call is type-checked with fully specified types. In C++ this is the case.
(very minor nitpick: I'd pick `auto&& x` over `auto x`)
True. A more interesting question might be what level of static safety and performance benefits you'd be willing to sacrifice to be able to write functions like this.
Personally, I don't find the kind of code I can't fit into static types particularly appealing, but I find the code navigation, error checking, and optimizations of static types to be priceless.
However, the intersection of programs I encounter in practice with the number of programs that can be statically checked is rather large.
Here I defined a simple `Pipeline` GADT for the argument list, which is just a list of functions with some extra type constraints to ensure that they can be composed. You could do the same thing with a more general type like HList but the type signature for the `compose` function would be much more verbose since you would need to define the relationships between each pair of adjacent function types through explicit constraints involving type families, whereas the `Pipeline` type handles that internally.
Perhaps you don't consider Haskell "mainstream" enough?
from typing import Callable, Any
def compose(start: Callable[[Any], Any], *args: Callable[[Any], Any] -> Callable[[Any], Any]:
def helper(x: Any) -> Any:
for func in reversed(args):
x = func(x)
return start(x)
return helperFound this implementation which also provides pre-expanded forms that are first class functions for specific lengths of arguments docs: https://docs.racket-lang.org/typed-compose/index.html implementation: https://git.marvid.fr/scolobb/typed-compose/src/branch/maste...
Something like:
fn compose<X, T>(start: Box<Fn(T) -> X>, args: Vec<Box<Fn(T) -> T>>) -> Fn(T) -> X {
move |x: X| {
let mut x = x;
for func in args.iter(). reversed() {
x = func(x);
}
start(x)
}
}It's not too long ago that you either had very clumsy type systems - C, Java. These type systems were more of a chore than anything else. Especially the generic transition in java was just tedious, you had to type cast a lot of stuff, and the compiler would still yell at you, and things would still crash.
Or you had very powerful and advanced type systems - Haskell and C++ with templates for example. However, these type systems were just impenetrable. C++ template errors before clang error messages are something. They are certainly an error message. But fixing those without a close delta what happened? Pfsh. Nah.
In those days, dynamic typing was great. You could shed the chore of stupid types, and avoid the really arcane work of making really strong types work.
However, type systems have matured. Today, you can slap a few type annotations on a python function and a modern type inference engine can give you type prediction, accurate tab-completion and errro detection. In something like rust, you define a couple of types in important locations and everything else is inferred.
This in turn gives you the benefit of both: You care about types in a few key locations, but everything else is as simple as a dynamically typed language. And that's when statically typed languages can end up looking almost - or entirely - like a dynamically typed language. Except with less error potential.
"errros" are my nemesis in languages which automatically create a new symbol with every typo!