Records in Haskell

Gábor Lehel illissius at gmail.com
Wed Jan 18 14:11:23 CET 2012


On Wed, Jan 18, 2012 at 1:09 PM, Matthew Farkas-Dyck
<strake888 at gmail.com> wrote:
> On 18/01/2012, Greg Weber <greg at gregweber.info> wrote:
>> On Fri, Jan 13, 2012 at 8:52 PM, Simon Peyton-Jones
>> <simonpj at microsoft.com>wrote:
>>
>>>
>>> But, the Has constraints MUST exist, in full glory, in the constraint
>>> solver.  The only question is whether you can *abstract* over them.
>>> Imagine having a Num class that you could not abstract over. So you
>>> could write
>>>
>>>   k1 x = x + x :: Float
>>>   k2 x = x + x :: Integer
>>>   k3 x = x + x :: Int
>>>
>>> using the same '+' every time, which generates a Num constraint. The
>>> type signature fixes the type to Float, Integer, Int respectively, and
>>> tells you which '+' to use.  And that is exactly what ML does!
>>>
>>> But Haskell doesn't.  The Coolest Thing about Haskell is that you get
>>> to *abstract* over those Num constraints, so you can write
>>>
>>>  k :: Num a => a -> a
>>>  k x = x + x
>>>
>>> and now it works over *any* Num type.
>>>
>>> On reflection, it would be absurd not to do ths same thing for Has
>>> constraints.  If we are forced to have Has constraints internally, it
>>> woudl be criminal not to abstract over them.  And that is precisely
>>> what SORF is.
>>>
>>
>> So I understand that internally a Has constraint is great for resolving the
>> type.
>> What is the use case for having the Has abstraction externally exposed?
>>
>> I think there is a great temptation for this because we would have a
>> functionality we can point to that has some kind of extensible record
>> capability.
>>
>> But I believe the Has abstraction to be a form of weak-typing more so than
>> a form of extensibility. Just because 2 records have a field with the same
>> name, even with the same type in no way signifies they are related. Instead
>> we need a formal contract for such a relation. We already have that in type
>> classes, and they can already be used for this very capability in a more
>> type-safe way.
>>
>> My point is that Has abstractions are weak types and that likely we should
>> be searching for something stronger or using type classes. If I am wrong
>> then we should have convincing use cases outlined before we make this a
>> goal of a records implementation, and still I don't see why it needs to be
>> a blocking requirement if we are just trying to solve the basic records
>> issue.
>>
>>
>> Greg Weber
>>
>
> Has *is* a type class. It can be used and abused like any other.
> Record members with the same key ought to have the same semantics; the
> programmer must ensure this, not just call them all "x" or the like.
>
> Weak types these are not. The selector type is well-defined. The value
> type is well-defined. The record type is well-defined, but of course
> we define a type-class to let it be polymorphic.

I think the concern is -- similarly to "duck typing" -- that unrelated
modules or libraries might unintentionally choose the same name for
their record fields. This doesn't seem so theoretical. That they would
also choose the same -type- is less likely, but the possibility is
there. (Especially for things like, say, size :: Int, or name ::
String, or that sort of thing.)

The way type classes solve this for functions and methods, as compared
to duck typing, is that you have to explicitly declare an interface
(type class) - it's not merely implied by the name and type - and for
types you have to explicitly declare that they support that interface
(with an instance). Classes and their methods are distinguished not
just by their name, but also by the module they were defined in.

So the analogous thing for records, I think, if you want to be
super-safe about it, would probably be to explicitly declare the
existence of a record field/selector/projector (do we have an agreed
upon terminology?) -- I'm not sure whether the type would be declared
for the selector or in the record...

module ModuleA where

selector fieldA :: Int
selector fieldB :: a

...and in a potentially different module...

module ModuleB where
import ModuleA

data SomeRecord = SomeRecord { fieldA, fieldB :: String } -- again, I
have no idea where or how the types should be handled

...and in a third module...

foo :: Has r fieldA (Int?) => r -> Int
foo r = r.fieldA

If there were more than one 'selector fieldA' declaration in scope,
you would have to disambiguate them with the module name in the same
way as all other things:

foo :: Has r ModuleA.fieldA (Int?) => r -> Int
foo r = r.(ModuleA.fieldA)

I haven't thought this through and am not in any way recommending it
(especially not any of the specific details); I just saw a parallel.

(I *am*, however, uncomfortable with using straight-up type level
strings, without consideration for any particular alternative. If
nothing else they should at least be opaque symbols which can be
passed around and used in the supported contexts but not manipulated
as strings. String-based hackery should be left to Template Haskell,
and out of the type system. I can't really express at the moment why
in particular I think it would be bad, but it feels like it would be
bad.)



More information about the Glasgow-haskell-users mailing list