It doesn't have to be. You can implement 95%ish of duck typing in a static language without that much work. The last 5% you need dependent typing for and it's much more complicated, but that's still a statically typed system. Specifically, the remaining 5% is for conditionally using methods depending on other values, and so having code that still works on data that does not support all of the methods you code conditionally calls.
Here's a chunk of Haskell that I was tinkering with and it implements something very much like static duck typing:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances,
UndecidableInstances, IncoherentInstances, FlexibleContexts #-}
data Addon a b = Addon a b deriving Show
class Plugin a b where
first :: b -> a
set :: a -> b -> b
instance Plugin a a where
first = id
set = const
instance Plugin a (Addon a b) where
first (Addon a _) = a
set x (Addon a b) = Addon x b
instance (Plugin a b) => Plugin a (Addon c b)where
first (Addon _ b) = first b
set x (Addon a b) = Addon a $ set x b
class Runnable e b c where
run :: e -> b -> c
instance Plugin a b => Runnable (a -> c) b c where
run f p = f (first p)
instance (Plugin d b, Runnable e b c) => Runnable (d -> e) b c where
run f p = run (f $ first p) p
-- The constructor function should NOT be polymorphic *at all*
apply constructor f x = set (constructor $ run f x) x
The Plugin typeclass exists to express containment of types within other types. A type which is an instance of Plugin a b
means that it is a type b
for which first b
can be a value of type a
. The two instances given are all that is needed to derive all relevant cases and no further instance declarations are needed.
The Runnable typeclass allows us to chain Plugin instances together and so call a curried multi-argument function on a single datatype so long as it contains a valid component type for each curried argument of the function. Again, no further instance declarations are needed for usage.
The apply function allows us to take a composite type, apply a function to it, and store the result inside that composite type (so long as it already contains a value of the type we wish to store)
Usage would look like so:
data Increment = Inc Integer
data Counter = Count Integer
counter1 = union (Inc 2) (Count 3)
counter2 = union (Inc 1) (Count 0)
increment (Inc i) (Count c) = i + c
And indeed, this compiles, and we can run it and observe the output:
*Main> apply Count increment counter1
Addon (Inc 2) (Count 5)
*Main> apply Count increment counter2
Addon (Inc 1) (Count 1)
Granted, we had to turn on some rather ugly type extensions, but those extensions are not required to use the library, only to write it. The constructor argument to apply cannot be polymorphic because that would overload the type inference system and it would be unable to make the necessary intermediate type derivations. I was trying to get around that, but I have not made any progress in that direction.
operator++
andoperator!=
(loop increment and bounds check) can function as an iterator, because it walks and quacks like an iterator. This could be one of the many iterator classes defined by the STL, or even a bare pointer.java.util.Iterator
you are an iterator." C++ says "if you waddle and quack like an iterator, you are one." Simple and good example of the two types of typing.