Design Patterns – Patterns Only Possible in Dynamically Typed Languages

design-patternsdynamic-typingpythontype-systems

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"?

Best Answer

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)

Related Topic