To be clear, this would involve the static type system for your language forcing certain things to happen, or not compiling. The sequence:
1. fileOpen returned a Maybe<FileObject>
2. You want to store the FileObject somewhere, and it expects an actual FileObject.
3. Maybe<FileObject> is not itself a FileObject, so you have to figure out: is it Nothing, or a FileObject?
4. You have to handle the Nothing case (language forces you to, even if you do nothing about it - you explicitly decided to do nothing).
5. If it is indeed a FileObject, you store it for later use, and you are guaranteed it is, indeed, a FileObject.
And I should note that I don't necessarily dislike the optional types and such. I just also don't have the major disdain for null that the internet seems to send through a massive echo chamber nightly.
That's a problem with Java (because it doesn't let you do that) and a similar problem exists with dynamic languages (because types aren't attributes of variables, so any variable could be null -- or any other value, null isn't particularly exceptional in dynamic languages for being a source of this kind of problem), but its not a problem "with languages that have null", as there are statically typed languages that have null but distinguish nullable and non-nullable types (C# and other static .NET languages are probably the most well-known examples, though there are others.)
> But really, at that point it's just sugar for an option type.
No, having a the free choice not use nullable types doesn't make the languages nullable types into sugar for option types. Nullable types don't compose the way option types do -- the issue in the article this thread is about isn't about whether or not you have the choice for nullable types, its about the fact they don't compose so you can distinguish a function returning a null as a substantive value because the thing you tried to store was also a nullable type and had a null value (Just None, in a world with Options instead of nullable types) or where the null is a result of the lookup failing (None, in a world with Options instead of nullable types.)
The deficiency of nullable types in C# compared to Options isn't the lack of choice of writing non-nullable types -- as you certainly have that choice for any data you need to represent -- its that nullabe types don't compose the way Options do.
Well, any value type -- but, yes, a custom type has to be a struct to not be nullable.
> How do you specify that a function can take a non-nullable string?
"string" in C# is the name of a particular reference (and hence, nullable) type. If you want a function that takes a non-nullable sequence of characters, that's quite possible, but the type of its argument won't be string. (The fact that there is no such type predefined and the entire .NET framework library is built around the assumption that if you want a sequence of characters you are going to use the built-in, reference -- and therefore nullable -- string type is an issue with the cost, in programmer-time, of doing this for any substantial use, but its not a "the language doesn't let you have a non-nullable type for any kind of data you want" issue.)
The disdain is probably (allow me to project) because it is a totally unnecessary problem which can be solved easily with virtually no drawbacks.
Granted, this can certainly be done with optional types. So, I'm not ultimately against them. I just think they are oversold quite a lot.
Laziness doesn't come into it, since you won't even manage to compile your code, let alone run it, if you try to use a "Maybe FileHandle" when a "FileHandle" is expected.
With that said, laziness does cause problems related to file handles, but those have nothing to do with unhandled failures. The problem is that files are a limited resource which we should release as soon as possible (just like memory, sockets, etc.). Lazy evaluation is at odds with this, since it essentially defers function calls to the last possible moment. One way of thinking about it is that laziness is "call-by-need", but we never really need the return values of functions like "closeFile".
Edited to add:
Note that this can be the case in Haskell when you're relying on exceptions for error handling. That's the opposite approach from option types, though.
So, depending on just how "lazy" the program truly is, sure you passed it something that unwraps a Maybe, but only once you actually go to use it. That make sense?
Consider this fun bit of nonsense scala
def foo(x:=>String): Unit = {
println("Hello")
println(x)
}
If I call this as "foo(None.get)", then it will print "Hello" before barfing. In fact, if I didn't evaluate x, it wouldn't even barf. I had thought the laziness of Haskel was that ++. (Meaning I thought if I stored x off to another also lazy reference, it wouldn't barf right away.)To make that a little more clear of what I meant, some even more amusing scala.
class Foo(x: => String) {
def happy = println("Doing good.")
def sad = println(x)
}
If I create something with "new Foo(None.get)", I can call happy on this object all day long. If I ever call sad, I get an exception.Apologies for the ninja edit above, but this scala seems to show it isn't just IO that is at danger here.
class Foo(x : =>String) {
def happy = println("I'm good.")
lazy val ack = x
def sad = println(ack)
}
Again, if I do "new Foo(None.get)", I can call happy as much as I want, but "sad" will blow up.Now, I do not and could not claim that this sort of nonsense would have proliferated with the same abandon as null. I am claiming that this is not that unlike a null pointer exception, though.
case maybe_string of
Just string -> newFoo string
Nothing -> defaultFoo
Or to use one of the relevant higher-order functions: fmap newFoo maybe_string :: Maybe Foo
maybe defaultFoo newFoo maybe_string :: Foohttp://research.microsoft.com/en-us/um/people/simonpj/papers...
This is also why the Control.Exception module exposes `evaluate`, which embeds a value in an IO computation and thus resolves this problem... If you remember to use it.
http://hackage.haskell.org/package/base-4.7.0.0/docs/Control...
Fortunately, in idiomatic Haskell exceptions are considered to be an expert feature (basically I see them in concurrent IO or resource management code only) and partial functions—usually introduced by incomplete pattern matching like `None.get` does—are veboten.
It's not perfect, but -Wall will help with that.
My point here was more that just because you have a file handle doesn't mean you have a valid place to write data. Making it optional would possibly prevent some bugs, true. However, it doesn't really help as soon as you have a filehandle to a full filesystem, for example. (Or, well, any other problem that usually happens to cause grief with the filesystems. You got a file, but by the time you went to use it the system was full and you couldn't, etc.)
Regardless, I should have been clearer on many points (and I fully accept I was flat out wrong on at least a few :) ). I do think null pointers are bad. I also know with proper static analysis tools, you don't need a whole new type system to make things better. Unless you are considering tools such as Coverity and friends some form of type system.
I would, for the record.
There was a thread some days ago (https://news.ycombinator.com/item?id=7916554) where I described some hackary I used to get particular guarantees out of C's type system. My response in https://news.ycombinator.com/item?id=7918423 seems tangentially relevant here. Type systems aren't (only/necessarily) features of a language.
I think the only other differences stem ultimately from standardization. Tooling could exploit any type system a program is known to follow. Likewise, compiler optimizations, &c, &c.
Well, I realize that is false. But the hoops all of the examples I have seen that types have to jump through to accomplish that aren't exactly pleasant.
So, maybe my preference is just that tooling can give you many of the benefits of more advanced type systems, without having to have all of the boilerplate of it everywhere in the code.
At any rate, I should say thank you very much for keeping this dialog going. Been a blast!
By "tooling" there, I wasn't referring to static analysis. I meant things like type-directed name resolution, type-directed completion, Agda's "holes", and so on. Strictly, these do not rely on the type system being "a part of" the language, but they do rely on some degree of standardization on a particular type system for the tool to work (or work well).
"Interleaved calls to different types, for example, can be found through tooling."
I actually don't understand exactly what you mean here. You might be surprised what even pretty rudimentary type systems can find if you leverage 'em right, though - as I mentioned in that other thread, I track which threads access what resources, and which functions live on which threads, with C's type system unadorned by additional checks (I do use additional static analysis, it just is entirely unnecessary to catch this one).
"Well, I realize that is false. But the hoops all of the examples I have seen that types have to jump through to accomplish that aren't exactly pleasant.
"So, maybe my preference is just that tooling can give you many of the benefits of more advanced type systems, without having to have all of the boilerplate of it everywhere in the code."
Ironically, I think my C code has more annotations for my static analysis tool-of-choice than the C types. Certainly there are a lot of poor type systems, and a lot of languages that expose particular type systems poorly. Boilerplate type annotations are mostly unnecessary in languages with type inference.
As I've said elsewhere, though, your tooling may be "more advanced type systems". Splint, for instance, can enforce linear typing - something that is not a part of the type system laid out in the C standard (or that enforced by typical C compilers, even with warnings).
"At any rate, I should say thank you very much for keeping this dialog going. Been a blast!"
Yes, it's turned out great :)
That is, at the point that a Maybe String statically changes to a String is not necessarily where the program will explode. A lot like a null assign. (Again, in a lazy language. Unless I'm mistaken on this subthread, the point was simply that you can still have that level of WTFery with Maybe that you can have with a null.)
Now, rereading the beginning, the reference was to a "Haskel like" type. So, I think I was definitely off the rails on this. I do feel I learned something going down this particular rabbit hole, though. Thanks!
There is a function, fromMaybe, which takes a Maybe x and gives you an x... but it also takes a default!
Often even more useful is the maybe function:
maybe :: output -> (input -> output) -> Maybe input -> output
This takes a default output to use if you're given Nothing, a function to apply to the input and generate an output if you're given Just an input, and Maybe the input, and gives you the resulting output. As I said this is often more useful than "and use this default", because quite often the value you want to generate when it's Nothing is outside the image of the input under the function in question. For instance: maybe "no number" show maybe_integer
There's no number I can pass to show to get "no number", maybe lets me very conveniently avoid trying to force one.