There was a time, not long ago, where these were the kind of choices we had to make: Do I want it to be fast and easy to write the program, but sometimes there are more runtime bugs? Or do I want to be protected from more of those bugs at compiletime, but at the cost of having to waste more time writing more boilerplate?
That's over now. Dynamic languages were extremely exciting back when they were the only way to avoid adding a lot more bureaucracy to your code, but dynamic typing is now being obsoleted by type inference. So you're actually right -- dynamic typing doesn't add value anymore, now that more powerful strong type systems are catching on.
Huh? Where? How?
Languages like Haskell, the MLs, Scala, Rust, and some of the .NET family combine strong typing with type inference so you get the best of both: everything has a strong, enforced type, but you don't have to actually write it down unless the compiler can't figure it out, which it usually can.
Don't ignore them for being academic. Things have changed. Those kinds of languages with powerful typing systems have been steadily getting much more popular over the last few years. (2) And, I'm finding it more and more rare to hear about a brand new language these days that doesn't sport a combination of strong typing for safety with type inference for convenience, at least to some degree. Today's brand new esoteric languages are next decade's "enterprise standards" so I think everyone will be using this best-of-both-worlds approach eventually.
Even old man Java is moving slowly in that direction! (examples: diamond operator; inferring the functional interface for lambda expressions)
Learning dynamic languages for the first time now is too late, it's like if throughout the 1990s you were putting off replacing your tape deck with a CD player and then finally around 2002 you get around to buying a CD player.
EDIT: I reread your question. I sympathize with your desire for concrete code. Here's a Rosetta stone. A generic procedure to check whether a list is sorted or not, and code that calls it. The code that calls it does the right thing every day and the wrong thing on leap days, as an example of how compiletime errors win big over runtime errors when you have a bug in rarely-executed code. Code is untested, just a sketch. In particular, I'm still learning Haskell (hence the zeal of the newly converted... :P) and I haven't used Python as heavily as I used to for a couple years, so forgive
Traditional static typing:
// Java 7 - strong typing but no type inference
// Notice that you get safety, because the wrong usage is caught at compile time
// But you have to type a lot :-(
static interface Comparable { boolean compare(Comparable other); }
...
boolean isListSorted( List<T extends Comparable> xs ) {
T prev = null; for( T x: xs ) {
if( prev!=null && !prev.compare(xs) ) return false;
prev = x;
}
return true;
}
...
public final class String implement Comparable { ... }
public final class Animal /* definitely does not implement Comparable! :-) */ { ... }
...
// proper usage
List<String> names = Lists.newArrayListOf("Red", "Spinelli", "Ankh", "Morporkh", "Sam");
boolean areTheNamesSorted = isListSorted(names);
if(todayIsALeapDay()) {
// bad usage -- the compiler catches it, so you don't have to worry about this happening
// in production when a user enters data that send your app down a rarely-touched codepath
List<Animal> pets = Lists.newArrayListOf( Animal.getCat(), Animal.getDog(), Animal.getBird() );
boolean areTheNamesSorted = isListSorted(pets);
}
Dynamic typing:
class Animal:
...
# Python 2.6 -- dynamic typing
# notice how little keystrokes are needed
# but the bug is now
def isListSorted(xs):
# to keep it more similar to the Java7 version, zip is avoied
prev = None
for x in xs:
if prev is not None and not prev.compare(x): return False
prev = x
return True
...
# usage -- beautiful, isn't it?
names = ["Raph", "Argh", "Marge"]
areNamesSorted = isListSorted(names)
if isLeapDayToday():
# ...but here comes trouble in paradise!
animals = [Animal.getCat(), Animal.getDog()]
# raises a runtime exception. I hope your unit tests catch it, but unlike type
# systems it's hard to make absolutely sure you'll be alerted if you forget a test
# besides, then you've just traded the Evil of Having to Write Lots of Type Signatures
# for the Evil of Having to Write Lots of Unit Tests. What kind of terrible deal is that?
areAnimalsSorted = isListSorted(animals)
Powerful typing:
-- Haskell - full safety of strong typing, but easy on the fingers and eyes
class Comparable a where
compare :: a -> a -> Bool
end
instance Comparable String where
...
end
data Animal = ...
isListSorted [] = True
isListSorted x:[] = True
isListSorted x1:x2:xs = (compare x1 x2) && (isListSorted xs)
names = ["Raplph", "Argh", "Blarge"]
areNamesSorted = isListSorted names
animals = [Cat, Dog, Parrot]
-- compile time error because [Animal] is not compatible with Comparable a => [a] since
-- Animal is not an instance of comparable
areAnimalsSorted = isListSorted animals
Verbose powerful typing:
-- Same Haskell program as before, but we explicitly write down the types
-- Just for educational purposes. Like comments, you can omit them if you don't think
-- the reader needs them because it's obvious. Unlike comments, the compiler verifies their truth!
class Comparable a where
compare :: a -> a -> Bool
end
instance Comparable String where
...
end
data Animal = Cat | Dog | Parrot
isListSorted :: Ord a => [a] -> Bool
isListSorted [] = True
isListSorted x:[] = True
isListSorted x1:x2:xs = (compare x1 x2) && (isListSorted xs)
names :: [String]
names = ["Raplph", "Argh", "Blarge"]
areNamesSorted = isListSorted names
-- compile time error because [Animal] is not compatible with Comparable a => [a] since
-- Animal is not an instance of comparable
animals :: [Animal]
animals = [Cat, Dog, Parrot]
areAnimalsSorted = isListSorted animals