RE: [Haskell] Per-type function namespaces (was: Data.Set whishes)
| detail, of course ;). What I'm secretly hoping is that the | GHC/hugs/HBC people will see what I'm trying to achieve, tell me I'm | totally nuts, and then suggest an alternative, much simpler approach | which gives us exactly the same goal ... As I implied earlier, I am thus far unconvinced that the complexity cost justifies the benefit. Let me suggest two simpler alternatives. Alternative 1 ~~~~~~~~ Given import FiniteMap( FM, add {- :: FM a b -> a -> b -> FM a b -} ) and fm :: FM Int Bool, you want fm.add x y to mean the same as add fm x y (but in addition allow lots of unrelated 'add' functions) My objection is that the type of 'fm' is a matter of inference, whereas in Java it'd be a matter of explicit declaration. But you could adapt your story to be more explicit, thus FM.add fm x y The qualifier here is the *type* not the *module*. (Let's assume they share a name space for now.) This would work for record selectors too: data T = T { x::Int, y::Int } data S = S { x::Int, y::Int } Now f p q = T.x p + S.y q I guess the rule is that you can qualify a function by the name of the outermost type constructor of its first argument. I would restrict this only to functions with explicitly-declared types, so that there's no interaction with inference. Alternative 2 ~~~~~~~~ If the big bug-bear is record selectors, let's focus on them exclusively. I now think that ML got it right. ML records are simply labelled tuples. So just as (Bool,Int) is an anonymous type, so is {x::Bool, y::Int}. Indeed (Bool,Int) is just shorthand for {#1::Bool, #2::Int}. In Haskell, a function with a tuple argument takes exactly that tuple: f :: (Bool,Int) -> Bool f (x,y) = x Here, f cannot take a 3-tuple or a 89-tuple. In ML it's the same with records (I'm not getting the ML syntax right, but you'll get the idea): f :: {x::Bool, y::Int} -> Bool f r = r.x Here f cannot take a record of shape other than {x::Bool,y::Int}. You are allowed to write f {x, ...} = x but you must then explain f's argument type separately. For example you can write f :: {x::Bool, p::String, q::Bool} -> Bool f {x, ...} = x So there is no sub-typing, no row polymorphism, no attempt to give f r = r.x a fancy type that makes f applicable to any record with an x field. On the other hand, there is also no problem with many records having the same field name either, which is the problem we started with. There are no implicitly-defined record selectors either: you have to use pattern matching for that. My personal view is this: we should have adopted the ML view of records. It solves the immediate problem, and no more elaborate scheme seems sufficiently "right" to be declared the winner. Alas, like all other proposals, it's not backward compatible, and hence not likely to fly. Simon
Simon Peyton-Jones wrote:
If the big bug-bear is record selectors, let's focus on them exclusively. I now think that ML got it right. ML records are simply labelled tuples.
Note that this is true only for SML, not for Caml.
So just as (Bool,Int) is an anonymous type, so is {x::Bool, y::Int}. Indeed (Bool,Int) is just shorthand for {#1::Bool, #2::Int}.
A bit of nitpicking: (Bool,Int) would be shorthand for {1::Bool,2::Int}. In SML, labels may be numeric or alpha-numeric. OTOH, the hash is the projection operator (ASCII art for \pi), which can be used for both kinds of labels: #2 (x,y,z) #b {a=x, b=y, c=z} Actually, #l is just syntactic sugar for (\{l=x,...}->x), which implies that you might need type annotations.
There are no implicitly-defined record selectors either: you have to use pattern matching for that.
Or projection using #. Cheers, - Andreas -- Andreas Rossberg, rossberg@ps.uni-sb.de Let's get rid of those possible thingies! -- TB
Hi While we're talking about SML, I think there are a few other things worth stealing... 1) I still miss multiline pattern matching in lambda (fn p1 => e1 | p2 => e2 | ...) not strictly necessary, but often less disruptive of the text than either a let/where-introduced helper-function or \ x -> case x of ... especially when there's more than one argument. Doubtless there's some deep reason why Haskell doesn't have these that I'm too young to remember. 2) I miss local implementation-decls in interface-decls end Is there some fabulous layout rule for `where' clauses that means the same thing? 3) I miss `open' from the module system. This allows let open Structure in expression end local open Structure in declarations end reducing your program's acne. I often wonder why the OO world has no room for Pascal's lovely old `with ... do ...'. [Pascal even had variant records with case expressions: it wasn't remotely type-safe, but it looked like datatypes. What happened to them? Programmers like sums, but software engineers like products...] Haskell already has per-module namespaces; everything has a unique long name; good start. Now what we need is more control over the meaning of short names. Could we have local opening of (already imported) modules, simply rebinding the relevant short names in their scope? You'd get the localized namespace you might want at the cost of one scoped declaration, rather than a zillion projections. And while we're at it...
Simon Peyton-Jones wrote:
If the big bug-bear is record selectors, let's focus on them exclusively. I now think that ML got it right. ML records are simply labelled tuples.
The pattern-matching was always dreadful, especially if you just wanted to tweak one field, but I expect that could be dealt with. Andreas Rossberg wrote:
Note that this is true only for SML, not for Caml.
So just as (Bool,Int) is an anonymous type, so is {x::Bool, y::Int}. Indeed (Bool,Int) is just shorthand for {#1::Bool, #2::Int}.
A bit of nitpicking: (Bool,Int) would be shorthand for {1::Bool,2::Int}. In SML, labels may be numeric or alpha-numeric. OTOH, the hash is the projection operator (ASCII art for \pi), which can be used for both kinds of labels:
#2 (x,y,z) #b {a=x, b=y, c=z}
Actually, #l is just syntactic sugar for (\{l=x,...}->x), which implies that you might need type annotations.
That's a neat piece of syntax, but if you're willing to look at types, you might be able to introduce an opening operator for records, <| say, at the cost of restricting field labels to being valid names. ie if r :: {a::A, b::B, c::C} r <| e means let {a=a,b=b,c=c} = r in e Again, the usual projection is a special case. No reason why you couldn't use this opening notation for the existing field-labelled datatypes. OK, you'd be shadowing projection functions with field names, but that's not so shocking. This opens a further, if questionable, possibility, viz data MyQuad = MyQuad {a::Int, b::Int, c::Int} <| f x = (a * x + b) * x + c declaring a computed field f for that constructor. As usual, only your conscience prevents you misusing this facility in a multi-constructor type. For backward compatibility, you'd need to leave a, b and c as global names, but you could choose to make f accessible only on opening. That leaves open the possibility of data MyQuad = MyQuad {globa::Int, globb::Int, globc::Int} <| a = globa b = globb c = globc f :: Int -> Int f x = (a * x + b) * x + c which I'm sure could be sugared to data MyQuad = MyQuad {<| a::Int, <| b::Int, <| c::Int} <| f :: Int -> Int f x = (a * x + b) * x + c meaning that the fields should not have global names, only local names on opening. Or one could add `methods' to the whole type, being functions on that type, defined by pattern matching, but with one argument left of <| data Fred = F1 | F2 | F3 with cycle :: Fred F1 <| cycle = F2 F2 <| cycle = F3 F3 <| cycle = F1 Again, cycle wouldn't be globally declared. Goodness me, it's a per-type namespace, but not for `ordinary' application. There's a catch, of course. When you write r <| e you give the typechecker no clue as to the type of r: it just has to infer the type of r and hope it's a datatype. I suggest this is perfectly sustainable, given that (1) your function already has a top-level type signature, hasn't it? (2) this sort of thing happens all the time with ad-hoc polymorphism anyway; when you have class Blah x where blah :: x -> x instance Blah (Maybe Int) where blah Nothing = Just 0 blah (Just x) = Just (x + 1) what's blah Nothing ? Is this plausible? Conor
Conor McBride wrote: [...]
There's a catch, of course. When you write
r <| e
you give the typechecker no clue as to the type of r: it just has to infer the type of r and hope it's a datatype.
This is reminiscent of an issue I encountered a year or so ago, when designing a language. The main example I used was a function for calculating the magnitude of a two dimensional vector. It would be nice to simplify this magnitude v = sqrt ((v <| x)*(v <| x) + (v <| y)*(v <| y)) to this magnitude v = v <| sqrt (x*x + y*y) but how was the compiler to know that x and y are fields of v, but sqrt and (+) and (*) aren't? What I ended up doing was making a rule that if the record expression (the part to the left of the <|) could not be statically tracked to something which revealed the field names, then the right hand side (of the <|) must contain exactly one free variable. This static tracking took place before type inference, so it was in line with Simon PJ's preference for keeping scope and type inference separate. I also used some syntactic sugar for explicit record narrowing, so the final version of the magnitude function was magnitude v = v(.x, .y) <| sqrt (x*x + y*y) which was quite similar to Cayenne's "open ... use ... in ..." feature. Regards, Tom Pledger
On Thu, Mar 04, 2004 at 09:21:23AM -0000, Simon Peyton-Jones wrote:
My personal view is this: we should have adopted the ML view of records. It solves the immediate problem, and no more elaborate scheme seems sufficiently "right" to be declared the winner. Alas, like all other proposals, it's not backward compatible, and hence not likely to fly.
About a year ago, you were toying with a simple polymorphic system with just "has" predicates. If these were automatically derived, it seems you'd get something quite close to backward compatible, except for the pesky extra lifting in record types, and not being able to omit fields when constructing the record. (And update might not statically check that all fields belong to the same constructor, for the simple version of the type system.) Is that just too clunky?
participants (5)
-
Andreas Rossberg -
Conor McBride -
Ross Paterson -
Simon Peyton-Jones -
Tom Pledger