3 ms·
Something that bugs me in Haskell, and which Mars inherits as it is mostly the same semantics - you've taken the effort to create a good type system with option
by sparkie 12y ago
Something that bugs me in Haskell, and which Mars inherits as it is mostly the same semantics - you've taken the effort to create a good type system with optional types and type checking, only to decide to not actually use them for some common operations you're performing in the language (namely, head/tail).
?> Nil.head
Runtime Error: List instance has no field 'head'
Such runtime errors could easily be avoided by making head/tail return Maybe, and using Nothing in the case of Nil. This hardly complicates code, but it provides the correct behavior, particularly when it comes to doing "records" properly. The Mars docs give an example of how I consider copying Haskell's semantics directly without improving them leads to flawed code.
type Foo:
X(u :: Num, v :: Num)
Y(v :: Num)
There's nothing wrong with `v` here, as it is total, but you can easily introduce a runtime error by using `u` incorrectly.
?> x = Y(1)
?> x.u
Runtime Error: Foo instance has no field 'u'
This is why using records in combination with sum types in Haskell is widely considered a bad idea. It's a misfeature as far as I'm concerned. If given the chance to redo Haskell's awful record system, I'd fix it.
The problem can be trivially avoided by making `u` return `Maybe Num` in this example, where it returns Nothing for Y and `Just 1` for X. Ideally, the compiler should check if field names are total over the constructors - if not, it should automatically return an optional type. If the fields are total there's no need. I doubt there would be any noticeable performance cost to moving the error to compile time - particularly because you don't actually need to use it in internal implementations of map/fold and such - you could have an internal function which mimics the current behavior, but don't expose it from the module.
- tel 12y agoYeah, the Prelude in Haskell has a lot of warts. Interestingly, if you use some of the Template Haskell derivations for lenses you get this record-of-sum optionality resolved at least partly.
- mgiuca 12y ago(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!)
- sparkie 12y agoI'll give that I didn't give thought to what would happen if you later added u, but on the other hand, I'd consider that changing Y (if you expose it from the module) is a breaking change in any circumstance. If we consider for example, a Haskell equivalent with pattern matching. data Foo = X { u :: Int, v :: Int } | Y { v :: Int } bar :: Foo -> ... bar (X u v) = ... bar (Y v) = ... If "u" were then added to Y, this pattern matching would break - the names of the fields don't matter in this case - the arity does. We just can't add to Y here without going through every pattern match on Foo to fix it. 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. module Foo (Foo, x, y) where data Foo = X Int Int | Y Int Int x :: Int -> Int -> Foo x = X y :: Int -> Foo y = Y defaultU defaultU = 0 u :: Foo -> Maybe Int u (X a _) = Just a u _ = Nothing v :: Foo -> Int v (X _ b) = b v (Y b) = b 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. data LList a = Nil | Cons Int a (LList a) 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. cons :: a -> LList a -> LList a cons a Nil = Cons 1 a Nil cons a (Cons n h t) = Cons (n + 1) a (Cons h t) length :: LList a -> Int length Nil = 0 length (Cons n _ _) = n 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. public class Foo { public Int u; public Int v; private Foo(Int u, Int v) { this.u = u; this.v = v; } private Foo(Int v) { this.v = v; } public static Foo X(Int u, Int v) { return new Foo(u, v); } public static Foo Y(Int v) { return new Foo(v); } } 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.