Agree with this 100%. I think dynamically typed languages are a transitional technology we'll mostly leave behind as the kinks get worked out of modern type systems.
Agree with this 100%. I think dynamically typed languages are a transitional technology we'll mostly leave behind as the kinks get worked out of modern type systems.
I can count on one hand the number of type issues which have caused trouble for me in Ruby over > 5 years of producing complex systems with it. I haven't used Haskell though so would be interested to see some examples of this. If his audience is Ruby users, it is not very informative to just say it is better in a general way. The main point of comparison in the article point 1 seems to be with unit tests, however there are no concrete examples given of a failing test from a real program where types would have helped. A strong type system adds friction and verbosity, so it would need to come with a lot of advantages for me to prefer it to Ruby, which will not allow something like 2 + "2" but does try to get out of your way as much as possible.
It's quite hard to compare type systems as they vary so much but at least here is he comparing just two systems Ruby/Haskell, so comparisons can be straightforward examples of situations where types have caused him trouble with direct comparisons. I'd be interested to see an example of something like a telephone number (from untrusted user input) where the Haskell type system is superior to having some checks on formatting, and an explanation for those less familiar with Haskell of what advantages the type system gives.
† Concrete example off the top of my head: In Ruby, we do `foo.instance_variable_get(:@bar)`. If we accidentally write `foo.instance_variable_get(:bar)`, that's a hard error, but it isn't a type error. Haskellers would generally express a constraint like that with the type system, so the compiler would let them know when they made such a mistake.
(Also, don't forget that every unintended nil is a type error! If you've been doing heavy Ruby work for years and gotten fewer than six NoMethodErrors, I will hang my head in shame.)
But then in Ruby if you want to you can encapsulate behaviour and your intent in objects instead of types - as concepts become more complex, you may introduce an object which encapsulates the data and provides checked interfaces for it. Taking the example of a telephone number, you might define a PhoneNumber Class in Ruby which encodes your intent and enforces an interface in much the same way as a Type in Haskell might (?), but if your use of telephone numbers is simply as an unformatted string, you don't have to introduce that complication initially. A static typing system is not the only way to encode that sort of information is it?
I do find the example above somewhat puzzling, as idiomatic ruby would be more like foo.bar if you want the value of bar (which should have an accessor defined if you are allowed to read it). What I was hoping for was an example of the two languages side by side demonstrating some small mistake which leads to errors or unintended consequences because of a lack of static typing in Ruby.
If you're counting no method errors as type errors then I must hang my head in shame :) Usually those are caught before going into production though by either unit tests or normal testing. You catch typos (with or without strong typing) on compilation in a compiled language, but you have to catch them with testing at runtime in an interpreted one. But is that really related to static typing or compiled versus interpreted?
I'm intrigued by the enforced types of Haskell though, so this article is an interesting starting point in the comparison, and thanks for trying to explain it.
I think the idea is that a strong static type system will allow you to describe the constraints idiomatically and concisely, whereas describing them using the usual OOP tools gets overkill very quickly.
aNumber = "+0123456789"
def dial(number)
# does the dialling
return someConnection
end
dial(aNumber)
(It's been a long time since I've done any Ruby, so forgive any glaring syntax faults.) That method expects a string - aNumber is an example of which - but there's no annotation to explain that to the runtime (Ruby doesn't have a compiler in the sense we refer to one here).In Haskell, something similar might look like this (I'm going to be verbose and specify types here, but the compiler can actually infer a few of them):
aNumber :: String
aNumber = "+0123456789"
dial :: String -> PhoneConnection
dial number = -- a function which does some dialing
-- somewhere else, the above is called as
someConnection = dial aNumber
The "::" lines are type signatures. The first says that the variable aNumber is a String. The second says that the function "dial" takes a string argument, and returns an instance of the PhoneConnection type (in real Haskell, there'd be some IO monad stuff wrapping it, but we can happily ignore that for now).Type signatures aren't generally used when defining variables, and Haskell can generally infer them for simple functions - but they're very useful when writing code, as alongside informing the compiler of our intent, they inform other developers of what the function does. Anyone coming along in future can easily see that they need to pass the "dial" function a String, for instance, and will get back a PhoneConnection. In Ruby, however, other developers either need to hope you've documented it, or inspect your code to figure out what it expects.
FWIW, in Java, the function definition/type signature might look like:
public PhoneConnection dial(String number) {}
Now, let's say I want to make things a little more obvious to people reading my code. I can make the following nips/tucks: type PhoneNumber = String
aNumber :: PhoneNumber
aNumber = "+0123456789"
dial :: PhoneNumber -> PhoneConnection
dial number = -- a function which does some dialing
Here, we've used Haskell's type aliasing. All this does is say that the type PhoneNumber is the same as a String. It's not really useful at this point, but it means that it's a little more clear what the dial function requires. Developers inspecting it will see that the PhoneNumber type is really a string, but they can see what it is that String is supposed to store (of course, they still have no idea of intent).There's no real analogy for Java here - it'd be like saying something like:
public class PhoneNumber extends String {}
(Note that: the Java code doesn't define any new functions for PhoneNumber, the Haskell code isn't really object extension, and the Java String class is final so you can't do this anyway. It's not really like doing that at all, but hopefully it illustrates the point).Now, let's say I want to change this String into an object, to make it more robust.
In Ruby:
class PhoneNumber
def initialize(countryCode, areaCode, number)
@countryCode = countryCode
@areaCode = areaCode
@number = number
end
end
The code inside the dialling function will also change, but the actual function definition _doesn't_ - and any code calling that "dial" function won't be aware that it needs to change. It's still: def dial(number)
# ...
end
dial(aNumber) # which might still be our string, so at runtime, we're going to crash!
In Haskell, you might do something like: data PhoneNumber = PhoneNumber { countryCode :: Int, areaCode :: Int, number :: Int }
aNumber = PhoneNumber 01 23 456789
dial :: PhoneNumber -> PhoneConnection
dial number = -- do stuff
Now, because you have that type definition, anything that still uses String numbers will cause compilation to fail. Your code will not produce an executable that you can run. Which is a Good Thing(tm)! Because it means that you've prevented a whole class of runtime error/crash.So, yeah. Static typing is cool because (with a compiler) it helps you catch and prevent runtime errors. Haskell's is particularly cool because it's very terse, and very flexible (I haven't really got into it here, fwiw - this was a very basic example :)) - which prevents some of the extreme annoyances you face dealing with Java's verbose (and, thanks to generic type erasure, slightly broken) type system.
PS, as a little extra - Nil/NullPointerException type stuff is difficult to cause in Haskell. When you say a function returns a type - PhoneConnection - it must return that type. If there's a chance of it erroring and returning Nil/null, you return a "Maybe" type which encapsulates it instead. That means, of course, updating your type signature:
dial :: PhoneNumber -> Maybe PhoneConnection
Which, in turn, means that before you can extract any data out of the PhoneConnection,you explicitly must check that there's actually something there you can work with - which is very, very cool :).I think the concrete examples really help show up the differences between the languages here. I might quibble with your initialisation in ruby (I'd expect it to be initialised with a string and hide the internal representation), but it is clear here why the Haskell type system might help you avoid issues with changing an interface and forgetting to change things which call it (though I can't say I've run into that a lot in Ruby, I can see where it might be useful, particularly with large groups collaborating or a large program).
I would argue that the compilation step is what is responsible for finding errors at compile time rather than runtime, but then the type system is perhaps required to enforce that.
It is. A Ruby (or Python, or JavaScript, ...) source code inspector cannot, in general, infer concrete types for variables. You're allowed to say (JS example)
var x = "fred";
x = 3;
and now, if you try to write a JavaScript inspector to infer the types so it could check that you used x correctly, it would not be able to.Of course, there are cases where you can infer useful things about your code, but for most real-world JS code, it would produce a lot of false positives or false negatives, and wouldn't be very useful. (Think about monkeypatching...)
The bit where dynamic languages get really complicated is that they allow for you to use types that hadn't been taken into consideration when the language was designed. For example, in Python you can write code using duck typing and it will Just Work(TM) but if you want to do something equivalent in Haskell you would need tell GHC to use one of those weird language extensions just to have the program typecheck. The monkeypatching you mention goes more along this line, I think.
I kind of agree with your second sentiment - but I did want to point out that it's entirely possible to write a dynamically typed compiled language too.
It boils down to type systems: weak/strong, and dynamic/static. C, for instance, is weak/static: you define types, but you can basically pass around whatever you want to:
#include<stdio.h>
int main(int argc, char* argv[]) {
int result = add(1, 2);
printf("%d", result);
return 0;
}
int add(int a, char b[]){
return a + b;
}
The above will happily compile - and run. The method header for "add" says it takes an integer and a character array - essentially, a String in C (for people who actually use C: I've avoided pointers because they're confusing, and my C is even rustier than my Ruby).Inside that method though, I do something entirely braindead: I add the integer and character array as if that were an operation that makes sense. Then I call "add" with two integers anyway, and let it do what it wants. It actually does return 3: rather than throwing a runtime error to say "hey, this is stupid, I should have a String here and you can't add a String to an integer!", it just trundles merrily along. A strong type system wouldn't let you compile or run this: it'd tell you off for trying to call a function with invalid arguments, then (hopefully) tell you off for using "+" in a nonsensical way.
FWIW, the following also works:
#include<stdio.h>
int main(int argc, char* argv[]) {
int result;
char aString[] = {'h', 'e', 'l', 'l', 'o'};
result = add(1, aString);
printf("%d", result);
return 0;
}
int add(int a, char b[]){
return a + b;
}
The number returned and printed here is derived from the location in memory of the characters you've written. I think. Either way, it makes no sense (for most people, anyway - C hackers may have some use for such an operation).At the other end of the spectrum, Python and Ruby are strong/dynamic: you can pass whatever types around you want, but the interpreter will crash if you try to do something the type system doesn't allow. You can't do 1 + "test": it makes no sense. It won't even try to run it and return something nonsensical, it just won't run.
All that said: I don't know if there are any strong/static languages without a compiler. I can't seem to think of an application of such a language: the best way to exploit a strong/static type system is to have it tell you, up-front, that you've made a mistake (and, of course, have your compiler optimize the pants off your code for runtime).
I also don't know of any languages which are compiled, but fully dynamically typed (Scala, maybe? Although it allows static typing too). Considering the benefits you can gain from type analysis during compilation, again, the concept seems a bad fit. Although (same as in the previous example), you could happily write a language that is!
So, yes, compiling the code is what enables you to perform those checks - but a strong type system is a part of compilation in its own right :).
Haskell style => function applied to parameter, e.g f(x) Ruby duck type style => parameter applies function to itself e.g x.f()
An example:
module Dialable
def dial
# do stuff with self
puts "Dialling..."
end
end
# I wouldn't do this, but...
class String
include Dialable
end
# So, phone number as a string can be dialled
my_phone_number = "01 23 45678"
my_phone_number.dial
Dialling...
# I'd rather do this
class PhoneNumber < String
include Dialable
end
my_phone_number = PhoneNumber.new "01 23 45678"
my_phone_number.dial
Dialling...
# now you get an error on non phone number strings
"just a random string".dial
NoMethodError: undefined method `dial' for "just a random string":String
my_phone_number == "01 23 45678"
=> true
my_phone_number == "just a random string"
=> false
You get all the regex and string functions for free too.As far as I'm concerned Java's biggest failure, orders of magnitude worse than all others, is to make java.lang.String a final class.
1. having nullable references by default strikes me as a bigger issue
2. I don't see any reason I'd want to subclass String, actually (though I could see wanting to create an alternate implementation e.g. ropes-based). So String being a final class makes perfect sense as far as I'm concerned (unless it were an interface or some sort of "proxy" class as is often done in Cocoa). On the other hand, I'd give a phalange to easily create an unrelated type (typesystem-wise) with the same implementation. What `newtype` provides in Haskell.
As per your #2, that's what I was hinting at except that the implementation effort of final vs. non-final is negligible, thus making it harder to excuse as far as I'm concerned.
I mean I understand why having a Name, ZipCode or UUID stringish class helps ensure program correctness, I do not understand (out of ignorance) how it would improve your code vs a wrapper.
* It is significantly less verbose, therefore simpler and more likely to be used at all. And less error-prone
* It can provide string APIs working on itself (either by default or through the aliasing declaration) precluding the need to manually re-implement things like comparisons or printing
I don't think immutable Strings are a bad idea but I do think it's unfortunate that Java:
- Made Strings immutable, used them everywhere, and only later worked out that perhaps CharSequence would have been better in a lot of places.
- Didn't provide a sensible way to handle Object extensions
String is final in most languages OO languages.
http://www.cas.mcmaster.ca/~emil/Publications_files/Mikhajlo...
API design is a very complex issue. Any change in a base class can have unintended consequences.
Specially in components sold as libraries to development companies, where you as a customer don't have access to the source code.
You're right, one of the OOP pillars is extensibility, but inheritance is just one way of doing it.
In my experience a good type system doesn't add much friction and the extra documentation you get from specifying types more than pays for itself in the long run.
Sure, all problems can be solved in theory with things like meticulous documentation, rigorous coding standards that everybody follows and lots of detailed tests. But in practice those sort of informal constraints will break down over years as developers of varying skill levels come and go. Better to formally force as much of that as possible with a decent type system.
Nowadays I will take a static language with automatic type inference over a dynamic language anytime, preferably one that with direct support for FP.
The IDE support, code navigation and refactoring, and runtime performance just beat dynamic languages all the time.
For the few use cases where dynamic types really make a difference, that can be supported by some kind of variant type or reflection.
Its like how most vehicle accidents involving impact speeds of over 300 MPH involve planes and not cars or boats. Its not that cars and boats are safer..
In the enterprise projects where I work, usually with around 200+ developers scattered around the globe, it is unthinkable to use dynamic languages in such environments.
We already have issues with static languages in the CI system. I cannot think how it would look like doing the project in a dynamic language.
For smaller projects however, with smaller teams, dynamic languages do have less overhead, which is perhaps why they are so popular for web apps.
54 years old transitional technologies?
Look the split between dynamic typing and static typing is as old as computing itself (older actually, see untyped v typed lambda calculi), as it looks to me you're asserting properties your prover can't cash.
Maybe my background in chemistry colors my view but I don't see 54 years as really such a long time for any intellectual discipline. We're still in the early days of software engineering, IMO.
While my favorite language right now is Haskell due to its sophisticated type system and ambitious design goal of enabling engineers to coral and isolate error-causing code into monads, I have to disagree.
Dynamic languages can be looked at as the next evolution of non-memory-managed -> memory-managed. Next is statically/strongly typed -> dynamically-typed. In both cases you go from manual management of some feature of the language, to having the vm or compiler do it for you, and that increasing abstraction and automation is the story of language development (and technology in general).
Non-memory-managed languages like C, C++, ObjC are still around and are used by highly skilled developers to squeeze the utmost performance out of the system, Chromium/Chrome/V8 being a good everyday example.
But where that kind of optimization is not strictly needed or where the automatic memory management of, say the Hotspot JVM, is sufficient, memory-managed languages will flourish. Further, memory-managed languages benefit from the continuous improvement in vm technology, and over time continue to approach non-managed in many situations.
I expect something similar to happen with type systems. Where a particular project does not need the strict control provided by a type system, dynamically typed languages will flourish (as they are in the startup web space).
Perhaps one day, vm's and dynamic type inference will be so good that neither manual memory-managed languages nor strongly typed ones will be necessary,but given Haskell's design goals and the type of difficult problems it's trying to solve (software assurance, security, etc.) I don't expect that will be anytime soon. They'll all continue to coexist for the foreseeable future.
Having said that, Haskell's type system is a pleasure to use, and imho actually increases productivity by both requiring and helping you think more clearly about the data flowing through your code.
When compilers/runtimes become smart enough, you won't need a static type system, because the runtime one will catch all your errors.
Here's why I think it's wrong:
Catching errors is not something that I want done at runtime.
And I agree, I tend to prefer type systems like haskell that offer better compile time assurance than testing, but my point is there's a large contingent of programmers who don't, and who drive uptake of increasingly automated and abstracted dynamically typed languages (this isn't a prediction, but an observation of how things already are).
the thesis is that there are places where the type system just gets in the way, and there are places where it is invaluable.
[1] http://phillyemergingtech.com/2012/system/presentations/Horr...