Disclaimer: I don’t want this to become a flame war in the comments. I’m coming from a position of ignorance, and well aware of it. While I’d like this post to provoke thought, it’s not meant to be provocative in the common use of the term.
Chapter 14 of C# in Depth is about dynamic typing in C#. A couple of reviewers have justifiably said that I’m fairly keen on the mantra of "don’t use dynamic typing unless you need it" – and that possibly I’m doing dynamic typing a disservice by not pointing out more of its positive aspects. I completely agree, and I’d love to be more positive – but the problem is that I’m not (yet) convinced about why dynamic typing is something I would want to embrace.
Now I want to start off by making something clear: this is meant to be about dynamic typing. Often advocates for dynamically typed languages will mention:
- REPL (read-eval-print-loop) abilities which allow for a very fast feedback loop while experimenting
- Terseness – the lack of type names everywhere makes code shorter
- Code evaluated at execution time (so config files can be scripts etc)
I don’t count any of these as benefits of dynamic typing per se. They’re benefits which often come alongside dynamic typing, but they’re not dependent on dynamic typing. The terseness argument is the one most closely tied to their dynamic nature, but various languages with powerful type inference show that being statically typed doesn’t mean having to specify type names everywhere. (C#’s var keyword is a very restricted form of type inference, compared with – say – that of F#.)
What I’m talking about is binding being performed at execution time and only at execution time. That allows for:
- Duck typing
- Dynamic reaction to previously undeclared messages
- Other parts of dynamic typing I’m unaware of (how could there not be any?)
What I’m interested in is how often these are used within real world (rather than demonstration) code. It may well be that I’m suffering from Blub’s paradox – that I can’t see the valid uses of these features simply because I haven’t used them enough. Just to be clear, I’m not saying that I never encounter problems where I would welcome dynamic typing – but I don’t run into them every day, whereas I get help from the compiler every day.
Just as an indicator of how set in my statically typed ways I am, at the recent Stack Overflow DevDays event in London, Michael Sparks went through Peter Norvig’s spelling corrector. It’s a neat piece of code (and yes, I’ll finish that port some time) but I kept finding it hard to understand simply because the types weren’t spelled out. Terseness can certainly be beneficial, but in this case I would personally have found it simpler if the variable and method types had been explicitly declared.
So, for the dynamic typing fans (and I’m sure several readers will come into that category):
- How often do you take advantage of dynamic typing in a way that really wouldn’t be feasible (or would be very clunky) in a statically typed language?
- Is it usually the same single problem which crops up regularly, or do you find a wide variety of problems benefit from dynamic typing?
- When you declare a variable (or first assign a value to a variable, if your language doesn’t use explicit declarations) how often do you really either not know its type or want to use some aspect of it which wouldn’t typically have been available in a statically typed environment?
- What balance do you find in your use of duck typing (the same method/member/message has already been declared on multiple types, but there’s no common type or interface) vs truly dynamic reaction based on introspection of the message within code (e.g. building a query based on the name of the method, such as FindBooksByAuthor("Josh Bloch"))?
- What aspects of dynamic typing do I appear to be completely unaware of?
Hopefully someone will be able to turn the light bulb on for me, so I can be more genuinely enthusiastic about dynamic typing, and perhaps even diversify from my comfort zone of C#…
The problem is that the typing system is just one aspect of a larger picture. If you don’t have dynamic dispatch (like you do in Ruby but don’t in C#), then dynamic typing is the same as passing everything as an object and casting it to the desired type in your methods. If your language doesn’t make it easy to inspect or reflect on your objects, then dynamic typing is more likely to cause you more ceremonious pain than syntactic gain.
Ruby emphasizes behavior at runtime. C# emphasizes behavior at compile time. Turning dynamic typing “on” in C# is not the same as taking full advantage of a dynamically typed language like Ruby. The differences in the languages are so much larger than their respective syntax. I don’t think you can truly learn and understand dynamic typing in a statically typed bubble.
LikeLike
Generally, I believe that many of the things you can do with dynamic typing can also be achieved with well-designed strongly-typed code. However, there are a few areas where dynamic typing can add a level of expressiveness that strongly-typed languages lose. For example:
1. Creating Domain-Specific Languages. I’ve seen a few elegant examples of this in languages like Ruby and Python; and while you can certainly do this in a strongly typed language (the MS DSL framework is actually quite nice), dynamic typing helps you fit the syntax and structure of your DSL to better fit your domain. Also, when creating DSLs a common task is to transform the constructs and expressions in the DSL into equivalent forms in another representation (C#, SQL, etc.). Untyped languages allow you to treat all expressions homogeneously. Take a look at http://www.artima.com/rubycs/articles/ruby_as_dsl.html.
2. Configuration Management. Many systems today use high-level, strongly typed languages like C# or Java for their implementation, and then rely on flimsy XML or KVP (key-value pair) models for configuration. I have two major “beefs” with using XML/KVP for configuration: a) the structure and syntax of XML/KVP is obtuse, and lends itself to a lot of repetition/redundancy, and b) XML/KVP is entirely declarative and generally doesn’t provide a way to intermix imperative statements. For example, try creating a web.config file that scales the number of worker threads in your ASP.NET thread pool to be equal to the number of CPU cores times 3. You can’t. Or try dividing a single configuration file into manageable modules that get “dynamically included” as needed. Not generally possible. But if you use a language like Ruby or PHP to define your configuration, you can easily mix declarative and imperative constructs as you need to; you can also dynamically assemble them from multiple fragments.
3. Meta-Programming. Strongly-typed languages can make it hard (or impossible) to do generative programming or construct type-agnostic higher-order functions. For example, writing a code in C# that takes an arbitrary function (with any number of parameters/return value), a list of parameters, and then “mapping” it onto some set of data or an iterable collection is not possible. Languages like F# take the approach of performing sophisticated type inference based on the definitions of functions to ensure type-safety. Languages like Ruby/PHP go the other direction and essentially ignore the types involved in the expression, assuming that the programmer will ensure that all types are compatible/coercible. I don’t know which approach is ultimately better, but I can say that when I’ve needed to write generative meta-code I find it easier in Ruby than in F# (Full disclosure: I’ve only tried it twice in F# and given up each time because my limited brain wasn’t able to grok the intermediate or end result).
LikeLike
@Leo: Thanks, that’s exactly the sort of thing I was looking for. (You might want to look at Guice for the second option, btw. I don’t know if any of the .NET IoC containers work in a similar way.)
LikeLike
Er, somewhat late.
I work in both Delphi and Python. Delphi is strongly statically-typed; Python is strongly dynamically typed. My understanding of how to work in each differs according to the following point-of-view substitution:
1) When in Delphi, my typical thought is, “When I am inside a method with arguments, what feature set (fields, properties, methods) does each argument support?”
2) When in Python, my typical thought is, “When I am calling this method with arguments, what features (fields, properties, methods) must be available within the arguments I provide?”
Now admittedly, this is as much about duck-typing as it is about dynamic typing (how could it not be?), but the essence of the difference is the massively increased scope for polymorphism in Python compared to Delphi.
Given something like
def foo(arg)
arg.method()
it should become clear that anything passed into foo that has a “.method()” in it will work. There is a huge difference between this, and setting the type of the argument to be some ancestor class from which it is required that all objects needing to be passed to “foo” must be descended.
In these situations, duck-typing, and as a consequence dynamic typing, sidestep the problems relating to multiple inheritance that tend to become problematic in statically-typed languages (subjective, agreed, but my opinion).
I struggled with this a great deal initially, because I found it very difficult to grok Python code because I never knew what was being passed into functions; how then to know what the arguments support?
The resolution to this problem lies in learning new habits in reading code: what defines the argument to a function (in a dynamically-typed language) is what gets *done* to it in that function, not what it *is* outside the context of that function. Again, this is about duck-typing, but dynamic typing is required for this to work. In python, a “valid” argument to a function means something (anything!) that will provide the functionality that that function requires. In a sense, one must temporarily suspend one’s normal thought process that would apply when reading statically-typed code. The “type” is set by what the argument is required to be able to do, not by external declaration.
I can shift between Delphi and Python quite easily, and the difference in typing mechanisms is not a big deal (given the shift in understanding described above). Code reuse is obviously greater in python, because functions are agnostic w.r.t. types, and therefore somewhat more atomic. I get to be lazier in Delphi because the IDE and the static typing require less planning upfront, but I am generally more productive in python because the standard library is so good, and I can write functions to operate on my classes before the classes are even defined. In this respect, I constantly rely on dynamic typing in python as a language feature.
My experience with respect to safety (or danger, depending on how full your glass is) is that for both Delphi and Python, the prevalence of bugs is about equally likely and has nothing to do with static or dynamic typing, although every so often my Python code surprises me by running correctly on the first try.
LikeLike
@cjrh: So when you’re *calling* a method, how do you know whether a method will be valid or not? Does the documentation always specify everything that will be called on that argument, and what it expects those calls to do?
To me, static typing is a way of providing that information in a single word… and with interfaces, the lack of multiple inheritance doesn’t get in my way much anyway.
The downside is situations where various types *do* have the method I need, but they have no common interface. That’s where duck typing would really shine – but I don’t find I come across it that often. Is it regularly a problem for you when you’re coding in Delphi?
LikeLike
@skeet:
For knowing what can be passed to a function, it seems that conventionally you either:
a) follow documentation (if it exists)
b) get documentation from the docstring of the function at the REPL prompt, e.g. “>>> print help(foo)”. (this usually exists)
c) read the source. (this always exists)
It is often the case that a python library will say something like “this function should be passed a file-like object” (cf. Django), which means that you can pass actual file-like objects, but also anything else that contains .read() and .write(), and perhaps a few other members that might be explicitly mentioned in the docs. In practice, it is not that bad once you fully internalize the fact that the argument type doesn’t matter, only the operations performed on it. On the other hand, every now and then I see recipes for performing explicit type-checking on the arguments to functions, which seems IMO completely misguided (they should rather check for the existing of required methods/members on the arguments).
Lack of multiple inheritance…perhaps I wasn’t clear earlier: I tried to explain that duck-typing gives you, among several other things, basically unlimited polymorphism, which feeds greater code-reuse. That has pros and cons. One can always live without it (one can get by with very little, I have used FORTRAN somewhat) as you said, but that is IMO a significant benefit of dynamic typing. ymmv and all that. The relative weights of the pros and cons vary for different use cases, different projects, different individuals. You’re a smart guy, and you’ve heard all the arguments before. If you’ve already tinkered with something like python yourself, and found it came up wanting, well then that’s that; but if not, there is something else remaining to do: see for yourself. The tutorial included in the python install takes about 2 hours to get through, and gets you about 80% effective to write programs.
I find this discussion similar to how the introduction of subversion as a version control system caused much angst (myself included) because it removed file-locking (Oh No, everything will be clobbered!). But once you get used to the extra freedom, and deal with exceptional cases as required, you wouldn’t want to go back. Of course now we see a similar trend regarding the DVCS backlash with many people clutching at the safety of their SVN. But in a few years, DVCS will be completely dominant and we’ll be asking ourselves however did we get anything done without it.
The fact that static typing still produces the benefit of fast-running code remains a very compelling point for a large number of use-cases. Were dynamic languages to close the performance gap more substantially than has been the case so far, I would expect to see a much greater shift in that direction. The claim that static typing is “safer” than dynamic typing is often made, but it has consistently been my experience that there is no difference in the nature and number of bugs between my delphi code and python code. It just takes me longer to produce them in Delphi. Oh, and I pretty much never get off-by-one errors in python, because almost everything is directly iterable; but this has nothing to do with dynamic typing.
Regarding feature frustration, I don’t code in Delphi the same way I code in python, so in Delphi I don’t even think in these terms, therefore the lack of duck-typing is not problematic. I guess in a sense you just resign yourself to duplicating code for different types as needed. You do your best to abstract, but only so far and no further, and then deal with it. It is not something that I think about often. Idiomatic Delphi is very far from idiomatic python. It is sometimes frustrating to have to declare so much upfront in Delphi, and lack of multi-line strings really suck, but regarding object patterns I stick to the tried and tested, straight and narrow simple object inheritance with minimal polymorphism sprinkled throughout; and this works reliably and well. It’s kinda like asking if I miss LINQ in Delphi, or asking a FORTRAN programmer if they miss Delphi classes: the question itself seems odd, because idiomatic use of each language implementation accomplishes similar objectives in different ways. This isn’t only a language issue, because the specific implementation, including the provided libraries make a big difference. The most powerful syntax in the world is no match for a single library call that does everything you need, for example.
I will say that the more complex the problem to solve, all else being equal, the more I will be looking towards Python rather than Delphi, and I think the dynamic nature of python plays a huge part in that, especially when the problem domain isn’t even fully revealed before you start writing code. In contrast, the simpler projects, especially GUI-based stuff would be done in Delphi.
LikeLike
@cjrh: Thanks for all the detail. I find your last comment particularly interesting – I would have expected an approach of “quick and dirty, one off code is fine in Python – for large enterprise systems I’d use Delphi.” It’s interesting to hear it working the other way round…
It does sound like I’m not going to appreciate the benefits of dynamic typing without diving in for a significant project – which is a pain, as I haven’t got time to do that at the moment :(
LikeLike