Actually, path-dependent types are orthogonal to structural vs nominal typing. It's not really clear what an inner class means in the context of a simple structurally-typed language. It is, however, very possible to define this. If you were to define inner classes in a structurally typed context, you would need to ensure that cases like the one you listed would be rejected (for precisely the same reasons that Scala rejects them).
You would reject such cases by doing the same thing that Scala does: model the path-dependent type as an existential type. The same pack/unpack procedure surrounding object access would hold, and the results would look almost identical to what Scala does. The results may seem like a nominal type equality, but it would still be a structural type system since the question of type compatibility will still be decided on interface rather than name.
Structural typing does have a lot of implications, but (perhaps surprisingly) most of the same concepts we all know and love from nominal type systems do carry over into structural. Structural typing is nothing more than a different way of defining type compatibility.
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)
Best Answer
In a dynamically typed system, values have types at runtime but variables and functions do not. In a statically typed system, variables and functions have types known and checked at compile-time. E.g. in Python
x
can be anything; at runtime, if it is1
it's a number and if it is"foo"
, it's a string. You would only know which typex
was at runtime, and it could be different each time you ran the program. In a language like Java, you would writeint x
ifx
was to be a number, and you would know at compile time thatx
always has to be anint
."Explicit" and "implicit" types both refer to static type systems. The defining characteristic of a static system is that the types are known at compile time, but not necessarily that they have to be written out. In Java, types are explicit--you have to write them out. So in Java, a method might look something like:
The types are both known at compile time (static) and written out (explicit). However, there are also languages that do not force you to write the type out. They can infer the type of a function from its body and how it is used. An example would be OCaml, where you can write something like:
Since you used
+
, OCaml can figure out thatx
has to be anint
all on its own. So the type offoo
(foo : int -> int
) is known at compile time, just like the Java example. It is entirely static. However, since the compiler can figure out what the types have to be on its own, you do not have to write them out yourself: they're implicit.In short: whether a type system is explicit or implicit is a property of static systems. It is a completely different question from whether a type system is dynamic or static.
Often, you have type systems that are at times explicit and at times implicit.
For example, I believe C# lets you infer types using the
var
keyword. So instead of writingint x = 10
, you can writevar x = 10
and the compiler figures out thatx
has to be anint
. C++ does something similar withauto
. These systems are usually explicit but have some inference.On the flipside, there are systems that are usually implicit but sometimes force you to write out a type signature. Haskell is a great example. Most of the time, Haskell can infer the types for you. However, sometimes you can write code that is ambiguous like
show . read
, where Haskell cannot figure out the types on its own. In this case, you would be forced to explicitly specify the type of eithershow
orread
. Additionally, some more advanced features of the type system (like rank-n polymorphism) make inference undecidable--that is, it is not guaranteed to halt. This means that code using this feature often needs explicit type signatures.