Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Gonna take an unpopular opinion here, but I completely hate Haskell records.

I'm sure that there's some kind of purity to them, but the fact that a two records can't share field names ends up making them incredibly difficult to use for anything practical. Granted, using the makeFields function with Lenses allows you to kind of avoid the clashing, but that in itself feels like somewhat of a hack.



I don't know if I hate Haskell records, but I certainly agree they feel like an ugly hack.

They're essentially just syntactic sugar for a standard product type with auto-generated functions for access and update. Unfortunately those functions come with foot guns. Here's two:

1. As you pointed out, clashing field names (since fields don't truly exist but are rather functions)

2. Partiality of the generated functions if you have multiple constructors for a type, at least one of which is a record.

Lenses are great, but would benefit from first-class integration as the "blessed" way of interacting with records (this hopefully would also reduce their learning curve).

One of my top wishes for Haskell is proper row types a la Purescript. Purescript records and polymorphic variants are amazing. I would absolutely love if this https://github.com/natefaubion/purescript-checked-exceptions was possible in Haskell.


I would definitely support making Lenses an officially-integrated part of the language, since for the most part they do somewhat address my complaints. If they could make the syntax for it feel a little less hackey I think I wouldn't whine at all about records.

I'm not super familiar with Purescript, so I can't comment on the row types; are they more-pleasant than Haskell's?


Yes! Syntactically Purescript essentially automatically gives you lightweight lenses for records. E.g.

  myValue { field0 { subField0 { subSubField0 = 5 }}}
Semantically... oh boy, row types (i.e. extensible product types) and polymorphic variants (i.e. extensible sum types) are fantastic! I'd go so far as to say as long as you give me `newtype`, I would be in favor of row types and polymorphic variants completely replacing `data` declarations.

  -- forall x here means that our record can have other fields
  resetName :: forall x. { name :: String | x } -> { name :: String | x }
  resetName itemWithName = itemWithName { name = "Default" }

  -- Notice how Person is just a type synonym!
  -- And there's no data constructor
  -- { name :: String, id :: Int } is a type, not just a constructor
  type Person = { name :: String, id :: Int }

  type Pet = { name :: String, owner :: Person }

  bob = { name : "Bob", id : 0 }
  
  doggy = { name : "Doggy", owner : bob }

  -- Look they both work!
  bobReset = resetName bob

  doggyReset = resetName doggy

  -- And no clashes, because records are a first class type declaration
  bob.name == "Bob"
  doggyReset.name == "Default"
  
Purescript unfortunately doesn't have built-in polymorphic variants (you can derive them from row types with a bit of type-level hackery), so a bit of this is pseudo-Purescript (mainly the case declaration, functionally you can get the same thing, it just looks slightly different).

  newtype TextTooShort = ...
  newtype TextNotValidASCII = ...

  findFirst5CharWord :: forall e. String -> Either (TextTooShort + e) String

  asciiNumberRepresentation :: forall e. String -> Either (TextNotValidASCII + e) (Array Int)

  processString :: forall e. String -> Either (TextTooShort + TextNotValidASCII + e) (Array Int)
  -- No lifting or wrapping of intermediate errors required!
  -- All the types still unify!
  processString = findFirst5CharWord `andThen` asciiNumberRepresentation

  -- But you still get exhaustivity checking!
  displayString :: String -> String
  displayString input = case (processString input) of
    Left TextTooShort -> "You didn't give me enough text!"
    Left TextNotValidAscii -> "Whoops no ASCII rep exists!"
    Right arrayOfInts -> show arrayOfInts


Functional languages tend to have name resolution as a separate step before type checking. OO languages tend to intermingle name resolution and type checking.

For example in OO languages `obj.func(1, 2)` will typically require the compiler to know the type of obj before knowing which func method is being referred to here.

In Haskell however the equivalent expression is `func obj 1 2`. The compiler has already performed name resolution and knows exactly what func is. So that when doing type-checking, it is not only possible for typing information to propagate from object to method, but also from function to object. That is to say, it is possible that the compiler initially knows what type func has, but has no idea what type obj has; it will only do so after type inference using the type of func.

This makes type inference more general and more elegant. I suspect it also makes the implementation of name resolution easier. Unfortunately as you have pointed out, it's practically more of a nuisance.

Also note that this applies to more than just names of fields associated with a type; it even applies to type names associated with a type. For example if you have

    class G a where
      data N a :: *
The name N is directly usable without even mentioning G. This would require, say in C++, you to write `G::N` to refer to an inner class:

    class G {
    public:
      class N;
    };
The reasoning is of course the same.


For those of us who don't do haskell, can you give us an example of what does not work and then work around?


For an example of clashing:

  data Person = Person {
    id :: Int,
    name :: Text
  }

  data Account = Account {
    id :: Int, # Oh no! Name clash
    amount :: Int
  }
This is because `id` isn't really a "field," among other thing it's a generated function so that

  id :: Amount -> Int
  id (Amount actualId _) = actualId
This particular case is compounded by the fact that `id` is already the name of the identity function in Haskell's standard library (one reason why many projects that roll their own standard library rename `id` to `identity`).

For the partiality:

  data AllowedItems = Person { id :: Int, name :: Text} | JustANormalInteger Int

  name (JustANormalInteger 5) # Blows up at runtime
That is name has the type AllowedItems -> Text, even though JustANormalInteger 5 is also a member of AllowedItems

The workaround for clashing is to prefix your fields with your type name (luckily Haskell still has module namespaces so you don't need a globally unique name, just one unique for your module).

The workaround for partiality is to disallow a type with a record to be anything other than that record (e.g. disallow JustANormalInteger or require that Person {...} must first be an independent type before it can be used in AllowedItems).

The talk of lenses is a way to generalize the notion of field accessors and talk about the "path" generated by a series of field accessors in a first class way. E.g. you might imagine that in a standard OO language

  myClass.field0.subField0.subSubField0
could have a stand-alone concept of `field0.subField0.subsubField0` as a path through `myClass` that you could then use either to get a value (get(field0.subField0.subSubField0, myClass)) or set a value (set(field0.subField0.subSubField0, myClass, newValue)).

Moreover, if you can talk about the paths through a class in a first-class way, what's to say that this path must actually correspond to a true field in a class? It could just be anything "field-like." For example:

  lens = integer.lastDigit.leastSignificantBit
  set(345, lens, 0) // Yields 344
even though the individual digits of myInteger aren't actually generally truly fields in any OO language nor are the individual bits of a number.

Because you have now removed the need for true "fields," lenses are one strategy to get around the hackiness of fields in Haskell and in fact in many ways represent an advance in expressiveness over "true" fields (although they suffer from not really being integrated into the language in the way faux-fields are currently in Haskell).


Whoops... apparently I was reading bash while writing this comment. Comments in Haskell should be `--` not `#`.


> but the fact that a two records can't share field names

That is awkward sometimes yes. Record fields are functions, and you can't have two functions with the same name in the same namespace.

You can, however, use namespaces to import two separate modules with conflicting names. It does mean you have to define the two different records in two different modules.

If, you want different records to have common fields because they are conceptually related, you probably want to use type classes.


> If, you want different records to have common fields because they are conceptually related, you probably want to use type classes.

I'm not a fan of ad-hoc typeclasses. I know that various approaches in modern Haskell advocate for them, but I think that ad-hoc typeclasses are a smell for overuse of typeclasses when plain-old data types should suffice.

I am a very big proponent of demanding that typeclasses have associated laws. Otherwise you can descend into the same hierarchy mess that plagues statically-typed class-based languages. And sometimes you just want to have different fields be named the same thing without an underlying profound relationship between the two.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: