| (I am the designer of Mars.) You make an interesting point. I haven't decided if I agree with you, but here are my thoughts. If I understand you, you're saying that x.v should have type Num, but x.u should have type Maybe(Num) (because u is not totally covered). That sounds like a very bad thing, because it means that if I were to change the type so that u is covered, by changing Y to: Y(u :: Num, v :: Num) then suddenly everywhere I refer to x.u would break, because x.u would now have type Num, not Maybe(Num). If I was to go for the "safe" option, it would have to be that all fields return a Maybe no matter what (and I think that would make fields quite unpleasant to use). I think a better plan (which I've designed but didn't implement -- and I realise that now executing this plan would require a backwards incompatible version of the language) would be to do a simple static analysis of switch statements which records exactly which constructors a variable might have at any given program point, and make it a compile-time error to access a field of a variable unless it is provable that the variable has that field. So in general, x.u would be a compile error. But this code would be legal, and never generate a runtime error: switch x:
case X:
whatever(x.u) You could also do error checking up front to avoid nesting your code too much: switch x:
case Y:
error("blah")
# Guaranteed to have a 'u'.
whatever(x.u) (For the record, I actually didn't blindly copy Haskell's semantics in this instance; I came up with this scheme myself and then only later discovered that Haskell had the exact same scheme!) |
The only case where changing Y is not a "breaking change" is if Y is never exported from the module - one of Haskell's most underused features is the ability to hide constructors and expose custom functions in their place - such functions would easily allow adding to Y (but not change the function y) without breaking the public interface - we need a default value for u though.
Just manually expanding what records do, except you have more control over changes to it - if you expect changes, you probably want to do this. The caveat is that you can no longer pattern match over X/Y from outside the module - unless you expose "isX, isY" functions, and use -XViewPatterns or some other extension. I actually prefer manually expanding things out this way because I consider Haskell's record system to be so poor.It's not just a poor record system, but also the idea of encapsulation. To me, exposing constructors is like making the guts of your classes public in an OOP language - allowing any outsider to access fields really leads to tightly coupled code, and makes it difficult to introduce changes like the one you've suggested.
Consider a trivially modified list class where we want to optimise the performance of `length` if it were used frequently. I could define a data type for it with an extra "Int" field to hold the length.
What would I expose from this module? Almost certainly not the constructors, because I wouldn't want a consumer arbitrarily inserting integers, nor should they need to include it in their pattern matches - what I really want is to expose exactly the same "Cons" and "length" functions as the regular list which doesn't contain the head index. This is really what we want to expose, the constructors themselves don't matter - exposing constructors is only even useful in the cases where the structure of the type maps exactly to the operations we want to perform on it from outside the module, and thus we don't need to "hide" anything. It's not all that common, more often than not you want to hide things (OOP wasn't created by accident). Most haskell programmers think like C programmers - in terms of data rather than the operations you perform on the data.If we continue this OOP analogy to the record syntax, we're treating Foo as some object exposing its public fields and some constructor functions.
Now, if we construct Foo.Y(1), and attempt to access `u`, we get the dreaded "NullReferenceException". In Haskell and Mars we've simply renamed `null` to "Runtime error", but it's effectively the same thing.To me it's a design flaw. Optional obviates the need for null when used properly. I'm not sure what would be the best way to go about it - whether my previous suggestion, or making all fields Maybe, restricting records to types with only one constructor, or just do away with records altogether, because they're ultimately unnecessary. Writing out your own constructor functions gives you more flexibility, and having record syntax is not all that useful compared to say, using lenses.