10 ms·
The thing is, it's not that straightforward. It's not about avoiding type errors that would have cropped up in Ruby, but about getting the type system to encode
by chc 15y ago
The thing is, it's not that straightforward. It's not about avoiding type errors that would have cropped up in Ruby, but about getting the type system to encode as much of your program's semantics as possible. For example, in Ruby, you use strings and symbols for a lot of disparate things. In Haskell, you'd introduce a type for each purpose to encode your intent in a way the compiler understands†. In Haskell, you're actually going out of your way to create more potential type errors, because that's more stuff the compiler can check for you.
† 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.)
- mtrimpe 15y agoI agree very much. 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.
- masklinn 15y agoI don't know: 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.
- riffraff 15y agohow would a nominally different string type be better than wrapping the string ? 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.
- masklinn 15y ago> how would a nominally different string type be better than wrapping the string ? * 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
- mtrimpe 15y agoI must agree with the nullable one. Let's say the biggest failure that I haven't heard anyone else talk about. 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.
- pjmlp 15y agoWhy a failure? String is final in most languages OO languages.
- Produce 15y agoI would consider having any final classes in a standard library to be a failure. One of the pillars of OOP is extensibility.
- pjmlp 15y agoThen you should learn about the fragile base class problem, http://www.cas.mcmaster.ca/~emil/Publications_files/MikhajlovSekerinski98FragileBaseClassProblem.pdf 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.
- deleted 15y ago[deleted]
- soc88 15y agoReplied to the wrong comment?
- scanr 15y agoIt's amusing that in a thread about the advantages of Haskell, immutability is being identified as the largest failure a language has made. 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
- ben0x539 15y agoA class being final means that you can't derive from it in Java, not that instances of the class are immutable.
- healthyahoo 15y agohttp://healthyahoo.net/weight_loss/10-tips-for-fast-weight-loss.html http://healthyahoo.net/weight_loss/10-tips-for-fast-weight-l...
- cashew 15y agoTo ensure immutability of a type in Java, you typically need to prevent it from being extended.
- yxhuvud 15y agoBiggest failure? try Integer(100) == Integer(100) and Integer(200) == Integer(200)
- grey-area 15y agoFor example, in Ruby, you use strings and symbols for a lot of disparate things. In Haskell, you'd introduce a type for each purpose to encode your intent in a way the compiler understands 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.
- zoul 15y agoBut 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. 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.
- frio 15y agoYou can start with simple types in Haskell too, FWIW. To take your phone number example, let's say I'm making a very simple program that dials a number. In Ruby, I'm talking about something like: 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 :).