30

I've read a related question Are there any design patterns that are unnecessary in dynamic languages like Python? and remembered this quote on Wikiquote.org

The wonderful thing about dynamic typing is it lets you express anything that is computable. And type systems don’t — type systems are typically decidable, and they restrict you to a subset. People who favor static type systems say “it’s fine, it’s good enough; all the interesting programs you want to write will work as types”. But that’s ridiculous — once you have a type system, you don’t even know what interesting programs are there.

--- Software Engineering Radio Episode 140: Newspeak and Pluggable Types with Gilad Bracha

I wonder, are there useful design patterns or strategies that, using the formulation of the quote, "don't work as types"?

  • 3
    I've found double dispatch and the Visitor pattern to be very difficult to accomplish in statically typed languages, but easily accomplishable in dynamic languages. See this answer (and the question) for example: programmers.stackexchange.com/a/288153/122079 – user3002473 Aug 13 '16 at 0:04
  • 7
    Of course. Any pattern that involves creating new classes at runtime, for example. (that's also possible in Java, but not in C++; there's a sliding scale of dynamism). – immibis Aug 13 '16 at 0:16
  • 1
    It would depend a lot on how sophisticated your type system is :-) Functional languages usually do quite good at this. – Bergi Aug 13 '16 at 20:58
  • 1
    Everyone seems to be talking type systems like Java and C# instead of Haskell or OCaml. A language with a powerful type system can be as concise as a dynamic language but keep type safety. – Andrew Piliser Sep 2 '17 at 0:54
  • @immibis That's incorrect. Static type systems can absolutely create new, "dynamic" classes at run-time. See Chapter 33 of Practical Foundations for Programming Languages. – gardenhead Sep 3 '17 at 5:34
4

First-class types

Dynamic typing means that you have first-class types: you can inspect, create and store types at runtime, including the language's own types. It also means that values are typed, not variables.

Statically typed language may produce code that relies on dynamic types too, like method dispatch, type classes, etc. but in a way that is generally invisible to the runtime. At best, they give you some way to perform introspection. Alternatively, you could simulate types as values but then you have an ad-hoc dynamic type system.

However, dynamic type systems rarely have only first-class types. You can have first-class symbols, first-class packages, first-class .... everything. This is in contrast to the strict separation between the compiler's language and the runtime language in statically typed languages. What the compiler or interpreter can do the runtime can do, too.

Now, let's agree that type inference is a good thing and that I like to have my code checked before running it. However, I also like being able to produce and compile code at runtime. And I love to precompute things at compile-time too. In a dynamically typed language, this is done with the same language. In OCaml, you have the module/functor type system, which is different from the main type system, which is different from the preprocessor language. In C++, you have the template language which has nothing to do with the main language, which is generally ignorant of types during execution. And that's fine in those language, because they don't want to provide more.

Ultimately, that does not change really what kind of software you can develop, but the expressivity changes how you develop them and whether it is hard or not.

Patterns

Patterns that rely on dynamic types are patterns which involve dynamic environments: open classes, dispatching, in-memory databases of objects, serialization, etc. Simple things like generic containers work because a vector does not forget at runtime about the type of objects it holds (no need for parametric types).

I tried to introduce the many ways code is evaluated in Common Lisp as well as examples of possible static analyses (this is SBCL). The sandbox example compiles a tiny subset of Lisp code fetched from a separate file. In order to be reasonably safe, I change the readtable, allow only a subset of standard symbols and wrap things with a timeout.

;;
;; Fetching systems, installing them, etc. 
;; ASDF and QL provide provide resp. a Make-like facility 
;; and system management inside the runtime: those are
;; not distinct programs.
;; Reflexivity allows to develop dedicated tools: for example,
;; being able to find the transitive reduction of dependencies
;; to parallelize builds. 
;; https://gitlab.common-lisp.net/xcvb/asdf-dependency-grovel
;;
(ql:quickload 'trivial-timeout)

;;
;; Readtables are part of the runtime.
;; See also NAMED-READTABLES.
;;
(defparameter *safe-readtable* (copy-readtable *readtable*))
(set-macro-character #\# nil t *safe-readtable*)
(set-macro-character #\: (lambda (&rest args)
                           (declare (ignore args))
                           (error "Colon character disabled."))
                     nil
                     *safe-readtable*)

;; eval-when is necessary when compiling the whole file.
;; This makes the result of the form available in the compile-time
;; environment. 
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar +WHITELISTED-LISP-SYMBOLS+ 
    '(+ - * / lambda labels mod rem expt round 
      truncate floor ceiling values multiple-value-bind)))

;;
;; Read-time evaluation #.+WHITELISTED-LISP-SYMBOLS+
;; The same language is used to control the reader.
;;
(defpackage :sandbox
  (:import-from
   :common-lisp . #.+WHITELISTED-LISP-SYMBOLS+)
  (:export . #.+WHITELISTED-LISP-SYMBOLS+))

(declaim (inline read-sandbox))

(defun read-sandbox (stream &key (timeout 3))
  (declare (type (integer 0 10) timeout))
  (trivial-timeout:with-timeout (timeout)
    (let ((*read-eval* nil)
          (*readtable* *safe-readtable*)
          ;;
          ;; Packages are first-class: no possible name collision.
          ;;
          (package (make-package (gensym "SANDBOX") :use '(:sandbox))))
      (unwind-protect
           (let ((*package* package))
             (loop
                with stop = (gensym)
                for read = (read stream nil stop)
                until (eq read stop)
                ;;
                ;; Eval at runtime
                ;;
                for value = (eval read)
                ;;
                ;; Type checking
                ;;
                unless (functionp value)
                do (error "Not a function")
                ;; 
                ;; Compile at run-time
                ;;
                collect (compile nil value)))
        (delete-package package)))))

;;
;; Static type checking.
;; warning: Constant 50 conflicts with its asserted type (MOD 11)
;;
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in :timeout 50)))

;; get it right, this time
(defun read-sandbox-file (file)
  (with-open-file (in file)
    (read-sandbox in)))

#| /tmp/plugin.lisp
(lambda (x) (+ (* 3 x) 100))
(lambda (a b c) (* a b))
|#

(read-sandbox-file #P"/tmp/plugin.lisp")

;; 
;; caught COMMON-LISP:STYLE-WARNING:
;;   The variable C is defined but never used.
;;

(#<FUNCTION (LAMBDA (#:X)) {10068B008B}>
 #<FUNCTION (LAMBDA (#:A #:B #:C)) {10068D484B}>)

Nothing above is "impossible" to do with other languages. The plug-in approach in Blender, in music software or IDEs for statically compiled languages which do on-the-fly recompilation, etc. Instead of external tools, dynamic languages favor tools which make use of information that is already there. All the known callers of FOO? all the subclasses of BAR? all methods that are specialized by class ZOT? this is internalized data. Types are just another one aspect of this.


(see also: CFFI)

39

Short answer: no, because Turing equivalence.

Long answer: This guy's being a troll. While it's true that type systems "restrict you to a subset," the stuff outside that subset is, by definition, stuff that does not work.

Anything you're able to do in any Turing-complete programming language (which is language designed for general-purpose programming, plus plenty that aren't; it's a pretty low bar to clear and there are several examples of a system becoming Turing-complete unintentionally) you are able to do in any other Turing-complete programming language. This is called "Turing equivalence," and it only means exactly what it says. Importantly, it does not mean that you can do the other thing just as easily in the other language--some would argue that that's the entire point of creating a new programming language in the first place: to give you a better way of doing certain things that existing languages suck at.

A dynamic type system, for example, can be emulated on top of a static OO type system by just declaring all variables, parameters, and return values as the base Object type and then using reflection to access the specific data within, so when you realize this you see that there's literally nothing you can do in a dynamic language that you can't do in a static language. But doing it that way would be a huge mess, of course.

The guy from the quote is correct that static types restrict what you can do, but that's an important feature, not a problem. The lines on the road restrict what you can do in your car, but do you find them restrictive, or helpful? (I know I wouldn't want to drive on a busy, complex road where there's nothing telling the cars going the opposite direction to keep to their side and not come over where I'm driving!) By setting up rules that clearly delineate what's considered invalid behavior and ensuring that it won't happen, you greatly decrease the chances of a nasty crash occurring.

Also, he's mischaracterizing the other side. It's not that "all the interesting programs you want to write will work as types", but rather "all the interesting programs you want to write will require types." Once you get beyond a certain level of complexity, it becomes very difficult to maintain the codebase without a type system to keep you in line, for two reasons.

First, because code with no type annotations is hard to read. Consider the following Python:

def sendData(self, value):
   self.connection.send(serialize(value.someProperty))

What do you expect the data to look like that the system at the other end of the connection receives? And if it's receiving something that looks completely wrong, how do you figure out what's going on?

It all depends on the structure of value.someProperty. But what does it look like? Good question! What's calling sendData()? What is it passing? What does that variable look like? Where did it come from? If it's not local, you have to trace through the entire history of value to track down what's going on. Maybe you're passing something else that also has a someProperty property, but it doesn't do what you think it does?

Now let's look at it with type annotations, as you might see in the Boo language, which uses very similar syntax but is statically typed:

def SendData(value as MyDataType):
   self.Connection.Send(Serialize(value.SomeProperty))

If there's something going wrong, suddenly your job of debugging just got an order of magnitude easier: look up the definition of MyDataType! Plus, the chance of getting bad behavior because you passed some incompatible type that also has a property with the same name suddenly goes to zero, because the type system won't let you make that mistake.

The second reason builds on the first: in a large and complex project, you've most likely got multiple contributors. (And if not, you're building it yourself over a long time, which is essentially the same thing. Try reading code you wrote 3 years ago if you don't believe me!) This means that you don't know what was going through the head of the person who wrote almost any given part of the code at the time they wrote it, because you weren't there, or don't remember if it was your own code a long time ago. Having type declarations really helps you understand what the intention of the code was!

People like the guy in the quote frequently mischaracterize the benefits of static typing as being about "helping the compiler" or "all about efficiency" in a world where nearly unlimited hardware resources make that less and less relevant with each passing year. But as I've shown, while those benefits certainly do exist, the primary benefit is in the human factors, particularly code readability and maintainability. (The added efficiency is certainly a nice bonus, though!)

  • 24
    "This guy's being a troll." – I'm not sure that an ad hominem attack is going to help your otherwise well-presented case. And while I am well aware that argument from authority is an equally bad fallacy as ad hominem, I would still like to point out that Gilad Bracha has probably designed more languages and (most relevant for this discussion) more static type systems than most. Just a small excerpt: he is the sole designer of Newspeak, co-designer of Dart, co-author of the Java Language Specification and Java Virtual Machine Specification, worked on the design of Java and the JVM, designed … – Jörg W Mittag Aug 12 '16 at 23:32
  • 10
    Strongtalk (a static type system for Smalltalk), the Dart type system, the Newspeak type system, his PhD thesis on modularity is the basis of pretty much every modern module system (e.g. Java 9, ECMAScript 2015, Scala, Dart, Newspeak, Ioke, Seph), his paper(s) on mixins revolutionized the way we think about them. Now, that does not mean that he is right, but I do think that having designed multiple static type systems makes him a bit more than a "troll". – Jörg W Mittag Aug 12 '16 at 23:35
  • 17
    "While it's true that type systems "restrict you to a subset," the stuff outside that subset is, by definition, stuff that does not work." – This is wrong. We know from the Undecidability of the Halting Problem, Rice's Theorem, and the myriad of other Undecidability and Uncomputability results that a static type checker can not decide for all programs whether they are type-safe or type-unsafe. It can't accept those programs (some of which are type-unsafe), so the only sane choice is to reject them (however, some of those are type-safe). Alternatively, the language has to be designed in … – Jörg W Mittag Aug 12 '16 at 23:38
  • 9
    … such a way as to make it impossible for the programmer to write those undecidable programs, but again, some of those are actually type-safe. So, no matter how you slice it: the programmer is prevented from writing type-safe programs. And since there are actually infinitely many of them (usually), we can be almost certain that at least some of them are not only stuff that does work, but also useful. – Jörg W Mittag Aug 12 '16 at 23:40
  • 8
    @MasonWheeler: the Halting Problem comes up all the time, precisely in the context of static type checking. Unless languages are carefully designed to prevent the programmer from writing certain kinds of programs, static type checking quickly becomes equivalent to solving the Halting Problem. Either you end up with programs you aren't allowed to write because they might confuse the type checker, or you end up with programs you are allowed to write but they take an infinite amount of time to type check. – Jörg W Mittag Aug 12 '16 at 23:57
27

I'm going to side-step the 'pattern' part because I think it devolves into the definition of what is or isn't a pattern and I've long lost interest in that debate. What I will say is that there are things you can do in some languages that you can't do in others. Let me be clear, I'm not saying there are problems you can solve in one language that you can't solve in another. Mason has already pointed out Turing completeness.

For example, I've written a class in python that takes wraps a XML DOM element and makes it into a first class object. That is, you can write the code:

doc.header.status.text()

and you have the contents of that path in from a parsed XML object. kind of neat and tidy, IMO. And if there isn't a head node, it just returns a dummy objects that contain nothing but dummy objects (turtles all the way down.) There's no real way to do that in, say, Java. You'd have to have compiled a class ahead of time that based on some knowledge of the structure of the XML. Putting aside whether this is a good idea, this kind of thing really does change the way you solve problems in a dynamic language. I'm not saying it changes in a way that is necessarily always better, however. There are some definite costs to dynamic approaches and Mason's answer gives a decent overview. Whether they are a good choice depends on many factors.

On a side note, you can do this in Java because you can build a python interpreter in Java. The fact that solving a specific problem in a given language may mean building an interpreter or something akin to it is often overlooked when people talk about Turing completeness.

  • 4
    You can't do this in Java because Java's poorly designed. It wouldn't be that hard in C# using IDynamicMetaObjectProvider, and it's dead simple in Boo. (Here's an implementation in less than 100 lines, included as part of the standard source tree on GitHub, because it's that easy!) – Mason Wheeler Aug 12 '16 at 22:48
  • 6
    @MasonWheeler "IDynamicMetaObjectProvider"? Is that related to C#'s dynamic keyword? ...which effectively just tacks on dynamic typing to C#? Not sure your argument is valid if I'm right. – jpmc26 Aug 13 '16 at 0:01
  • 9
    @MasonWheeler You're getting into semantics. Without getting into a debate about minutiae (We're not developing a mathematical formalism on SE here.), dynamic typing is the practice of foregoing compile time decisions around types, especially the verification that each type has the particular members the program accesses. That is the goal that dynamic accomplishes in C#. "Reflection and dictionary lookups" happen at runtime, not compile time. I'm really not sure how you can make a case that it doesn't add dynamic typing to the language. My point is that Jimmy's last paragraph covers that. – jpmc26 Aug 13 '16 at 1:12
  • 44
    Despite not being a huge fan of Java, I also dare say that calling Java "poorly designed" specifically because it didn't add dynamic typing is... overzealous. – jpmc26 Aug 13 '16 at 1:15
  • 5
    Apart from the slightly more convenient syntax, how is this different from a dictionary? – Theodoros Chatzigiannakis Aug 13 '16 at 8:29
10

The quote is correct, but also really disingenuous. Let's break it down to see why:

The wonderful thing about dynamic typing is it lets you express anything that is computable.

Well, not quite. A language with dynamic typing lets you express anything as long as it's Turing complete, which most are. The type system itself does not let you express everything. Let's give him the benefit of the doubt here though.

And type systems don’t — type systems are typically decidable, and they restrict you to a subset.

This is true, but notice we are now firmly talking about what the type system allows, not what the language that uses a type system allows. While it is possible to use a type system to calculate stuff at compile time, this in generally not Turing complete (as the type system is generally decidable), but almost any statically typed language is also Turing complete in its runtime (dependently typed languages are not, but I don't believe we are talking about them here).

People who favor static type systems say “it’s fine, it’s good enough; all the interesting programs you want to write will work as types”. But that’s ridiculous — once you have a type system, you don’t even know what interesting programs are there.

The trouble is that dynamically types languages do have a static type. Sometimes everything is a string, and more commonly there is some tagged union where every thing is either a bag of properties or a value like an int or a double. The trouble is that static languages can do this as well, historically it was a bit clunkier to do this, but modern statically typed languages make this pretty much as easy to do as using a dynamically types language, so how can there be a difference in what the programmer can see as an interesting program? Static languages have exactly the same tagged unions as well as other types.

To answer the question in the title: No, there are no design patterns that can't be implemented in a statically typed language, because you can always implement enough of a dynamic system to get them. There may be patterns that you get for 'free' in a dynamic language; this may or may not be worth putting up with the downsides of those languages for YMMV.

  • 2
    I'm not completely sure whether you just answered yes or no. Sounds more like a no to me. – user7610 Aug 13 '16 at 8:08
  • 1
    @TheodorosChatzigiannakis Yes, how else would dynamic languages be implemented? First, you'll pass for an astronaut architect if you ever want to implement a dynamic class system or anything else a little bit involved. Second, you probably don't have the resource to make it debuggable, fully introspectable, performant ("just use a dictionary" is how slow languages are implemented). Third, some dynamic features are best used when being integrated in the whole language, not just as a library: think garbage collection for example (there are GCs as libraries, but they are not commonly used). – coredump Aug 13 '16 at 9:55
  • 1
    @Theodoros According to the paper I already linked here once, all but 2.5% of structures (in Python modules the researches looked at) can be easily expressed in a typed language. Maybe the 2.5% makes paying the costs of dynamic typing worth it. That's essentially what my question was about. neverworkintheory.org/2016/06/13/polymorphism-in-python.html – user7610 Aug 13 '16 at 10:17
  • 3
    @JiriDanek As far as I can tell, there is nothing that prevents a statically typed language from having polymorphic call spots and maintaining static typing in the process. See Static Type Checking of Multi-Methods. Maybe I'm misunderstanding your link. – Theodoros Chatzigiannakis Aug 13 '16 at 10:22
  • 1
    " A language with dynamic typing lets you express anything as long as it's Turing complete, which most are. " While this is of course a true statement, it doesn't really hold in "the real world" because the amount of text one has to write could be extremely large. – Daniel Jour Aug 13 '16 at 17:02
4

There are surely things you can only do in dynamically typed languages. But they wouldn't necessarily be good design.

You might assign first an integer 5 then a string 'five', or a Cat object, to the same variable. But you're only making it harder for a reader of your code to figure out what's going on, what's the purpose of every variable.

You might add a new method to a library Ruby class and access its private fields. There might be cases where such a hack can be useful but this would be a violation of encapsulation. (I don't mind adding methods only relying on the public interface, but that's nothing that statically typed C# extension methods can't do.)

You might add a new field to an object of somebody else's class to pass some extra data around with it. But it's better design to just create a new structure, or extend the original type.

Generally, the more organized you want your code to stay, the less advantage you should take from being able to dynamically change type definitions or assign values of different types to the same variable. But then your code is no different from what you could achieve in a statically typed language.

What dynamic languages are good at is syntactic sugar. For example, when reading a deserialized JSON object you may refer to a nested value simply as obj.data.article[0].content - much neater than say obj.getJSONObject("data").getJSONArray("article").getJSONObject(0).getString("content").

Ruby developers especially could talk at lengths about magic that can be achieved by implementing method_missing, which is a method allowing you to handle attempted calls to undeclared methods. For example, ActiveRecord ORM uses it so that you could make a call User.find_by_email('joe@example.com') without ever declaring find_by_email method. Of course it's nothing that couldn't be achived as UserRepository.FindBy("email", "joe@example.com") in a statically typed language, but you can't deny it its neatness.

  • 4
    There are surely things you can only do in statically typed languages. But they wouldn't necessarily be good design. – coredump Aug 13 '16 at 9:01
  • 2
    The point about syntactic sugar has very little to do with dynamic typing and everything with, well, syntax. – leftaroundabout Aug 13 '16 at 23:04
  • @leftaroundabout Patterns have everything to do with syntax. Type systems also have a lot to do with it. – immibis Aug 14 '16 at 1:26
4

The Dynamic Proxy pattern is a shortcut for implementing proxy objects without needing one class per type you need to proxy.

class Proxy(object):
    def __init__(self, obj):
        self.__target = obj

    def __getattr__(self, attr):
        return getattr(self.__target, attr)

Using this, Proxy(someObject) creates a new object that behaves the same as someObject. Obviously you'll also want to add additional functionality somehow, but this is a useful base to begin from. In a complete static language, you'd need to either write one Proxy class per type you want to proxy or use dynamic code generation (which, admittedly, is included in the standard library of many static languages, largely because their designers are aware of the problems not being able to do this cause).

Another use case of dynamic languages is so-called "monkey patching". In many ways, this is an anti-pattern rather than a pattern, but it can be used in useful ways if done carefully. And while there's no theoretical reason monkey patching couldn't be implemented in a static language, I've never seen one that actually has it.

  • I think I might be able to emulate this in Go. There is a set of methods that all proxied objects must have (otherwise the duck might not quack and all falls apart). I can create a Go interface with these methods. I'll have to think on it more, but I think what I have in mind will work. – user7610 Aug 15 '16 at 8:26
  • You can something similar in any .NET language with RealProxy and generics. – LittleEwok Aug 21 '16 at 20:59
  • @LittleEwok - RealProxy uses runtime code generation -- like I say, many modern static languages have a workaround like this, but it's still easier in a dynamic language. – Jules Aug 22 '16 at 11:42
  • C# extension methods are kinda like monkey patching made safe. You can't change existing methods, but you can add new ones. – Andrew Piliser Sep 2 '17 at 0:45
3

Yes, there are many patterns and techniques which are only possible in a dynamically typed language.

Monkey patching is a technique where properties or methods are added to objects or classes at runtime. This technique not possible in a statically typed language since this means types and operations cannot be verified at compile time. Or to put it another way, if a language supports monkey-patching it is by definition a dynamic language.

It can be proven that if a language supports monkey patching (or similar techniques to modify types at runtime), it cannot be statically type checked. So it is not just a limitation in currently existing languages, it is a fundamental limitation of static typing.

So the quote is definitely correct - more things are possible in a dynamic language than in a statically typed language. On the other hand, certain kinds of analysis is only possible in a statically typed language. For example you always know which operations are allowed on a given type, which allows you to detect illegal operations at compile type. No such verification is possible in a dynamic language when operations can be added or removed at runtime.

This is why there is no obvious "best" in the conflict between static and dynamic languages. Static languages give up certain power at runtime in exchange for a different kind of power at compile time, which they believe reduces the number of bugs and makes development easier. Some believe the trade-off is worth it, others doesn't.

Other answers have argued Turing-equivalence means anything possible in one language is possible in all languages. But this does not follow. To support something like monkey-patching in a static language, you basically have to implement a dynamic sub-language inside the static language. This is of course possible, but I would argue you are then programming in a embedded dynamic language, since you also lose the static type checking which exist in the host language.

C# since version 4 have supported dynamically typed objects. Clearly the language designers see benefit in having both kind of typing available. But it also shows you can't have your cake and eat i too: When you use dynamic objects in C# you gain the ability do something like monkey patching, but you also lose static type verification for interaction with these objects.

  • +1 your second to last paragraph I think is the crucial argument. I'd still argue that there is a difference though as with static types you have full control of where and what you can monkey patch – jk. Oct 14 '16 at 11:53
2

I wonder, are there useful design patterns or strategies that, using the formulation of the quote, "don't work as types"?

Yes and no.

There are situation in which the programmer knows the type of a variable with more precision then a compiler. The compiler may know that something is an Object, but the programmer will know (due the invariants of the program) that it is actually a String.

Let me show some examples of this:

Map<Class<?>, Function<?, String>> someMap;
someMap.get(object.getClass()).apply(object);

I know that someMap.get(T.class) will return a Function<T, String>, because of how I constructed someMap. But Java is only sure that I've got a Function.

Another example:

data = parseJSON(someJson)
validate(data, someJsonSchema);
print(data.properties.rowCount);

I know that data.properties.rowCount will be a valid reference and an integer, because I've validated the data against a schema. If that field were missing, an exception would have been thrown. But a compiler would only know that is either throwing an exception or return some sort of generic JSONValue.

Another example:

x, y, z = struct.unpack("II6s", data)

The "II6s" defines the way that data encode three variables. Since I've specified the format, I know which types will be returned. A compiler would only know that it returns a tuple.

The unifying theme of all these examples is that the programmer knows the type, but a Java level type system won't be able to reflect that. The compiler won't know the types, and thus a statically typed language won't let me call them, whereas a dynamicly typed language will.

That's what the original quote it getting at:

The wonderful thing about dynamic typing is it lets you express anything that is computable. And type systems don’t — type systems are typically decidable, and they restrict you to a subset.

When using dynamic typing I can use the most derived type I know about, not simply the most derived type my language's type system knows. In all the cases above, I have code which is semantically correct, but will be rejected by a static typing system.

However, to return to your question:

I wonder, are there useful design patterns or strategies that, using the formulation of the quote, "don't work as types"?

Any of the above examples, and indeed any example of dynamic typing can be made valid in static typing by adding appropriate casts. If you know a type your compiler doesn't, simply tell the compiler by casting the value. So, at some level, you aren't going to get any additional patterns by using dynamic typing. You just might need to cast more to get working staticly typed code.

The advantage of dynamic typing is that you can simply use these patterns without fretting about the fact that its tricky to convince your type system of their validity. It doesn't change the patterns available, it just possibly makes them easier to implement because you don't have to figure out how to make your type system recognize the pattern or add casts to subvert the type system.

  • 1
    why is java the cut off point at which you shouldn't go to a 'more advanced/complicated type system'? – jk. Aug 13 '16 at 21:32
  • 2
    @jk, what leads you to think that's what I'm saying? I explicitly avoided taking sides on whether or not a more advanced/complicated type system was worthwhile. – Winston Ewert Aug 13 '16 at 21:50
  • 2
    Some of these are terrible examples, and the others seem to be more language decisions rather than typed vs non-typed. I'm particularly confused at why people think deserialization is so complex in typed languages. The typed result would be data = parseJSON<SomeSchema>(someJson); print(data.properties.rowCount); and if one hasn't got a class to deserialize to we can fall back to data = parseJSON(someJson); print(data["properties.rowCount"]); - which is still typed and expresses the same intent. – NPSF3000 Aug 14 '16 at 6:44
  • 2
    @NPSF3000, how does the parseJSON function work? It would seem to either use reflection or macros. How could data["properties.rowCount"] be typed in a static language? How could it know that the resulting value is an integer? – Winston Ewert Aug 14 '16 at 14:34
  • 2
    @NPSF3000, how do you plan on using it if you don't know its an integer? How do you plan on looping over elements in a list in the JSON without knowing that it was an array? The point of my example was that I knew that data.properties was an object and I knew that data.properties.rowCount was an integer and I could simply write code that used them. Your proposed data["properties.rowCount"] doesn't provide the same thing. – Winston Ewert Aug 14 '16 at 14:46
1

Here are a few examples from Objective-C (dynamically typed) which are not possible in C++ (statically typed):

  • Putting objects of several distinct classes into the same container.
    Of course, this requires runtime type inspection to subsequently interpret the contents of the container, and most friends of static typing will object that you shouldn't be doing this in the first place. But I have found that, beyond the religious debates, this can come in handy.

  • Expanding a class without subclassing.
    In Objective-C, you can define new member functions for existing classes, including language defined ones like NSString. For example, you can add a method stripPrefixIfPresent:, so that you can say [@"foo/bar/baz" stripPrefixIfPresent:@"foo/"] (note the use of the NSSring literals @"").

  • Use of object-oriented callbacks.
    In statically typed languages like Java and C++, you have to go to considerable lengths to allow a library to call an arbitrary member of a user-supplied object. In Java, the workaround is the interface/adaptor pair plus an anonymous class, in C++ the workaround is usually template based, which implies that library code has to be exposed to user code. In Objective-C, you just pass the object reference plus the selector for the method to the library, and the library can simply and directly invoke the callback.

  • I can do the first in C++ by casting to void*, but that is circumventing the type system, so it does not count. I can do the second in C# with extension methods, perfectly within a type system. For the third, I think the "selector for the method" can be a lambda, so any statically typed language with lambdas can do the same, if I understand correctly. I am not familiar with ObjC. – user7610 Aug 16 '16 at 7:11
  • 1
    @JiriDanek "I can do the first in C++ by casting to void*", not exactly, the code that reads elements has no way to retrieve the actual type on its own. You need type tags. Besides, I don't think that saying "I can do this in <language>" is the appropriate/productive way to look at this, because you can always emulate them. What matters is the gain in expressivity vs. complexity of implementation. Also, you seem to think that if a language has both static and dynamic capabilities (Java, C#), it belongs exclusively to the "static" family of languages. – coredump Aug 16 '16 at 8:34
  • 1
    @JiriDanek void* alone is not dynamic typing, it is lack of typing. But yes, dynamic_cast, virtual tables etc. make C++ not purely statically typed. Is that bad? – coredump Aug 16 '16 at 9:26
  • 1
    It suggests that having the option of subverting the type system when needed is useful. Having an escape hatch when you need it. Or somebody considered it useful. Otherwise they would not put it into the language. – user7610 Aug 16 '16 at 11:04
  • 2
    @JiriDanek I think, you pretty much nailed it with your last comment. Those escape hatches can be extremely useful if used with care. Nevertheless, with great power comes great responsibility, and plenty are the people who abuse it... Thus, it feels a lot better to use a pointer to a generic base class that all other classes are derived from by definition (as is the case both in Objective-C and Java), and to rely on RTTI to tell the cases apart, than to cast a void* to some specific object type. The former produces a runtime error if you messed up, the later results in undefined behavior. – cmaster Aug 16 '16 at 18:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.