Doing radical refactoring often involves just making those changes and then fixing all the IDE or compiler errors until it runs again.
Doing radical refactoring often involves just making those changes and then fixing all the IDE or compiler errors until it runs again.
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.
In the second case, Python would allow the code to run but would potentially produce a runtime TypeError when it reached that point depending on exactly the code does. It might also run fine (e.g. I'm just passing that variable to json.dump()) or produce unexpected output (e.g. I'm passing that code to print() and that worked for int, and str, but then someone called it with None and I didn't want "None" in the output).
The point was that while those differ in how they're implemented, the experience can be fairly similar when you're in the middle of the code-test cycle. My example wasn't the most complicated dynamic typing scenario but it's an example of why this works pretty well: most Python code isn't highly dynamic or dynamic everywhere — typically there are a few places which might be challenging for analysis but there's also a LOT of code which only ever works with a single input type. If your IDE provides feedback on all of that code, you're going to avoid a fair number of other bugs and free up time for the hard parts.
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
My point was that we are almost never hurt by that reduction.
> you are specifying that each function cannot have multiple overloads
Haskell has type classes, and if we restrict ourselves to local type inference it's fairly easy to have C++ style overloads without even that. So no, I'm not specifying such a thing.
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.
[1] The need to test much more, the need for a better, more accurate documentation, the higher cost of refactoring, even the higher prototyping times (I prototype faster with a REPL that has static typing, because I don't to debug type errors).
On the other hand, because Common Lisp has resumable exceptions and on-the-fly redefinition of just about everything, I prototype significantly faster in CL because I can just let the debugger stay open until I fix the issue and then hit “continue”.
I no longer believe the “static faster for development than dynamic” thesis because I think a lot depends on how the programmer thinks about programming and which tools are available.
See, if I have an unexpected error, that's because I fucked up my program. And because of that, my runtime state is likely screwed as well. So not only do I have to correct my error, I have to correct its consequences before I restart the program. I can't just resume its execution and hope for the best, I need to know that whatever state I keep is not rotten.
On the other hand, that way of doing things is not exclusive to dynamic languages. There's thing things called "dynamically loaded libraries", that you can use even in C. Game programmers routinely recompile & reload specific dlls just so they can correct their mistakes without restarting the whole game. And those who have written in-game editors have a very powerful stop-debug-restart cycle. On top of a statically typed language.
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.
And once there are two types T1 and T2 which are neither subtypes, nor supertypes of each other, and 2 expressions A1 and A2 of types T1 and T2, and a statically undecideable expression p, then statically typing "if p then A else B" is a problem.
And yes, I agree: Most if not all practical type systems will not accept an if-else statement if they cannot unify both branches of the conditional into a single type. Which makes sense. Because you have to act on the result of the expression, and then it needs to have some common type.
But on a purely theoretical basis, it is entirely possible and valid to have an undecideable type system. Which, btw, happens for a lot of languages: https://3fx.ch/typing-is-hard.html . There are C++ programs which are provably impossible to type at compile time.
sealed class Program
class HaltingProgram : Program()
class InfiniteProgram : Program()
fun checkIsHalting(p: ByteArray) = if (halts(p)) HaltingProgram() else InfiniteProgram()
This program will type check just fine. The inferred return type will be Program because that's the nearest shared ancestor type of both possible return types. Good luck implementing the halts() function of course, but that's not the type system's problem.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!
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.
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.
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.
I know this isn't the approach of choice for most folks but hey - I'm working with ADHD so I've got to make some allowances for some neurodiversity.
When I use a dynamic language I get no errors in dev, I need to run/invoke the program to see if it works. It may appear to work fine as I haven't executed a specific code path hence dynamic languages have extremely high test coverage. With dynamic languages I am delaying my feedback loop, I may get some visual output quicker but that doesn't mean my program is correct.
With a strongly typed language and utilizing types you use the compiler to guide you. The compiler says hey, this isn't correct, fix it, you go fix the error and recompile and repeat.
I've used Elm before and it's the only time I had a complex Javascript UI just compile and work first time. It's like a wow, did that just happen.
With Typescript it's not quite to the level of Elm but find my experience working with React etc far more productive. Typescript says hey, that's wrong, I expect ... you gave ..., you work through the errors and when it runs generally there's less silly mistakes than when I just use Javascript.
I'm learning Rust, the compiler error messages have greatly helped. When you compile it says hey, you tried to do ..., maybe you want ... instead. Not to sure what the suggestion is I try it and 9 times out of 10 it works, compiles, program runs.
With types you generally get better IDE auto complete support etc.
Now i'm using Python for my day job. My experience has been painful, discovering what arguments functions take, passing in wrong values, needing to run slow test suites, finding errors at runtime. Yes you can use type hints and I do but I find them far less reliable.
I guess I'm not a very good programmer so learnt to lean on a compiler to do the hard work for me, and if you have good type support you can lean on types more to get the compiler to help you more.
In Haskell I can write complex logic by writing out the types and ADT's. I've written whole programs with tests to verify the logic without writing a program. I find this incredible efficient for prototyping ideas, just write the types, the functions signitures etc etc. Once that is done you implement the functions, hit compile then boom, your shocked it just worked first time running.
Last week, i could've done either a dataframe, a list of list, a list of tuple, a dict of tuples, a dict of lists (this was a bad idea that did not survive more than 2s in my head) or a list of dict. I started coding with a dataframe in mind (i guess i wanted to show off my numpy/pandas skills to my devops colleagues), but adding type hints to my prototypes shut down the idea pretty quick: lot of complexity for nothing.
Yes, I'm a total scatterbrain. Types let me remind myself later that I did in fact forget what I'm doing and what I did. It lets past-me protect future-me.
The IDE was my logical buddy, and every idea's possibility was rapidly shown with it. And I need to massage things a bit, I go faster because I know what's missing.
The only time I liked eclipse/java :)
After university, the opposite became true. No difficult to diagnose undefined behavior because of ambiguity in typing.
if you write dynamic OO languages with a static mentality in mind, i.e. you try to enforce some sort of global type expectation before the program runs, then obviously static languages are better, because you're trying to write static code.
Benefiting from dynamic languages means ditching that mindset altogether.